diff --git a/.github/ISSUE_TEMPLATE/----.md b/.github/ISSUE_TEMPLATE/----.md
index 1d0e97226..fb9f8bfd6 100644
--- a/.github/ISSUE_TEMPLATE/----.md
+++ b/.github/ISSUE_TEMPLATE/----.md
@@ -1,11 +1,35 @@
---
name: 需求建议
about: 提出针对本项目的想法和建议
-title: "[Feature] "
+title: "[Feature] 需求标题"
labels: 类型:需求
assignees:
- ibuler
- baijiangjie
---
-**请描述您的需求或者改进建议.**
+## 注意
+_针对过于简单的需求描述不予考虑。请确保提供足够的细节和信息以支持功能的开发和实现。_
+
+## 功能名称
+[在这里输入功能的名称或标题]
+
+## 功能描述
+[在这里描述该功能的详细内容,包括其作用、目的和所需的功能]
+
+## 用户故事(可选)
+[如果适用,可以提供用户故事来更好地理解该功能的使用场景和用户期望]
+
+## 功能要求
+- [要求1:描述该功能的具体要求,如界面设计、交互逻辑等]
+- [要求2:描述该功能的另一个具体要求]
+- [以此类推,列出所有相关的功能要求]
+
+## 示例或原型(可选)
+[如果有的话,提供该功能的示例或原型图以更好地说明功能的实现方式]
+
+## 优先级
+[描述该功能的优先级,如高、中、低,或使用数字等其他标识]
+
+## 备注(可选)
+[在这里添加任何其他相关信息或备注]
diff --git a/.github/ISSUE_TEMPLATE/bug---.md b/.github/ISSUE_TEMPLATE/bug---.md
index 491a6ba80..2da1561ed 100644
--- a/.github/ISSUE_TEMPLATE/bug---.md
+++ b/.github/ISSUE_TEMPLATE/bug---.md
@@ -1,22 +1,51 @@
---
name: Bug 提交
about: 提交产品缺陷帮助我们更好的改进
-title: "[Bug] "
+title: "[Bug] Bug 标题"
labels: 类型:Bug
assignees:
- baijiangjie
---
-**JumpServer 版本( v2.28 之前的版本不再支持 )**
+## 注意
+**JumpServer 版本( v2.28 之前的版本不再支持 )**
+_针对过于简单的 Bug 描述不予考虑。请确保提供足够的细节和信息以支持 Bug 的复现和修复。_
+
+## 当前使用的 JumpServer 版本 (必填)
+[在这里输入当前使用的 JumpServer 的版本号]
+
+## 使用的版本类型 (必填)
+- [ ] 社区版
+- [ ] 企业版
+- [ ] 企业试用版
-**浏览器版本**
+## 版本安装方式 (必填)
+- [ ] 在线安装 (一键命令)
+- [ ] 离线安装 (下载离线包)
+- [ ] All-in-One
+- [ ] 1Panel 安装
+- [ ] Kubernetes 安装
+- [ ] 源码安装
+## Bug 描述 (详细)
+[在这里描述 Bug 的详细情况,包括其影响和出现的具体情况]
-**Bug 描述**
+## 复现步骤
+1. [描述如何复现 Bug 的第一步]
+2. [描述如何复现 Bug 的第二步]
+3. [以此类推,列出所有复现 Bug 所需的步骤]
+## 期望行为
+[描述 Bug 出现时期望的系统行为或结果]
-**Bug 重现步骤(有截图更好)**
-1.
-2.
-3.
+## 实际行为
+[描述实际上发生了什么,以及 Bug 出现的具体情况]
+
+## 系统环境
+- 操作系统:[例如:Windows 10, macOS Big Sur]
+- 浏览器/应用版本:[如果适用,请提供相关版本信息]
+- 其他相关环境信息:[如果有其他相关环境信息,请在此处提供]
+
+## 附加信息(可选)
+[在这里添加任何其他相关信息,如截图、错误信息等]
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
index 3be4afd1b..3f7d673f0 100644
--- a/.github/ISSUE_TEMPLATE/question.md
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -1,10 +1,50 @@
---
name: 问题咨询
about: 提出针对本项目安装部署、使用及其他方面的相关问题
-title: "[Question] "
+title: "[Question] 问题标题"
labels: 类型:提问
assignees:
- baijiangjie
---
+## 注意
+**请描述您的问题.**
+**JumpServer 版本( v2.28 之前的版本不再支持 )**
+_针对过于简单的 Bug 描述不予考虑。请确保提供足够的细节和信息以支持 Bug 的复现和修复。_
+
+## 当前使用的 JumpServer 版本 (必填)
+[在这里输入当前使用的 JumpServer 的版本号]
+
+## 使用的版本类型 (必填)
+- [ ] 社区版
+- [ ] 企业版
+- [ ] 企业试用版
+
+
+## 版本安装方式 (必填)
+- [ ] 在线安装 (一键命令)
+- [ ] 离线安装 (下载离线包)
+- [ ] All-in-One
+- [ ] 1Panel 安装
+- [ ] Kubernetes 安装
+- [ ] 源码安装
+
+## 问题描述 (详细)
+[在这里描述你遇到的问题]
+
+## 背景信息
+- 操作系统:[例如:Windows 10, macOS Big Sur]
+- 浏览器/应用版本:[如果适用,请提供相关版本信息]
+- 其他相关环境信息:[如果有其他相关环境信息,请在此处提供]
+
+## 具体问题
+[在这里详细描述你的问题,包括任何相关细节或错误信息]
+
+## 尝试过的解决方法
+[如果你已经尝试过解决问题,请在这里列出你已经尝试过的解决方法]
+
+## 预期结果
+[描述你期望的解决方案或结果]
+
+## 我们的期望
+[描述你希望我们提供的帮助或支持]
-**请描述您的问题.**
diff --git a/.gitignore b/.gitignore
index 985f77580..b8fff0d67 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,3 +43,4 @@ releashe
data/*
test.py
.history/
+.test/
diff --git a/Dockerfile-ce b/Dockerfile-ce
index 850c56218..622532f6d 100644
--- a/Dockerfile-ce
+++ b/Dockerfile-ce
@@ -111,8 +111,17 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \
&& sed -i "s@# export @export @g" ~/.bashrc \
&& sed -i "s@# alias @alias @g" ~/.bashrc
+ARG RECEPTOR_VERSION=v1.4.5
+RUN set -ex \
+ && wget -O /opt/receptor.tar.gz https://github.com/ansible/receptor/releases/download/${RECEPTOR_VERSION}/receptor_${RECEPTOR_VERSION/v/}_linux_${TARGETARCH}.tar.gz \
+ && tar -xf /opt/receptor.tar.gz -C /usr/local/bin/ \
+ && chown root:root /usr/local/bin/receptor \
+ && chmod 755 /usr/local/bin/receptor \
+ && rm -f /opt/receptor.tar.gz
+
COPY --from=stage-2 /opt/py3 /opt/py3
COPY --from=stage-1 /opt/jumpserver/release/jumpserver /opt/jumpserver
+COPY --from=stage-1 /opt/jumpserver/release/jumpserver/apps/libs/ansible/ansible.cfg /etc/ansible/
WORKDIR /opt/jumpserver
diff --git a/README_EN.md b/README_EN.md
index 7f302195e..16da1a8ca 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -85,7 +85,7 @@ If you find a security problem, please contact us directly:
- 400-052-0755
### License & Copyright
-Copyright (c) 2014-2022 FIT2CLOUD Tech, Inc., All rights reserved.
+Copyright (c) 2014-2024 FIT2CLOUD Tech, Inc., All rights reserved.
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
diff --git a/apps/accounts/api/automations/backup.py b/apps/accounts/api/automations/backup.py
index b2d6d7352..6810087f7 100644
--- a/apps/accounts/api/automations/backup.py
+++ b/apps/accounts/api/automations/backup.py
@@ -18,9 +18,8 @@ __all__ = [
class AccountBackupPlanViewSet(OrgBulkModelViewSet):
model = AccountBackupAutomation
- filter_fields = ('name',)
- search_fields = filter_fields
- ordering = ('name',)
+ filterset_fields = ('name',)
+ search_fields = filterset_fields
serializer_class = serializers.AccountBackupSerializer
diff --git a/apps/accounts/api/automations/base.py b/apps/accounts/api/automations/base.py
index 70ebfd709..a9692eae6 100644
--- a/apps/accounts/api/automations/base.py
+++ b/apps/accounts/api/automations/base.py
@@ -20,8 +20,8 @@ __all__ = [
class AutomationAssetsListApi(generics.ListAPIView):
model = BaseAutomation
serializer_class = serializers.AutomationAssetsSerializer
- filter_fields = ("name", "address")
- search_fields = filter_fields
+ filterset_fields = ("name", "address")
+ search_fields = filterset_fields
def get_object(self):
pk = self.kwargs.get('pk')
diff --git a/apps/accounts/api/automations/change_secret.py b/apps/accounts/api/automations/change_secret.py
index f1c522fc4..05ee515dc 100644
--- a/apps/accounts/api/automations/change_secret.py
+++ b/apps/accounts/api/automations/change_secret.py
@@ -6,9 +6,12 @@ from rest_framework.response import Response
from accounts import serializers
from accounts.const import AutomationTypes
+from accounts.filters import ChangeSecretRecordFilterSet
from accounts.models import ChangeSecretAutomation, ChangeSecretRecord
from accounts.tasks import execute_automation_record_task
+from authentication.permissions import UserConfirmation, ConfirmType
from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet
+from rbac.permissions import RBACPermission
from .base import (
AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi,
AutomationNodeAddRemoveApi, AutomationExecutionViewSet
@@ -24,35 +27,54 @@ __all__ = [
class ChangeSecretAutomationViewSet(OrgBulkModelViewSet):
model = ChangeSecretAutomation
- filter_fields = ('name', 'secret_type', 'secret_strategy')
- search_fields = filter_fields
+ filterset_fields = ('name', 'secret_type', 'secret_strategy')
+ search_fields = filterset_fields
serializer_class = serializers.ChangeSecretAutomationSerializer
class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet):
- serializer_class = serializers.ChangeSecretRecordSerializer
- filterset_fields = ('asset_id', 'execution_id')
+ filterset_class = ChangeSecretRecordFilterSet
search_fields = ('asset__address',)
tp = AutomationTypes.change_secret
+ serializer_classes = {
+ 'default': serializers.ChangeSecretRecordSerializer,
+ 'secret': serializers.ChangeSecretRecordViewSecretSerializer,
+ }
rbac_perms = {
'execute': 'accounts.add_changesecretexecution',
+ 'secret': 'accounts.view_changesecretrecord',
}
+ def get_permissions(self):
+ if self.action == 'secret':
+ self.permission_classes = [
+ RBACPermission,
+ UserConfirmation.require(ConfirmType.MFA)
+ ]
+ return super().get_permissions()
+
def get_queryset(self):
return ChangeSecretRecord.objects.all()
@action(methods=['post'], detail=False, url_path='execute')
def execute(self, request, *args, **kwargs):
- record_id = request.data.get('record_id')
- record = self.get_queryset().filter(pk=record_id)
- if not record:
+ record_ids = request.data.get('record_ids')
+ records = self.get_queryset().filter(id__in=record_ids)
+ execution_count = records.values_list('execution_id', flat=True).distinct().count()
+ if execution_count != 1:
return Response(
- {'detail': 'record not found'},
- status=status.HTTP_404_NOT_FOUND
+ {'detail': 'Only one execution is allowed to execute'},
+ status=status.HTTP_400_BAD_REQUEST
)
- task = execute_automation_record_task.delay(record_id, self.tp)
+ task = execute_automation_record_task.delay(record_ids, self.tp)
return Response({'task': task.id}, status=status.HTTP_200_OK)
+ @action(methods=['get'], detail=True, url_path='secret')
+ def secret(self, request, *args, **kwargs):
+ instance = self.get_object()
+ serializer = self.get_serializer(instance)
+ return Response(serializer.data)
+
class ChangSecretExecutionViewSet(AutomationExecutionViewSet):
rbac_perms = (
diff --git a/apps/accounts/api/automations/gather_accounts.py b/apps/accounts/api/automations/gather_accounts.py
index e6a846368..e125ed96b 100644
--- a/apps/accounts/api/automations/gather_accounts.py
+++ b/apps/accounts/api/automations/gather_accounts.py
@@ -20,8 +20,8 @@ __all__ = [
class GatherAccountsAutomationViewSet(OrgBulkModelViewSet):
model = GatherAccountsAutomation
- filter_fields = ('name',)
- search_fields = filter_fields
+ filterset_fields = ('name',)
+ search_fields = filterset_fields
serializer_class = serializers.GatherAccountAutomationSerializer
diff --git a/apps/accounts/api/automations/push_account.py b/apps/accounts/api/automations/push_account.py
index e8832815b..1fa5c1219 100644
--- a/apps/accounts/api/automations/push_account.py
+++ b/apps/accounts/api/automations/push_account.py
@@ -20,8 +20,8 @@ __all__ = [
class PushAccountAutomationViewSet(OrgBulkModelViewSet):
model = PushAccountAutomation
- filter_fields = ('name', 'secret_type', 'secret_strategy')
- search_fields = filter_fields
+ filterset_fields = ('name', 'secret_type', 'secret_strategy')
+ search_fields = filterset_fields
serializer_class = serializers.PushAccountAutomationSerializer
diff --git a/apps/accounts/automations/backup_account/handlers.py b/apps/accounts/automations/backup_account/handlers.py
index 6a00a2436..e2a0cdb3c 100644
--- a/apps/accounts/automations/backup_account/handlers.py
+++ b/apps/accounts/automations/backup_account/handlers.py
@@ -6,7 +6,7 @@ from django.conf import settings
from rest_framework import serializers
from xlsxwriter import Workbook
-from accounts.const.automation import AccountBackupType
+from accounts.const import AccountBackupType
from accounts.models.automations.backup_account import AccountBackupAutomation
from accounts.notifications import AccountBackupExecutionTaskMsg, AccountBackupByObjStorageExecutionTaskMsg
from accounts.serializers import AccountSecretSerializer
diff --git a/apps/accounts/automations/change_secret/custom/ssh/main.yml b/apps/accounts/automations/change_secret/custom/ssh/main.yml
index 8ff38475f..42201392b 100644
--- a/apps/accounts/automations/change_secret/custom/ssh/main.yml
+++ b/apps/accounts/automations/change_secret/custom/ssh/main.yml
@@ -18,6 +18,8 @@
become_user: "{{ custom_become_user | default('') }}"
become_password: "{{ custom_become_password | default('') }}"
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
+ old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
+ gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
register: ping_info
delegate_to: localhost
@@ -54,4 +56,6 @@
become_user: "{{ account.become.ansible_user | default('') }}"
become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
+ old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
+ gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
delegate_to: localhost
diff --git a/apps/accounts/automations/change_secret/host/aix/main.yml b/apps/accounts/automations/change_secret/host/aix/main.yml
index e86319ce1..fa8d0f54f 100644
--- a/apps/accounts/automations/change_secret/host/aix/main.yml
+++ b/apps/accounts/automations/change_secret/host/aix/main.yml
@@ -85,6 +85,7 @@
become_user: "{{ account.become.ansible_user | default('') }}"
become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
+ old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "password"
delegate_to: localhost
@@ -95,5 +96,6 @@
login_user: "{{ account.username }}"
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
+ old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key"
delegate_to: localhost
diff --git a/apps/accounts/automations/change_secret/host/posix/main.yml b/apps/accounts/automations/change_secret/host/posix/main.yml
index 6ac4b7aa9..a77347f00 100644
--- a/apps/accounts/automations/change_secret/host/posix/main.yml
+++ b/apps/accounts/automations/change_secret/host/posix/main.yml
@@ -85,6 +85,7 @@
become_user: "{{ account.become.ansible_user | default('') }}"
become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
+ old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "password"
delegate_to: localhost
@@ -95,5 +96,6 @@
login_user: "{{ account.username }}"
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
+ old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key"
delegate_to: localhost
diff --git a/apps/accounts/automations/change_secret/manager.py b/apps/accounts/automations/change_secret/manager.py
index e4d1a0fd5..f08d01584 100644
--- a/apps/accounts/automations/change_secret/manager.py
+++ b/apps/accounts/automations/change_secret/manager.py
@@ -7,9 +7,9 @@ from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from xlsxwriter import Workbook
-from accounts.const import AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy
+from accounts.const import AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy, ChangeSecretRecordStatusChoice
from accounts.models import ChangeSecretRecord
-from accounts.notifications import ChangeSecretExecutionTaskMsg
+from accounts.notifications import ChangeSecretExecutionTaskMsg, ChangeSecretFailedMsg
from accounts.serializers import ChangeSecretRecordBackUpSerializer
from assets.const import HostTypes
from common.utils import get_logger
@@ -27,7 +27,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- self.record_id = self.execution.snapshot.get('record_id')
+ self.record_map = self.execution.snapshot.get('record_map', {})
self.secret_type = self.execution.snapshot.get('secret_type')
self.secret_strategy = self.execution.snapshot.get(
'secret_strategy', SecretStrategy.custom
@@ -123,14 +123,20 @@ class ChangeSecretManager(AccountBasePlaybookManager):
print(f'new_secret is None, account: {account}')
continue
- if self.record_id is None:
+ asset_account_id = f'{asset.id}-{account.id}'
+ if asset_account_id not in self.record_map:
recorder = ChangeSecretRecord(
asset=asset, account=account, execution=self.execution,
old_secret=account.secret, new_secret=new_secret,
)
records.append(recorder)
else:
- recorder = ChangeSecretRecord.objects.get(id=self.record_id)
+ record_id = self.record_map[asset_account_id]
+ try:
+ recorder = ChangeSecretRecord.objects.get(id=record_id)
+ except ChangeSecretRecord.DoesNotExist:
+ print(f"Record {record_id} not found")
+ continue
self.name_recorder_mapper[h['name']] = recorder
@@ -158,25 +164,43 @@ class ChangeSecretManager(AccountBasePlaybookManager):
recorder = self.name_recorder_mapper.get(host)
if not recorder:
return
- recorder.status = 'success'
+ recorder.status = ChangeSecretRecordStatusChoice.success.value
recorder.date_finished = timezone.now()
- recorder.save()
+
account = recorder.account
if not account:
print("Account not found, deleted ?")
return
account.secret = recorder.new_secret
account.date_updated = timezone.now()
- account.save(update_fields=['secret', 'date_updated'])
+
+ max_retries = 3
+ retry_count = 0
+
+ while retry_count < max_retries:
+ try:
+ recorder.save()
+ account.save(update_fields=['secret', 'version', 'date_updated'])
+ break
+ except Exception as e:
+ retry_count += 1
+ if retry_count == max_retries:
+ self.on_host_error(host, str(e), result)
+ else:
+ print(f'retry {retry_count} times for {host} recorder save error: {e}')
+ time.sleep(1)
def on_host_error(self, host, error, result):
recorder = self.name_recorder_mapper.get(host)
if not recorder:
return
- recorder.status = 'failed'
+ recorder.status = ChangeSecretRecordStatusChoice.failed.value
recorder.date_finished = timezone.now()
recorder.error = error
- recorder.save()
+ try:
+ recorder.save()
+ except Exception as e:
+ print(f"\033[31m Save {host} recorder error: {e} \033[0m\n")
def on_runner_failed(self, runner, e):
logger.error("Account error: ", e)
@@ -192,7 +216,7 @@ class ChangeSecretManager(AccountBasePlaybookManager):
def get_summary(recorders):
total, succeed, failed = 0, 0, 0
for recorder in recorders:
- if recorder.status == 'success':
+ if recorder.status == ChangeSecretRecordStatusChoice.success.value:
succeed += 1
else:
failed += 1
@@ -209,18 +233,35 @@ class ChangeSecretManager(AccountBasePlaybookManager):
summary = self.get_summary(recorders)
print(summary, end='')
- if self.record_id:
+ if self.record_map:
return
- self.send_recorder_mail(recorders, summary)
+ failed_recorders = [
+ r for r in recorders
+ if r.status == ChangeSecretRecordStatusChoice.failed.value
+ ]
- def send_recorder_mail(self, recorders, summary):
recipients = self.execution.recipients
- if not recorders or not recipients:
+ recipients = User.objects.filter(id__in=list(recipients.keys()))
+ if not recipients:
return
- recipients = User.objects.filter(id__in=list(recipients.keys()))
+ if failed_recorders:
+ name = self.execution.snapshot.get('name')
+ execution_id = str(self.execution.id)
+ _ids = [r.id for r in failed_recorders]
+ asset_account_errors = ChangeSecretRecord.objects.filter(
+ id__in=_ids).values_list('asset__name', 'account__username', 'error')
+ for user in recipients:
+ ChangeSecretFailedMsg(name, execution_id, user, asset_account_errors).publish()
+
+ if not recorders:
+ return
+
+ self.send_recorder_mail(recipients, recorders, summary)
+
+ def send_recorder_mail(self, recipients, recorders, summary):
name = self.execution.snapshot['name']
path = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp')
filename = os.path.join(path, f'{name}-{local_now_filename()}-{time.time()}.xlsx')
diff --git a/apps/accounts/automations/gather_accounts/manager.py b/apps/accounts/automations/gather_accounts/manager.py
index 1c9ae990f..f8949df4d 100644
--- a/apps/accounts/automations/gather_accounts/manager.py
+++ b/apps/accounts/automations/gather_accounts/manager.py
@@ -58,7 +58,7 @@ class GatherAccountsManager(AccountBasePlaybookManager):
result = self.filter_success_result(asset.type, info)
self.collect_asset_account_info(asset, result)
else:
- logger.error(f'Not found {host} info')
+ print(f'\033[31m Not found {host} info \033[0m\n')
def update_or_create_accounts(self):
for asset, data in self.asset_account_info.items():
diff --git a/apps/accounts/automations/push_account/host/aix/main.yml b/apps/accounts/automations/push_account/host/aix/main.yml
index b0256348c..5f76f79a8 100644
--- a/apps/accounts/automations/push_account/host/aix/main.yml
+++ b/apps/accounts/automations/push_account/host/aix/main.yml
@@ -85,6 +85,7 @@
become_user: "{{ account.become.ansible_user | default('') }}"
become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
+ old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "password"
delegate_to: localhost
@@ -95,6 +96,7 @@
login_user: "{{ account.username }}"
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
+ old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key"
delegate_to: localhost
diff --git a/apps/accounts/automations/push_account/host/posix/main.yml b/apps/accounts/automations/push_account/host/posix/main.yml
index 2d2cc8e3e..f5cb53144 100644
--- a/apps/accounts/automations/push_account/host/posix/main.yml
+++ b/apps/accounts/automations/push_account/host/posix/main.yml
@@ -85,6 +85,7 @@
become_user: "{{ account.become.ansible_user | default('') }}"
become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
+ old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "password"
delegate_to: localhost
@@ -95,6 +96,7 @@
login_user: "{{ account.username }}"
login_private_key_path: "{{ account.private_key_path }}"
gateway_args: "{{ jms_asset.ansible_ssh_common_args | default('') }}"
+ old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
when: account.secret_type == "ssh_key"
delegate_to: localhost
diff --git a/apps/accounts/automations/remove_account/manager.py b/apps/accounts/automations/remove_account/manager.py
index 37dd28f2d..38a22538b 100644
--- a/apps/accounts/automations/remove_account/manager.py
+++ b/apps/accounts/automations/remove_account/manager.py
@@ -60,8 +60,11 @@ class RemoveAccountManager(AccountBasePlaybookManager):
if not tuple_asset_gather_account:
return
asset, gather_account = tuple_asset_gather_account
- Account.objects.filter(
- asset_id=asset.id,
- username=gather_account.username
- ).delete()
- gather_account.delete()
+ try:
+ Account.objects.filter(
+ asset_id=asset.id,
+ username=gather_account.username
+ ).delete()
+ gather_account.delete()
+ except Exception as e:
+ print(f'\033[31m Delete account {gather_account.username} failed: {e} \033[0m\n')
diff --git a/apps/accounts/automations/verify_account/custom/rdp/main.yml b/apps/accounts/automations/verify_account/custom/rdp/main.yml
index b0c7cbe4f..2d8bcb883 100644
--- a/apps/accounts/automations/verify_account/custom/rdp/main.yml
+++ b/apps/accounts/automations/verify_account/custom/rdp/main.yml
@@ -3,6 +3,7 @@
vars:
ansible_shell_type: sh
ansible_connection: local
+ ansible_python_interpreter: /opt/py3/bin/python
tasks:
- name: Verify account (pyfreerdp)
diff --git a/apps/accounts/automations/verify_account/custom/ssh/main.yml b/apps/accounts/automations/verify_account/custom/ssh/main.yml
index 05be21f0c..31178666f 100644
--- a/apps/accounts/automations/verify_account/custom/ssh/main.yml
+++ b/apps/accounts/automations/verify_account/custom/ssh/main.yml
@@ -19,3 +19,5 @@
become_user: "{{ account.become.ansible_user | default('') }}"
become_password: "{{ account.become.ansible_password | default('') }}"
become_private_key_path: "{{ account.become.ansible_ssh_private_key_file | default(None) }}"
+ old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
+ gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
diff --git a/apps/accounts/automations/verify_account/manager.py b/apps/accounts/automations/verify_account/manager.py
index 94be53b89..235e5c601 100644
--- a/apps/accounts/automations/verify_account/manager.py
+++ b/apps/accounts/automations/verify_account/manager.py
@@ -76,8 +76,14 @@ class VerifyAccountManager(AccountBasePlaybookManager):
def on_host_success(self, host, result):
account = self.host_account_mapper.get(host)
- account.set_connectivity(Connectivity.OK)
+ try:
+ account.set_connectivity(Connectivity.OK)
+ except Exception as e:
+ print(f'\033[31m Update account {account.name} connectivity failed: {e} \033[0m\n')
def on_host_error(self, host, error, result):
account = self.host_account_mapper.get(host)
- account.set_connectivity(Connectivity.ERR)
+ try:
+ account.set_connectivity(Connectivity.ERR)
+ except Exception as e:
+ print(f'\033[31m Update account {account.name} connectivity failed: {e} \033[0m\n')
diff --git a/apps/accounts/const/account.py b/apps/accounts/const/account.py
index b1d3e348e..d18036bcc 100644
--- a/apps/accounts/const/account.py
+++ b/apps/accounts/const/account.py
@@ -15,6 +15,7 @@ class AliasAccount(TextChoices):
INPUT = '@INPUT', _('Manual input')
USER = '@USER', _('Dynamic user')
ANON = '@ANON', _('Anonymous account')
+ SPEC = '@SPEC', _('Specified account')
@classmethod
def virtual_choices(cls):
diff --git a/apps/accounts/const/automation.py b/apps/accounts/const/automation.py
index cde7fe982..c0419486d 100644
--- a/apps/accounts/const/automation.py
+++ b/apps/accounts/const/automation.py
@@ -16,7 +16,7 @@ DEFAULT_PASSWORD_RULES = {
__all__ = [
'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity',
'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice',
- 'PushAccountActionChoice', 'AccountBackupType'
+ 'PushAccountActionChoice', 'AccountBackupType', 'ChangeSecretRecordStatusChoice',
]
@@ -103,3 +103,9 @@ class AccountBackupType(models.TextChoices):
email = 'email', _('Email')
# 目前只支持sftp方式
object_storage = 'object_storage', _('SFTP')
+
+
+class ChangeSecretRecordStatusChoice(models.TextChoices):
+ failed = 'failed', _('Failed')
+ success = 'success', _('Success')
+ pending = 'pending', _('Pending')
diff --git a/apps/accounts/filters.py b/apps/accounts/filters.py
index b26e9d391..622647079 100644
--- a/apps/accounts/filters.py
+++ b/apps/accounts/filters.py
@@ -5,7 +5,7 @@ from django_filters import rest_framework as drf_filters
from assets.models import Node
from common.drf.filters import BaseFilterSet
-from .models import Account, GatheredAccount
+from .models import Account, GatheredAccount, ChangeSecretRecord
class AccountFilterSet(BaseFilterSet):
@@ -61,3 +61,13 @@ class GatheredAccountFilterSet(BaseFilterSet):
class Meta:
model = GatheredAccount
fields = ['id', 'username']
+
+
+class ChangeSecretRecordFilterSet(BaseFilterSet):
+ asset_name = drf_filters.CharFilter(field_name='asset__name', lookup_expr='icontains')
+ account_username = drf_filters.CharFilter(field_name='account__username', lookup_expr='icontains')
+ execution_id = drf_filters.CharFilter(field_name='execution_id', lookup_expr='exact')
+
+ class Meta:
+ model = ChangeSecretRecord
+ fields = ['id', 'status', 'asset_id', 'execution']
diff --git a/apps/accounts/migrations/0014_virtualaccount.py b/apps/accounts/migrations/0014_virtualaccount.py
index b2c344fce..df0159fc9 100644
--- a/apps/accounts/migrations/0014_virtualaccount.py
+++ b/apps/accounts/migrations/0014_virtualaccount.py
@@ -1,8 +1,9 @@
# Generated by Django 4.1.10 on 2023-08-01 09:12
-from django.db import migrations, models
import uuid
+from django.db import migrations, models
+
class Migration(migrations.Migration):
@@ -20,7 +21,7 @@ class Migration(migrations.Migration):
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
- ('alias', models.CharField(choices=[('@INPUT', 'Manual input'), ('@USER', 'Dynamic user'), ('@ANON', 'Anonymous account')], max_length=128, verbose_name='Alias')),
+ ('alias', models.CharField(choices=[('@INPUT', 'Manual input'), ('@USER', 'Dynamic user'), ('@ANON', 'Anonymous account'), ('@SPEC', 'Specified account')], max_length=128, verbose_name='Alias')),
('secret_from_login', models.BooleanField(default=None, null=True, verbose_name='Secret from login')),
],
options={
diff --git a/apps/accounts/models/automations/backup_account.py b/apps/accounts/models/automations/backup_account.py
index db325c702..177e0dfa1 100644
--- a/apps/accounts/models/automations/backup_account.py
+++ b/apps/accounts/models/automations/backup_account.py
@@ -8,7 +8,7 @@ from django.db import models
from django.db.models import F
from django.utils.translation import gettext_lazy as _
-from accounts.const.automation import AccountBackupType
+from accounts.const import AccountBackupType
from common.const.choices import Trigger
from common.db import fields
from common.db.encoder import ModelJSONFieldEncoder
diff --git a/apps/accounts/models/automations/change_secret.py b/apps/accounts/models/automations/change_secret.py
index c575ac161..48c0a45e1 100644
--- a/apps/accounts/models/automations/change_secret.py
+++ b/apps/accounts/models/automations/change_secret.py
@@ -2,7 +2,7 @@ from django.db import models
from django.utils.translation import gettext_lazy as _
from accounts.const import (
- AutomationTypes
+ AutomationTypes, ChangeSecretRecordStatusChoice
)
from common.db import fields
from common.db.models import JMSBaseModel
@@ -40,7 +40,10 @@ class ChangeSecretRecord(JMSBaseModel):
new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('New secret'))
date_started = models.DateTimeField(blank=True, null=True, verbose_name=_('Date started'))
date_finished = models.DateTimeField(blank=True, null=True, verbose_name=_('Date finished'))
- status = models.CharField(max_length=16, default='pending', verbose_name=_('Status'))
+ status = models.CharField(
+ max_length=16, verbose_name=_('Status'),
+ default=ChangeSecretRecordStatusChoice.pending.value
+ )
error = models.TextField(blank=True, null=True, verbose_name=_('Error'))
class Meta:
diff --git a/apps/accounts/models/base.py b/apps/accounts/models/base.py
index 87cc14e2b..4b55752d6 100644
--- a/apps/accounts/models/base.py
+++ b/apps/accounts/models/base.py
@@ -137,16 +137,13 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
else:
return None
- @property
- def private_key_path(self):
+ def get_private_key_path(self, path):
if self.secret_type != SecretType.SSH_KEY \
or not self.secret \
or not self.private_key:
return None
- project_dir = settings.PROJECT_DIR
- tmp_dir = os.path.join(project_dir, 'tmp')
key_name = '.' + md5(self.private_key.encode('utf-8')).hexdigest()
- key_path = os.path.join(tmp_dir, key_name)
+ key_path = os.path.join(path, key_name)
if not os.path.exists(key_path):
# https://github.com/ansible/ansible-runner/issues/544
# ssh requires OpenSSH format keys to have a full ending newline.
@@ -158,6 +155,12 @@ class BaseAccount(VaultModelMixin, JMSOrgBaseModel):
os.chmod(key_path, 0o400)
return key_path
+ @property
+ def private_key_path(self):
+ project_dir = settings.PROJECT_DIR
+ tmp_dir = os.path.join(project_dir, 'tmp')
+ return self.get_private_key_path(tmp_dir)
+
def get_private_key(self):
if not self.private_key:
return None
diff --git a/apps/accounts/notifications.py b/apps/accounts/notifications.py
index 0082f8b80..e981443a0 100644
--- a/apps/accounts/notifications.py
+++ b/apps/accounts/notifications.py
@@ -1,6 +1,7 @@
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _
+from accounts.models import ChangeSecretRecord
from common.tasks import send_mail_attachment_async, upload_backup_to_obj_storage
from notifications.notifications import UserMessage
from terminal.models.component.storage import ReplayStorage
@@ -98,3 +99,35 @@ class GatherAccountChangeMsg(UserMessage):
def gen_test_msg(cls):
user = User.objects.first()
return cls(user, {})
+
+
+class ChangeSecretFailedMsg(UserMessage):
+ subject = _('Change secret or push account failed information')
+
+ def __init__(self, name, execution_id, user, asset_account_errors: list):
+ self.name = name
+ self.execution_id = execution_id
+ self.asset_account_errors = asset_account_errors
+ super().__init__(user)
+
+ def get_html_msg(self) -> dict:
+ context = {
+ 'name': self.name,
+ 'recipient': self.user,
+ 'execution_id': self.execution_id,
+ 'asset_account_errors': self.asset_account_errors
+ }
+ message = render_to_string('accounts/change_secret_failed_info.html', context)
+
+ return {
+ 'subject': str(self.subject),
+ 'message': message
+ }
+
+ @classmethod
+ def gen_test_msg(cls):
+ name = 'test'
+ user = User.objects.first()
+ record = ChangeSecretRecord.objects.first()
+ execution_id = str(record.execution_id)
+ return cls(name, execution_id, user, [])
diff --git a/apps/accounts/serializers/automations/base.py b/apps/accounts/serializers/automations/base.py
index c2cd21be3..1704c58a2 100644
--- a/apps/accounts/serializers/automations/base.py
+++ b/apps/accounts/serializers/automations/base.py
@@ -21,6 +21,7 @@ __all__ = [
class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer):
assets = ObjectRelatedField(many=True, required=False, queryset=Asset.objects, label=_('Assets'))
nodes = ObjectRelatedField(many=True, required=False, queryset=Node.objects, label=_('Nodes'))
+ is_periodic = serializers.BooleanField(default=False, required=False, label=_("Periodic perform"))
class Meta:
read_only_fields = [
diff --git a/apps/accounts/serializers/automations/change_secret.py b/apps/accounts/serializers/automations/change_secret.py
index 0ef0c4b53..e3e9e2ce0 100644
--- a/apps/accounts/serializers/automations/change_secret.py
+++ b/apps/accounts/serializers/automations/change_secret.py
@@ -4,7 +4,8 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from accounts.const import (
- AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy
+ AutomationTypes, SecretType, SecretStrategy,
+ SSHKeyStrategy, ChangeSecretRecordStatusChoice
)
from accounts.models import (
Account, ChangeSecretAutomation,
@@ -21,6 +22,7 @@ logger = get_logger(__file__)
__all__ = [
'ChangeSecretAutomationSerializer',
'ChangeSecretRecordSerializer',
+ 'ChangeSecretRecordViewSecretSerializer',
'ChangeSecretRecordBackUpSerializer',
'ChangeSecretUpdateAssetSerializer',
'ChangeSecretUpdateNodeSerializer',
@@ -104,7 +106,10 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ
class ChangeSecretRecordSerializer(serializers.ModelSerializer):
is_success = serializers.SerializerMethodField(label=_('Is success'))
asset = ObjectRelatedField(queryset=Asset.objects, label=_('Asset'))
- account = ObjectRelatedField(queryset=Account.objects, label=_('Account'))
+ account = ObjectRelatedField(
+ queryset=Account.objects, label=_('Account'),
+ attrs=("id", "name", "username")
+ )
execution = ObjectRelatedField(
queryset=AutomationExecution.objects, label=_('Automation task execution')
)
@@ -119,7 +124,16 @@ class ChangeSecretRecordSerializer(serializers.ModelSerializer):
@staticmethod
def get_is_success(obj):
- return obj.status == 'success'
+ return obj.status == ChangeSecretRecordStatusChoice.success.value
+
+
+class ChangeSecretRecordViewSecretSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ChangeSecretRecord
+ fields = [
+ 'id', 'old_secret', 'new_secret',
+ ]
+ read_only_fields = fields
class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
@@ -145,7 +159,7 @@ class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer):
@staticmethod
def get_is_success(obj):
- if obj.status == 'success':
+ if obj.status == ChangeSecretRecordStatusChoice.success.value:
return _("Success")
return _("Failed")
diff --git a/apps/accounts/tasks/automation.py b/apps/accounts/tasks/automation.py
index 7c81e417f..f691825ef 100644
--- a/apps/accounts/tasks/automation.py
+++ b/apps/accounts/tasks/automation.py
@@ -36,14 +36,14 @@ def execute_account_automation_task(pid, trigger, tp):
instance.execute(trigger)
-def record_task_activity_callback(self, record_id, *args, **kwargs):
+def record_task_activity_callback(self, record_ids, *args, **kwargs):
from accounts.models import ChangeSecretRecord
with tmp_to_root_org():
- record = get_object_or_none(ChangeSecretRecord, id=record_id)
- if not record:
+ records = ChangeSecretRecord.objects.filter(id__in=record_ids)
+ if not records:
return
- resource_ids = [record.id]
- org_id = record.execution.org_id
+ resource_ids = [str(i.id) for i in records]
+ org_id = records[0].execution.org_id
return resource_ids, org_id
@@ -51,22 +51,26 @@ def record_task_activity_callback(self, record_id, *args, **kwargs):
queue='ansible', verbose_name=_('Execute automation record'),
activity_callback=record_task_activity_callback
)
-def execute_automation_record_task(record_id, tp):
+def execute_automation_record_task(record_ids, tp):
from accounts.models import ChangeSecretRecord
+ task_name = gettext_noop('Execute automation record')
+
with tmp_to_root_org():
- instance = get_object_or_none(ChangeSecretRecord, pk=record_id)
- if not instance:
- logger.error("No automation record found: {}".format(record_id))
+ records = ChangeSecretRecord.objects.filter(id__in=record_ids)
+
+ if not records:
+ logger.error('No automation record found: {}'.format(record_ids))
return
- task_name = gettext_noop('Execute automation record')
+ record = records[0]
+ record_map = {f'{record.asset_id}-{record.account_id}': str(record.id) for record in records}
task_snapshot = {
- 'secret': instance.new_secret,
- 'secret_type': instance.execution.snapshot.get('secret_type'),
- 'accounts': [str(instance.account_id)],
- 'assets': [str(instance.asset_id)],
'params': {},
- 'record_id': record_id,
+ 'record_map': record_map,
+ 'secret': record.new_secret,
+ 'secret_type': record.execution.snapshot.get('secret_type'),
+ 'assets': [str(instance.asset_id) for instance in records],
+ 'accounts': [str(instance.account_id) for instance in records],
}
- with tmp_to_org(instance.execution.org_id):
+ with tmp_to_org(record.execution.org_id):
quickstart_automation_by_snapshot(task_name, tp, task_snapshot)
diff --git a/apps/accounts/tasks/remove_account.py b/apps/accounts/tasks/remove_account.py
index 6b637ed96..44bb9a840 100644
--- a/apps/accounts/tasks/remove_account.py
+++ b/apps/accounts/tasks/remove_account.py
@@ -55,7 +55,7 @@ def clean_historical_accounts():
history_model = Account.history.model
history_id_mapper = defaultdict(list)
- ids = history_model.objects.values('id').annotate(count=Count('id', distinct=True)) \
+ ids = history_model.objects.values('id').annotate(count=Count('id')) \
.filter(count__gte=limit).values_list('id', flat=True)
if not ids:
diff --git a/apps/accounts/tasks/template.py b/apps/accounts/tasks/template.py
index dbad2cf06..bc5416b9f 100644
--- a/apps/accounts/tasks/template.py
+++ b/apps/accounts/tasks/template.py
@@ -29,7 +29,8 @@ def template_sync_related_accounts(template_id, user_id=None):
name = template.name
username = template.username
secret_type = template.secret_type
- print(f'\033[32m>>> 开始同步模版名称、用户名、密钥类型到相关联的账号 ({datetime.now().strftime("%Y-%m-%d %H:%M:%S")})')
+ print(
+ f'\033[32m>>> 开始同步模板名称、用户名、密钥类型到相关联的账号 ({datetime.now().strftime("%Y-%m-%d %H:%M:%S")})')
with tmp_to_org(org_id):
for account in accounts:
account.name = name
diff --git a/apps/accounts/templates/accounts/asset_account_change_info.html b/apps/accounts/templates/accounts/asset_account_change_info.html
index 242908921..b778b3cd3 100644
--- a/apps/accounts/templates/accounts/asset_account_change_info.html
+++ b/apps/accounts/templates/accounts/asset_account_change_info.html
@@ -1,10 +1,10 @@
{% load i18n %}
-
+
{% trans 'Gather account change information' %}
- {% trans 'Asset' %} |
+ {% trans 'Asset' %} |
{% trans 'Added account' %} |
{% trans 'Deleted account' %} |
diff --git a/apps/accounts/templates/accounts/change_secret_failed_info.html b/apps/accounts/templates/accounts/change_secret_failed_info.html
new file mode 100644
index 000000000..442ef44b4
--- /dev/null
+++ b/apps/accounts/templates/accounts/change_secret_failed_info.html
@@ -0,0 +1,36 @@
+{% load i18n %}
+
+{% trans 'Task name' %}: {{ name }}
+{% trans 'Task execution id' %}: {{ execution_id }}
+{% trans 'Respectful' %} {{ recipient }}
+{% trans 'Hello! The following is the failure of changing the password of your assets or pushing the account. Please check and handle it in time.' %}
+
+
+
+
+ {% trans 'Asset' %} |
+ {% trans 'Account' %} |
+ {% trans 'Error' %} |
+
+
+
+ {% for asset_name, account_username, error in asset_account_errors %}
+
+ {{ asset_name }} |
+ {{ account_username }} |
+
+
+ {{ error }}
+
+ |
+
+ {% endfor %}
+
+
\ No newline at end of file
diff --git a/apps/assets/api/asset/asset.py b/apps/assets/api/asset/asset.py
index 0f66d4b9a..f7e088319 100644
--- a/apps/assets/api/asset/asset.py
+++ b/apps/assets/api/asset/asset.py
@@ -32,6 +32,7 @@ __all__ = [
class AssetFilterSet(BaseFilterSet):
platform = django_filters.CharFilter(method='filter_platform')
+ exclude_platform = django_filters.CharFilter(field_name="platform__name", lookup_expr='exact', exclude=True)
domain = django_filters.CharFilter(method='filter_domain')
type = django_filters.CharFilter(field_name="platform__type", lookup_expr="exact")
category = django_filters.CharFilter(field_name="platform__category", lookup_expr="exact")
@@ -92,7 +93,6 @@ class AssetViewSet(SuggestionMixin, OrgBulkModelViewSet):
model = Asset
filterset_class = AssetFilterSet
search_fields = ("name", "address", "comment")
- ordering = ('name',)
ordering_fields = ('name', 'address', 'connectivity', 'platform', 'date_updated', 'date_created')
serializer_classes = (
("default", serializers.AssetSerializer),
diff --git a/apps/assets/api/domain.py b/apps/assets/api/domain.py
index 46b586458..21cb3b2c7 100644
--- a/apps/assets/api/domain.py
+++ b/apps/assets/api/domain.py
@@ -19,7 +19,6 @@ class DomainViewSet(OrgBulkModelViewSet):
model = Domain
filterset_fields = ("name",)
search_fields = filterset_fields
- ordering = ('name',)
serializer_classes = {
'default': serializers.DomainSerializer,
'list': serializers.DomainListSerializer,
@@ -30,6 +29,10 @@ class DomainViewSet(OrgBulkModelViewSet):
return serializers.DomainWithGatewaySerializer
return super().get_serializer_class()
+ def partial_update(self, request, *args, **kwargs):
+ kwargs['partial'] = True
+ return self.update(request, *args, **kwargs)
+
class GatewayViewSet(HostViewSet):
perm_model = Gateway
diff --git a/apps/assets/api/platform.py b/apps/assets/api/platform.py
index 6a31f9519..4df9cb4a3 100644
--- a/apps/assets/api/platform.py
+++ b/apps/assets/api/platform.py
@@ -21,6 +21,7 @@ class AssetPlatformViewSet(JMSModelViewSet):
}
filterset_fields = ['name', 'category', 'type']
search_fields = ['name']
+ ordering = ['-internal', 'name']
rbac_perms = {
'categories': 'assets.view_platform',
'type_constraints': 'assets.view_platform',
diff --git a/apps/assets/automations/base/manager.py b/apps/assets/automations/base/manager.py
index 2a74243d0..d94e5583c 100644
--- a/apps/assets/automations/base/manager.py
+++ b/apps/assets/automations/base/manager.py
@@ -12,7 +12,7 @@ from sshtunnel import SSHTunnelForwarder
from assets.automations.methods import platform_automation_methods
from common.utils import get_logger, lazyproperty, is_openssh_format_key, ssh_pubkey_gen
-from ops.ansible import JMSInventory, PlaybookRunner, DefaultCallback
+from ops.ansible import JMSInventory, SuperPlaybookRunner, DefaultCallback
logger = get_logger(__name__)
@@ -54,7 +54,7 @@ class SSHTunnelManager:
not_valid.append(k)
else:
local_bind_port = server.local_bind_port
- host['ansible_host'] = jms_asset['address'] = host['login_host'] = '127.0.0.1'
+ host['ansible_host'] = jms_asset['address'] = host['login_host'] = 'jms_celery'
host['ansible_port'] = jms_asset['port'] = host['login_port'] = local_bind_port
servers.append(server)
@@ -269,7 +269,7 @@ class BasePlaybookManager:
if not playbook_path:
continue
- runer = PlaybookRunner(
+ runer = SuperPlaybookRunner(
inventory_path,
playbook_path,
self.runtime_dir,
@@ -314,7 +314,7 @@ class BasePlaybookManager:
def delete_runtime_dir(self):
if settings.DEBUG_DEV:
return
- shutil.rmtree(self.runtime_dir)
+ shutil.rmtree(self.runtime_dir, ignore_errors=True)
def run(self, *args, **kwargs):
print(">>> 任务准备阶段\n")
@@ -333,6 +333,7 @@ class BasePlaybookManager:
ssh_tunnel = SSHTunnelManager()
ssh_tunnel.local_gateway_prepare(runner)
try:
+ kwargs.update({"clean_workspace": False})
cb = runner.run(**kwargs)
self.on_runner_success(runner, cb)
except Exception as e:
diff --git a/apps/assets/automations/ping/custom/rdp/main.yml b/apps/assets/automations/ping/custom/rdp/main.yml
index 75e40c027..c0808e976 100644
--- a/apps/assets/automations/ping/custom/rdp/main.yml
+++ b/apps/assets/automations/ping/custom/rdp/main.yml
@@ -3,6 +3,7 @@
vars:
ansible_shell_type: sh
ansible_connection: local
+ ansible_python_interpreter: /opt/py3/bin/python
tasks:
- name: Test asset connection (pyfreerdp)
diff --git a/apps/assets/automations/ping/custom/ssh/main.yml b/apps/assets/automations/ping/custom/ssh/main.yml
index b974425be..d40a7a4e8 100644
--- a/apps/assets/automations/ping/custom/ssh/main.yml
+++ b/apps/assets/automations/ping/custom/ssh/main.yml
@@ -19,3 +19,6 @@
become_user: "{{ custom_become_user | default('') }}"
become_password: "{{ custom_become_password | default('') }}"
become_private_key_path: "{{ custom_become_private_key_path | default(None) }}"
+ old_ssh_version: "{{ jms_asset.old_ssh_version | default(False) }}"
+ gateway_args: "{{ jms_asset.ansible_ssh_common_args | default(None) }}"
+
diff --git a/apps/assets/automations/ping/custom/telnet/main.yml b/apps/assets/automations/ping/custom/telnet/main.yml
new file mode 100644
index 000000000..6e905ba27
--- /dev/null
+++ b/apps/assets/automations/ping/custom/telnet/main.yml
@@ -0,0 +1,11 @@
+- hosts: custom
+ gather_facts: no
+ vars:
+ ansible_connection: local
+ ansible_shell_type: sh
+
+ tasks:
+ - name: Test asset connection (telnet)
+ telnet_ping:
+ login_host: "{{ jms_asset.address }}"
+ login_port: "{{ jms_asset.port }}"
diff --git a/apps/assets/automations/ping/custom/telnet/manifest.yml b/apps/assets/automations/ping/custom/telnet/manifest.yml
new file mode 100644
index 000000000..fc3a0a40a
--- /dev/null
+++ b/apps/assets/automations/ping/custom/telnet/manifest.yml
@@ -0,0 +1,16 @@
+id: ping_by_telnet
+name: "{{ 'Ping by telnet' | trans }}"
+category:
+ - device
+ - host
+type:
+ - all
+method: ping
+protocol: telnet
+priority: 50
+
+i18n:
+ Ping by telnet:
+ zh: '使用 Python 模块 telnet 测试主机可连接性'
+ en: 'Ping by telnet module'
+ ja: 'Pythonモジュールtelnetを使用したホスト接続性のテスト'
diff --git a/apps/assets/automations/ping/manager.py b/apps/assets/automations/ping/manager.py
index 9925ad48d..6249a40c7 100644
--- a/apps/assets/automations/ping/manager.py
+++ b/apps/assets/automations/ping/manager.py
@@ -25,14 +25,22 @@ class PingManager(BasePlaybookManager):
def on_host_success(self, host, result):
asset, account = self.host_asset_and_account_mapper.get(host)
- asset.set_connectivity(Connectivity.OK)
- if not account:
- return
- account.set_connectivity(Connectivity.OK)
+ try:
+ asset.set_connectivity(Connectivity.OK)
+ if not account:
+ return
+ account.set_connectivity(Connectivity.OK)
+ except Exception as e:
+ print(f'\033[31m Update account {account.name} or '
+ f'update asset {asset.name} connectivity failed: {e} \033[0m\n')
def on_host_error(self, host, error, result):
asset, account = self.host_asset_and_account_mapper.get(host)
- asset.set_connectivity(Connectivity.ERR)
- if not account:
- return
- account.set_connectivity(Connectivity.ERR)
+ try:
+ asset.set_connectivity(Connectivity.ERR)
+ if not account:
+ return
+ account.set_connectivity(Connectivity.ERR)
+ except Exception as e:
+ print(f'\033[31m Update account {account.name} or '
+ f'update asset {asset.name} connectivity failed: {e} \033[0m\n')
diff --git a/apps/assets/automations/ping_gateway/manager.py b/apps/assets/automations/ping_gateway/manager.py
index c272570d3..f42090f07 100644
--- a/apps/assets/automations/ping_gateway/manager.py
+++ b/apps/assets/automations/ping_gateway/manager.py
@@ -92,18 +92,26 @@ class PingGatewayManager:
@staticmethod
def on_host_success(gateway, account):
print('\033[32m {} -> {}\033[0m\n'.format(gateway, account))
- gateway.set_connectivity(Connectivity.OK)
- if not account:
- return
- account.set_connectivity(Connectivity.OK)
+ try:
+ gateway.set_connectivity(Connectivity.OK)
+ if not account:
+ return
+ account.set_connectivity(Connectivity.OK)
+ except Exception as e:
+ print(f'\033[31m Update account {account.name} or '
+ f'update asset {gateway.name} connectivity failed: {e} \033[0m\n')
@staticmethod
def on_host_error(gateway, account, error):
print('\033[31m {} -> {} 原因: {} \033[0m\n'.format(gateway, account, error))
- gateway.set_connectivity(Connectivity.ERR)
- if not account:
- return
- account.set_connectivity(Connectivity.ERR)
+ try:
+ gateway.set_connectivity(Connectivity.ERR)
+ if not account:
+ return
+ account.set_connectivity(Connectivity.ERR)
+ except Exception as e:
+ print(f'\033[31m Update account {account.name} or '
+ f'update asset {gateway.name} connectivity failed: {e} \033[0m\n')
@staticmethod
def before_runner_start():
diff --git a/apps/assets/const/protocol.py b/apps/assets/const/protocol.py
index 790899690..a46afccfc 100644
--- a/apps/assets/const/protocol.py
+++ b/apps/assets/const/protocol.py
@@ -38,6 +38,14 @@ class Protocol(ChoicesMixin, models.TextChoices):
cls.ssh: {
'port': 22,
'secret_types': ['password', 'ssh_key'],
+ 'setting': {
+ 'old_ssh_version': {
+ 'type': 'bool',
+ 'default': False,
+ 'label': _('Old SSH version'),
+ 'help_text': _('Old SSH version like openssh 5.x or 6.x')
+ }
+ }
},
cls.sftp: {
'port': 22,
@@ -187,6 +195,14 @@ class Protocol(ChoicesMixin, models.TextChoices):
'port': 27017,
'required': True,
'secret_types': ['password'],
+ 'setting': {
+ 'auth_source': {
+ 'type': 'str',
+ 'default': 'admin',
+ 'label': _('Auth source'),
+ 'help_text': _('The database to authenticate against')
+ }
+ }
},
cls.redis: {
'port': 6379,
diff --git a/apps/assets/models/gateway.py b/apps/assets/models/gateway.py
index c474da26d..36b2a5a72 100644
--- a/apps/assets/models/gateway.py
+++ b/apps/assets/models/gateway.py
@@ -73,3 +73,7 @@ class Gateway(Host):
def private_key_path(self):
account = self.select_account
return account.private_key_path if account else None
+
+ def get_private_key_path(self, path):
+ account = self.select_account
+ return account.get_private_key_path(path) if account else None
diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py
index 5d9ec5c1c..7ca29e6bd 100644
--- a/apps/assets/models/node.py
+++ b/apps/assets/models/node.py
@@ -73,6 +73,10 @@ class FamilyMixin:
@classmethod
def get_nodes_all_children(cls, nodes, with_self=True):
pattern = cls.get_nodes_children_key_pattern(nodes, with_self=with_self)
+ if not pattern:
+ # 如果 pattern = ''
+ # key__iregex 报错 (1139, "Got error 'empty (sub)expression' from regexp")
+ return cls.objects.none()
return Node.objects.filter(key__iregex=pattern)
@classmethod
diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py
index 4ada8fc2f..4224e3983 100644
--- a/apps/assets/serializers/domain.py
+++ b/apps/assets/serializers/domain.py
@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
#
-from django.db.models import Count
+from django.db.models import Count, Q
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from common.serializers import ResourceLabelsMixin
from common.serializers.fields import ObjectRelatedField
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
+from assets.models.gateway import Gateway
from .gateway import GatewayWithAccountSecretSerializer
from ..models import Domain
@@ -15,7 +16,7 @@ __all__ = ['DomainSerializer', 'DomainWithGatewaySerializer', 'DomainListSeriali
class DomainSerializer(ResourceLabelsMixin, BulkOrgResourceModelSerializer):
gateways = ObjectRelatedField(
- many=True, required=False, label=_('Gateway'), read_only=True,
+ many=True, required=False, label=_('Gateway'), queryset=Gateway.objects
)
class Meta:
@@ -25,6 +26,9 @@ class DomainSerializer(ResourceLabelsMixin, BulkOrgResourceModelSerializer):
fields_m2m = ['assets', 'gateways']
read_only_fields = ['date_created']
fields = fields_small + fields_m2m + read_only_fields
+ extra_kwargs = {
+ 'assets': {'required': False},
+ }
def to_representation(self, instance):
data = super().to_representation(instance)
@@ -35,12 +39,17 @@ class DomainSerializer(ResourceLabelsMixin, BulkOrgResourceModelSerializer):
data['assets'] = [i for i in assets if str(i['id']) not in gateway_ids]
return data
- def update(self, instance, validated_data):
+ def create(self, validated_data):
assets = validated_data.pop('assets', [])
- assets = assets + list(instance.gateways)
- validated_data['assets'] = assets
- instance = super().update(instance, validated_data)
- return instance
+ gateways = validated_data.pop('gateways', [])
+ validated_data['assets'] = assets + gateways
+ return super().create(validated_data)
+
+ def update(self, instance, validated_data):
+ assets = validated_data.pop('assets', list(instance.assets.all()))
+ gateways = validated_data.pop('gateways', list(instance.gateways.all()))
+ validated_data['assets'] = assets + gateways
+ return super().update(instance, validated_data)
@classmethod
def setup_eager_loading(cls, queryset):
@@ -58,7 +67,7 @@ class DomainListSerializer(DomainSerializer):
@classmethod
def setup_eager_loading(cls, queryset):
queryset = queryset.annotate(
- assets_amount=Count('assets', distinct=True),
+ assets_amount=Count('assets', filter=~Q(assets__platform__name='Gateway'), distinct=True),
)
return queryset
diff --git a/apps/audits/api.py b/apps/audits/api.py
index 8bdc7e070..cd1513c5c 100644
--- a/apps/audits/api.py
+++ b/apps/audits/api.py
@@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
#
-
from importlib import import_module
from django.conf import settings
@@ -66,7 +65,7 @@ class FTPLogViewSet(OrgModelViewSet):
date_range_filter_fields = [
('date_start', ('date_from', 'date_to'))
]
- filterset_fields = ['user', 'asset', 'account', 'filename']
+ filterset_fields = ['user', 'asset', 'account', 'filename', 'session']
search_fields = filterset_fields
ordering = ['-date_start']
http_method_names = ['post', 'get', 'head', 'options', 'patch']
@@ -269,7 +268,7 @@ class UserSessionViewSet(CommonApiMixin, viewsets.ModelViewSet):
return user_ids
def get_queryset(self):
- keys = UserSession.get_keys()
+ keys = user_session_manager.get_keys()
queryset = UserSession.objects.filter(key__in=keys)
if current_org.is_root():
return queryset
@@ -288,6 +287,6 @@ class UserSessionViewSet(CommonApiMixin, viewsets.ModelViewSet):
keys = queryset.values_list('key', flat=True)
for key in keys:
- user_session_manager.decrement_or_remove(key)
+ user_session_manager.remove(key)
queryset.delete()
return Response(status=status.HTTP_200_OK)
diff --git a/apps/audits/handler.py b/apps/audits/handler.py
index 7d9e331cb..3f0e444f1 100644
--- a/apps/audits/handler.py
+++ b/apps/audits/handler.py
@@ -12,7 +12,10 @@ from common.utils.timezone import as_current_tz
from jumpserver.utils import current_request
from orgs.models import Organization
from orgs.utils import get_current_org_id
+from settings.models import Setting
from settings.serializers import SettingsSerializer
+from users.models import Preference
+from users.serializers import PreferenceSerializer
from .backends import get_operate_log_storage
logger = get_logger(__name__)
@@ -87,19 +90,15 @@ class OperatorLogHandler(metaclass=Singleton):
return log_id, before, after
@staticmethod
- def get_resource_display_from_setting(resource):
- resource_display = None
- setting_serializer = SettingsSerializer()
- label = setting_serializer.get_field_label(resource)
- if label is not None:
- resource_display = label
- return resource_display
-
- def get_resource_display(self, resource):
- resource_display = str(resource)
- return_value = self.get_resource_display_from_setting(resource_display)
- if return_value is not None:
- resource_display = return_value
+ def get_resource_display(resource):
+ if isinstance(resource, Setting):
+ serializer = SettingsSerializer()
+ resource_display = serializer.get_field_label(resource.name)
+ elif isinstance(resource, Preference):
+ serializer = PreferenceSerializer()
+ resource_display = serializer.get_field_label(resource.name)
+ else:
+ resource_display = str(resource)
return resource_display
@staticmethod
diff --git a/apps/audits/models.py b/apps/audits/models.py
index c4b4486a5..512f50bc1 100644
--- a/apps/audits/models.py
+++ b/apps/audits/models.py
@@ -288,16 +288,9 @@ class UserSession(models.Model):
ttl = caches[settings.SESSION_CACHE_ALIAS].ttl(cache_key)
return timezone.now() + timedelta(seconds=ttl)
- @staticmethod
- def get_keys():
- session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
- cache_key_prefix = session_store_cls.cache_key_prefix
- keys = caches[settings.SESSION_CACHE_ALIAS].iter_keys('*')
- return [k.replace(cache_key_prefix, '') for k in keys]
-
@classmethod
def clear_expired_sessions(cls):
- keys = cls.get_keys()
+ keys = user_session_manager.get_keys()
cls.objects.exclude(key__in=keys).delete()
class Meta:
diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py
index cda203537..3111b0d99 100644
--- a/apps/audits/serializers.py
+++ b/apps/audits/serializers.py
@@ -43,7 +43,7 @@ class FTPLogSerializer(serializers.ModelSerializer):
fields_small = fields_mini + [
"user", "remote_addr", "asset", "account",
"org_id", "operate", "filename", "date_start",
- "is_success", "has_file",
+ "is_success", "has_file", "session"
]
fields = fields_small
diff --git a/apps/audits/signal_handlers/login_log.py b/apps/audits/signal_handlers/login_log.py
index ea53716b4..cdcf244cb 100644
--- a/apps/audits/signal_handlers/login_log.py
+++ b/apps/audits/signal_handlers/login_log.py
@@ -36,6 +36,7 @@ class AuthBackendLabelMapping(LazyObject):
backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _("Auth Token")
backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _("WeCom")
backend_label_mapping[settings.AUTH_BACKEND_FEISHU] = _("FeiShu")
+ backend_label_mapping[settings.AUTH_BACKEND_LARK] = 'Lark'
backend_label_mapping[settings.AUTH_BACKEND_SLACK] = _("Slack")
backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _("DingTalk")
backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _("Temporary token")
diff --git a/apps/audits/signal_handlers/operate_log.py b/apps/audits/signal_handlers/operate_log.py
index d095a7ad6..39069b108 100644
--- a/apps/audits/signal_handlers/operate_log.py
+++ b/apps/audits/signal_handlers/operate_log.py
@@ -178,7 +178,7 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs):
'PermedAsset', 'PermedAccount', 'MenuPermission',
'Permission', 'TicketSession', 'ApplyLoginTicket',
'ApplyCommandTicket', 'ApplyLoginAssetTicket',
- 'FavoriteAsset', 'Asset'
+ 'FavoriteAsset',
}
for i, app in enumerate(apps.get_models(), 1):
app_name = app._meta.app_label
diff --git a/apps/audits/tasks.py b/apps/audits/tasks.py
index a22eaeb33..ac28e91cb 100644
--- a/apps/audits/tasks.py
+++ b/apps/audits/tasks.py
@@ -7,18 +7,17 @@ import subprocess
from celery import shared_task
from django.conf import settings
from django.core.files.storage import default_storage
+from django.db import transaction
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from common.const.crontab import CRONTAB_AT_AM_TWO
-from common.utils import get_log_keep_day, get_logger
from common.storage.ftp_file import FTPFileStorageHandler
-from ops.celery.decorator import (
- register_as_period_task, after_app_shutdown_clean_periodic
-)
+from common.utils import get_log_keep_day, get_logger
+from ops.celery.decorator import register_as_period_task
from ops.models import CeleryTaskExecution
-from terminal.models import Session, Command
from terminal.backends import server_replay_storage
+from terminal.models import Session, Command
from .models import UserLoginLog, OperateLog, FTPLog, ActivityLog, PasswordChangeLog
logger = get_logger(__name__)
@@ -57,9 +56,9 @@ def clean_ftp_log_period():
now = timezone.now()
days = get_log_keep_day('FTP_LOG_KEEP_DAYS')
expired_day = now - datetime.timedelta(days=days)
- file_store_dir = os.path.join(default_storage.base_location, 'ftp_file')
+ file_store_dir = os.path.join(default_storage.base_location, FTPLog.upload_to)
FTPLog.objects.filter(date_start__lt=expired_day).delete()
- command = "find %s -mtime +%s -exec rm -f {} \\;" % (
+ command = "find %s -mtime +%s -type f -exec rm -f {} \\;" % (
file_store_dir, days
)
subprocess.call(command, shell=True)
@@ -84,6 +83,15 @@ def clean_celery_tasks_period():
subprocess.call(command, shell=True)
+def batch_delete(queryset, batch_size=3000):
+ model = queryset.model
+ count = queryset.count()
+ with transaction.atomic():
+ for i in range(0, count, batch_size):
+ pks = queryset[i:i + batch_size].values_list('id', flat=True)
+ model.objects.filter(id__in=list(pks)).delete()
+
+
def clean_expired_session_period():
logger.info("Start clean expired session record, commands and replay")
days = get_log_keep_day('TERMINAL_SESSION_KEEP_DURATION')
@@ -93,9 +101,9 @@ def clean_expired_session_period():
expired_commands = Command.objects.filter(timestamp__lt=timestamp)
replay_dir = os.path.join(default_storage.base_location, 'replay')
- expired_sessions.delete()
+ batch_delete(expired_sessions)
logger.info("Clean session item done")
- expired_commands.delete()
+ batch_delete(expired_commands)
logger.info("Clean session command done")
command = "find %s -mtime +%s \\( -name '*.json' -o -name '*.tar' -o -name '*.gz' \\) -exec rm -f {} \\;" % (
replay_dir, days
@@ -108,7 +116,6 @@ def clean_expired_session_period():
@shared_task(verbose_name=_('Clean audits session task log'))
@register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
-@after_app_shutdown_clean_periodic
def clean_audits_log_period():
print("Start clean audit session task log")
clean_login_log_period()
diff --git a/apps/authentication/api/__init__.py b/apps/authentication/api/__init__.py
index 7a63c007c..4cfc6cc4a 100644
--- a/apps/authentication/api/__init__.py
+++ b/apps/authentication/api/__init__.py
@@ -2,13 +2,15 @@
#
from .access_key import *
+from .common import *
from .confirm import *
from .connection_token import *
from .feishu import *
+from .lark import *
from .login_confirm import *
from .mfa import *
from .password import *
+from .session import *
from .sso import *
from .temp_token import *
from .token import *
-from .common import *
diff --git a/apps/authentication/api/common.py b/apps/authentication/api/common.py
index 6624078a7..59e0e3eb2 100644
--- a/apps/authentication/api/common.py
+++ b/apps/authentication/api/common.py
@@ -12,7 +12,6 @@ from common.permissions import IsValidUser, OnlySuperUser
from common.utils import get_logger
from users.models import User
-
logger = get_logger(__file__)
@@ -24,6 +23,7 @@ class QRUnBindBase(APIView):
'wecom': {'user_field': 'wecom_id', 'not_bind_err': errors.WeComNotBound},
'dingtalk': {'user_field': 'dingtalk_id', 'not_bind_err': errors.DingTalkNotBound},
'feishu': {'user_field': 'feishu_id', 'not_bind_err': errors.FeiShuNotBound},
+ 'lark': {'user_field': 'lark_id', 'not_bind_err': errors.LarkNotBound},
'slack': {'user_field': 'slack_id', 'not_bind_err': errors.SlackNotBound},
}
user = self.user
diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py
index f76b6e037..37fb3e968 100644
--- a/apps/authentication/api/connection_token.py
+++ b/apps/authentication/api/connection_token.py
@@ -223,12 +223,17 @@ class ExtraActionApiMixin(RDPFileClientProtocolURLMixin):
validate_exchange_token: callable
@action(methods=['POST', 'GET'], detail=True, url_path='rdp-file')
- def get_rdp_file(self, *args, **kwargs):
+ def get_rdp_file(self, request, *args, **kwargs):
token = self.get_object()
token.is_valid()
filename, content = self.get_rdp_file_info(token)
- filename = '{}.rdp'.format(filename)
response = HttpResponse(content, content_type='application/octet-stream')
+
+ if is_true(request.query_params.get('reusable')):
+ token.set_reusable(True)
+ filename = '{}-{}'.format(filename, token.date_expired.strftime('%Y%m%d_%H%M%S'))
+
+ filename += '.rdp'
response['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'%s' % filename
return response
@@ -379,6 +384,7 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
if account.username != AliasAccount.INPUT:
data['input_username'] = ''
+
ticket = self._validate_acl(user, asset, account)
if ticket:
data['from_ticket'] = ticket
@@ -413,7 +419,10 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView
def _validate_acl(self, user, asset, account):
from acls.models import LoginAssetACL
- acls = LoginAssetACL.filter_queryset(user=user, asset=asset, account=account)
+ kwargs = {'user': user, 'asset': asset, 'account': account}
+ if account.username == AliasAccount.INPUT:
+ kwargs['account_username'] = self.input_username
+ acls = LoginAssetACL.filter_queryset(**kwargs)
ip = get_request_ip_or_data(self.request)
acl = LoginAssetACL.get_match_rule_acls(user, ip, acls)
if not acl:
@@ -503,20 +512,16 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet):
token.is_valid()
serializer = self.get_serializer(instance=token)
- expire_now = request.data.get('expire_now', None)
+ expire_now = request.data.get('expire_now', True)
asset_type = token.asset.type
# 设置默认值
- if expire_now is None:
- # TODO 暂时特殊处理 k8s 不过期
- if asset_type in ['k8s', 'kubernetes']:
- expire_now = False
- else:
- expire_now = not settings.CONNECTION_TOKEN_REUSABLE
+ if asset_type in ['k8s', 'kubernetes']:
+ expire_now = False
- if is_false(expire_now):
- logger.debug('Api specified, now expire now')
- elif token.is_reusable and settings.CONNECTION_TOKEN_REUSABLE:
+ if token.is_reusable and settings.CONNECTION_TOKEN_REUSABLE:
logger.debug('Token is reusable, not expire now')
+ elif is_false(expire_now):
+ logger.debug('Api specified, now expire now')
else:
token.expire()
diff --git a/apps/authentication/api/lark.py b/apps/authentication/api/lark.py
new file mode 100644
index 000000000..5c81fdfa6
--- /dev/null
+++ b/apps/authentication/api/lark.py
@@ -0,0 +1,8 @@
+from common.utils import get_logger
+from .feishu import FeiShuEventSubscriptionCallback
+
+logger = get_logger(__name__)
+
+
+class LarkEventSubscriptionCallback(FeiShuEventSubscriptionCallback):
+ pass
diff --git a/apps/authentication/api/login_confirm.py b/apps/authentication/api/login_confirm.py
index 7802b5b34..4dfa44b75 100644
--- a/apps/authentication/api/login_confirm.py
+++ b/apps/authentication/api/login_confirm.py
@@ -9,6 +9,7 @@ from common.utils import get_logger
from .. import errors, mixins
__all__ = ['TicketStatusApi']
+
logger = get_logger(__name__)
diff --git a/apps/authentication/api/session.py b/apps/authentication/api/session.py
new file mode 100644
index 000000000..37d4f82b5
--- /dev/null
+++ b/apps/authentication/api/session.py
@@ -0,0 +1,68 @@
+import time
+from threading import Thread
+
+from django.conf import settings
+from django.contrib.auth import logout
+from django.contrib.auth.models import AnonymousUser
+from rest_framework import generics
+from rest_framework import status
+from rest_framework.response import Response
+
+from common.sessions.cache import user_session_manager
+from common.utils import get_logger
+
+__all__ = ['UserSessionApi']
+
+logger = get_logger(__name__)
+
+
+class UserSessionManager:
+
+ def __init__(self, request):
+ self.request = request
+ self.session = request.session
+
+ def connect(self):
+ user_session_manager.add_or_increment(self.session.session_key)
+
+ def disconnect(self):
+ user_session_manager.decrement(self.session.session_key)
+ if self.should_delete_session():
+ thread = Thread(target=self.delay_delete_session)
+ thread.start()
+
+ def should_delete_session(self):
+ return (self.session.modified or settings.SESSION_SAVE_EVERY_REQUEST) and \
+ not self.session.is_empty() and \
+ self.session.get_expire_at_browser_close() and \
+ not user_session_manager.check_active(self.session.session_key)
+
+ def delay_delete_session(self):
+ timeout = 6
+ check_interval = 0.5
+
+ start_time = time.time()
+ while time.time() - start_time < timeout:
+ time.sleep(check_interval)
+ if user_session_manager.check_active(self.session.session_key):
+ return
+
+ logout(self.request)
+
+
+class UserSessionApi(generics.RetrieveDestroyAPIView):
+ permission_classes = ()
+
+ def retrieve(self, request, *args, **kwargs):
+ if isinstance(request.user, AnonymousUser):
+ return Response(status=status.HTTP_200_OK)
+
+ UserSessionManager(request).connect()
+ return Response(status=status.HTTP_200_OK)
+
+ def destroy(self, request, *args, **kwargs):
+ if isinstance(request.user, AnonymousUser):
+ return Response(status=status.HTTP_200_OK)
+
+ UserSessionManager(request).disconnect()
+ return Response(status=status.HTTP_204_NO_CONTENT)
diff --git a/apps/authentication/api/sso.py b/apps/authentication/api/sso.py
index 6e48bda41..a4cd6a67d 100644
--- a/apps/authentication/api/sso.py
+++ b/apps/authentication/api/sso.py
@@ -5,11 +5,13 @@ from django.conf import settings
from django.contrib.auth import login
from django.http.response import HttpResponseRedirect
from rest_framework import serializers
+from rest_framework import status
from rest_framework.decorators import action
from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework.response import Response
+from authentication.errors import ACLError
from common.api import JMSGenericViewSet
from common.const.http import POST, GET
from common.permissions import OnlySuperUser
@@ -17,7 +19,10 @@ from common.serializers import EmptySerializer
from common.utils import reverse, safe_next_url
from common.utils.timezone import utc_now
from users.models import User
-from ..errors import SSOAuthClosed
+from users.utils import LoginBlockUtil, LoginIpBlockUtil
+from ..errors import (
+ SSOAuthClosed, AuthFailedError, LoginConfirmBaseError, SSOAuthKeyTTLError
+)
from ..filters import AuthKeyQueryDeclaration
from ..mixins import AuthMixin
from ..models import SSOToken
@@ -63,31 +68,58 @@ class SSOViewSet(AuthMixin, JMSGenericViewSet):
此接口违反了 `Restful` 的规范
`GET` 应该是安全的方法,但此接口是不安全的
"""
+ status_code = status.HTTP_400_BAD_REQUEST
request.META['HTTP_X_JMS_LOGIN_TYPE'] = 'W'
authkey = request.query_params.get(AUTH_KEY)
next_url = request.query_params.get(NEXT_URL)
if not next_url or not next_url.startswith('/'):
next_url = reverse('index')
- if not authkey:
- raise serializers.ValidationError("authkey is required")
-
try:
+ if not authkey:
+ raise serializers.ValidationError("authkey is required")
+
authkey = UUID(authkey)
token = SSOToken.objects.get(authkey=authkey, expired=False)
- # 先过期,只能访问这一次
+ except (ValueError, SSOToken.DoesNotExist, serializers.ValidationError) as e:
+ error_msg = str(e)
+ self.send_auth_signal(success=False, reason=error_msg)
+ return Response({'error': error_msg}, status=status_code)
+
+ error_msg = None
+ user = token.user
+ username = user.username
+ ip = self.get_request_ip()
+
+ try:
+ if (utc_now().timestamp() - token.date_created.timestamp()) > settings.AUTH_SSO_AUTHKEY_TTL:
+ raise SSOAuthKeyTTLError()
+
+ self._check_is_block(username, True)
+ self._check_only_allow_exists_user_auth(username)
+ self._check_login_acl(user, ip)
+ self.check_user_login_confirm_if_need(user)
+
+ self.request.session['auth_backend'] = settings.AUTH_BACKEND_SSO
+ login(self.request, user, settings.AUTH_BACKEND_SSO)
+ self.send_auth_signal(success=True, user=user)
+ self.mark_mfa_ok('otp', user)
+
+ LoginIpBlockUtil(ip).clean_block_if_need()
+ LoginBlockUtil(username, ip).clean_failed_count()
+ self.clear_auth_mark()
+ except (ACLError, LoginConfirmBaseError): # 无需记录日志
+ pass
+ except (AuthFailedError, SSOAuthKeyTTLError) as e:
+ error_msg = e.msg
+ except Exception as e:
+ error_msg = str(e)
+ finally:
token.expired = True
token.save()
- except (ValueError, SSOToken.DoesNotExist):
- self.send_auth_signal(success=False, reason='authkey_invalid')
- return HttpResponseRedirect(next_url)
- # 判断是否过期
- if (utc_now().timestamp() - token.date_created.timestamp()) > settings.AUTH_SSO_AUTHKEY_TTL:
- self.send_auth_signal(success=False, reason='authkey_timeout')
+ if error_msg:
+ self.send_auth_signal(success=False, username=username, reason=error_msg)
+ return Response({'error': error_msg}, status=status_code)
+ else:
return HttpResponseRedirect(next_url)
-
- user = token.user
- login(self.request, user, settings.AUTH_BACKEND_SSO)
- self.send_auth_signal(success=True, user=user)
- return HttpResponseRedirect(next_url)
diff --git a/apps/authentication/backends/oauth2/views.py b/apps/authentication/backends/oauth2/views.py
index e3b919fff..88f82dfa3 100644
--- a/apps/authentication/backends/oauth2/views.py
+++ b/apps/authentication/backends/oauth2/views.py
@@ -4,10 +4,13 @@ from django.contrib import auth
from django.http import HttpResponseRedirect
from django.urls import reverse
from django.utils.http import urlencode
+from django.utils.translation import gettext_lazy as _
from authentication.utils import build_absolute_uri
-from common.utils import get_logger
+from authentication.views.mixins import FlashMessageMixin
from authentication.mixins import authenticate
+from common.utils import get_logger
+
logger = get_logger(__file__)
@@ -39,7 +42,7 @@ class OAuth2AuthRequestView(View):
return HttpResponseRedirect(redirect_url)
-class OAuth2AuthCallbackView(View):
+class OAuth2AuthCallbackView(View, FlashMessageMixin):
http_method_names = ['get', ]
def get(self, request):
@@ -51,6 +54,11 @@ class OAuth2AuthCallbackView(View):
if 'code' in callback_params:
logger.debug(log_prompt.format('Process authenticate'))
user = authenticate(code=callback_params['code'], request=request)
+
+ if err_msg := getattr(request, 'error_message', ''):
+ login_url = reverse('authentication:login') + '?admin=1'
+ return self.get_failed_response(login_url, title=_('Authentication failed'), msg=err_msg)
+
if user and user.is_valid:
logger.debug(log_prompt.format('Login: {}'.format(user)))
auth.login(self.request, user)
diff --git a/apps/authentication/backends/sso.py b/apps/authentication/backends/sso.py
index 66f15a3a4..5ee17a4ca 100644
--- a/apps/authentication/backends/sso.py
+++ b/apps/authentication/backends/sso.py
@@ -55,6 +55,12 @@ class FeiShuAuthentication(JMSModelBackend):
pass
+class LarkAuthentication(FeiShuAuthentication):
+ @staticmethod
+ def is_enabled():
+ return settings.AUTH_LARK
+
+
class SlackAuthentication(JMSModelBackend):
"""
什么也不做呀😺
@@ -72,5 +78,6 @@ class AuthorizationTokenAuthentication(JMSModelBackend):
"""
什么也不做呀😺
"""
+
def authenticate(self, request, **kwargs):
pass
diff --git a/apps/authentication/errors/failed.py b/apps/authentication/errors/failed.py
index f6d8004c6..729d93b6d 100644
--- a/apps/authentication/errors/failed.py
+++ b/apps/authentication/errors/failed.py
@@ -52,6 +52,10 @@ class AuthFailedError(Exception):
return str(self.msg)
+class SSOAuthKeyTTLError(Exception):
+ msg = 'sso_authkey_timeout'
+
+
class BlockGlobalIpLoginError(AuthFailedError):
error = 'block_global_ip_login'
diff --git a/apps/authentication/errors/mfa.py b/apps/authentication/errors/mfa.py
index b1ace594d..ae314ada6 100644
--- a/apps/authentication/errors/mfa.py
+++ b/apps/authentication/errors/mfa.py
@@ -33,6 +33,11 @@ class FeiShuNotBound(JMSException):
default_detail = _('FeiShu is not bound')
+class LarkNotBound(JMSException):
+ default_code = 'lark_not_bound'
+ default_detail = _('Lark is not bound')
+
+
class SlackNotBound(JMSException):
default_code = 'slack_not_bound'
default_detail = _('Slack is not bound')
diff --git a/apps/authentication/forms.py b/apps/authentication/forms.py
index 5f56a4a98..05d102e8e 100644
--- a/apps/authentication/forms.py
+++ b/apps/authentication/forms.py
@@ -17,10 +17,6 @@ class EncryptedField(forms.CharField):
class UserLoginForm(forms.Form):
- days_auto_login = int(settings.SESSION_COOKIE_AGE / 3600 / 24)
- disable_days_auto_login = settings.SESSION_EXPIRE_AT_BROWSER_CLOSE \
- or days_auto_login < 1
-
username = forms.CharField(
label=_('Username'), max_length=100,
widget=forms.TextInput(attrs={
@@ -34,15 +30,15 @@ class UserLoginForm(forms.Form):
)
auto_login = forms.BooleanField(
required=False, initial=False,
- widget=forms.CheckboxInput(
- attrs={'disabled': disable_days_auto_login}
- )
+ widget=forms.CheckboxInput()
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
auto_login_field = self.fields['auto_login']
- auto_login_field.label = _("{} days auto login").format(self.days_auto_login or 1)
+ auto_login_field.label = _("Auto login")
+ if settings.SESSION_EXPIRE_AT_BROWSER_CLOSE:
+ auto_login_field.widget = forms.HiddenInput()
def confirm_login_allowed(self, user):
if not user.is_staff:
diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py
index 31cb1dc19..721a189d7 100644
--- a/apps/authentication/mixins.py
+++ b/apps/authentication/mixins.py
@@ -363,7 +363,6 @@ class AuthACLMixin:
if acl.is_action(acl.ActionChoices.notice):
self.request.session['auth_notice_required'] = '1'
self.request.session['auth_acl_id'] = str(acl.id)
- return
def _check_third_party_login_acl(self):
request = self.request
diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py
index 2ef28eab1..f07874a2a 100644
--- a/apps/authentication/models/connection_token.py
+++ b/apps/authentication/models/connection_token.py
@@ -82,12 +82,15 @@ class ConnectionToken(JMSOrgBaseModel):
self.save(update_fields=['date_expired'])
def set_reusable(self, is_reusable):
+ if not settings.CONNECTION_TOKEN_REUSABLE:
+ return
self.is_reusable = is_reusable
if self.is_reusable:
seconds = settings.CONNECTION_TOKEN_REUSABLE_EXPIRATION
else:
seconds = settings.CONNECTION_TOKEN_ONETIME_EXPIRATION
- self.date_expired = timezone.now() + timedelta(seconds=seconds)
+
+ self.date_expired = self.date_created + timedelta(seconds=seconds)
self.save(update_fields=['is_reusable', 'date_expired'])
def renewal(self):
diff --git a/apps/authentication/signal_handlers.py b/apps/authentication/signal_handlers.py
index 943d751dd..e060e3c1f 100644
--- a/apps/authentication/signal_handlers.py
+++ b/apps/authentication/signal_handlers.py
@@ -1,5 +1,3 @@
-from importlib import import_module
-
from django.conf import settings
from django.contrib.auth import user_logged_in
from django.core.cache import cache
@@ -8,6 +6,7 @@ from django_cas_ng.signals import cas_user_authenticated
from apps.jumpserver.settings.auth import AUTHENTICATION_BACKENDS_THIRD_PARTY
from audits.models import UserSession
+from common.sessions.cache import user_session_manager
from .signals import post_auth_success, post_auth_failed, user_auth_failed, user_auth_success
@@ -32,8 +31,7 @@ def on_user_auth_login_success(sender, user, request, **kwargs):
lock_key = 'single_machine_login_' + str(user.id)
session_key = cache.get(lock_key)
if session_key and session_key != request.session.session_key:
- session = import_module(settings.SESSION_ENGINE).SessionStore(session_key)
- session.delete()
+ user_session_manager.remove(session_key)
UserSession.objects.filter(key=session_key).delete()
cache.set(lock_key, request.session.session_key, None)
diff --git a/apps/authentication/templates/authentication/login_wait_confirm.html b/apps/authentication/templates/authentication/login_wait_confirm.html
index 157ee1d1a..33d05e03a 100644
--- a/apps/authentication/templates/authentication/login_wait_confirm.html
+++ b/apps/authentication/templates/authentication/login_wait_confirm.html
@@ -95,6 +95,7 @@ function doRequestAuth() {
}
clearInterval(interval);
clearInterval(checkInterval);
+ cancelTicket();
$(".copy-btn").attr('disabled', 'disabled');
errorMsgRef.html(data.msg)
}
diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py
index 3bb7898df..62f52ac9f 100644
--- a/apps/authentication/urls/api_urls.py
+++ b/apps/authentication/urls/api_urls.py
@@ -22,6 +22,9 @@ urlpatterns = [
path('feishu/event/subscription/callback/', api.FeiShuEventSubscriptionCallback.as_view(),
name='feishu-event-subscription-callback'),
+ path('lark/event/subscription/callback/', api.LarkEventSubscriptionCallback.as_view(),
+ name='lark-event-subscription-callback'),
+
path('auth/', api.TokenCreateApi.as_view(), name='user-auth'),
path('confirm-oauth/', api.ConfirmBindORUNBindOAuth.as_view(), name='confirm-oauth'),
path('tokens/', api.TokenCreateApi.as_view(), name='auth-token'),
@@ -32,6 +35,7 @@ urlpatterns = [
path('password/reset-code/', api.UserResetPasswordSendCodeApi.as_view(), name='reset-password-code'),
path('password/verify/', api.UserPasswordVerifyApi.as_view(), name='user-password-verify'),
path('login-confirm-ticket/status/', api.TicketStatusApi.as_view(), name='login-confirm-ticket-status'),
+ path('user-session/', api.UserSessionApi.as_view(), name='user-session'),
]
urlpatterns += router.urls + passkey_urlpatterns
diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py
index a648ded51..94c15047f 100644
--- a/apps/authentication/urls/view_urls.py
+++ b/apps/authentication/urls/view_urls.py
@@ -49,6 +49,12 @@ urlpatterns = [
path('feishu/qr/bind/callback/', views.FeiShuQRBindCallbackView.as_view(), name='feishu-qr-bind-callback'),
path('feishu/qr/login/callback/', views.FeiShuQRLoginCallbackView.as_view(), name='feishu-qr-login-callback'),
+ path('lark/bind/start/', views.LarkEnableStartView.as_view(), name='lark-bind-start'),
+ path('lark/qr/bind/', views.LarkQRBindView.as_view(), name='lark-qr-bind'),
+ path('lark/qr/login/', views.LarkQRLoginView.as_view(), name='lark-qr-login'),
+ path('lark/qr/bind/callback/', views.LarkQRBindCallbackView.as_view(), name='lark-qr-bind-callback'),
+ path('lark/qr/login/callback/', views.LarkQRLoginCallbackView.as_view(), name='lark-qr-login-callback'),
+
path('slack/bind/start/', views.SlackEnableStartView.as_view(), name='slack-bind-start'),
path('slack/qr/bind/', views.SlackQRBindView.as_view(), name='slack-qr-bind'),
path('slack/qr/login/', views.SlackQRLoginView.as_view(), name='slack-qr-login'),
diff --git a/apps/authentication/views/__init__.py b/apps/authentication/views/__init__.py
index 214c8943a..3c51a1c85 100644
--- a/apps/authentication/views/__init__.py
+++ b/apps/authentication/views/__init__.py
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
#
-from .login import *
-from .mfa import *
-from .wecom import *
from .dingtalk import *
from .feishu import *
+from .lark import *
+from .login import *
+from .mfa import *
from .slack import *
+from .wecom import *
diff --git a/apps/authentication/views/base.py b/apps/authentication/views/base.py
index 598ec9a0d..8146e1cd2 100644
--- a/apps/authentication/views/base.py
+++ b/apps/authentication/views/base.py
@@ -1,8 +1,8 @@
from functools import lru_cache
from django.conf import settings
-from django.db.utils import IntegrityError
from django.contrib.auth import logout as auth_logout
+from django.db.utils import IntegrityError
from django.utils.module_loading import import_string
from django.utils.translation import gettext_lazy as _
from django.views import View
@@ -12,8 +12,8 @@ from authentication import errors
from authentication.mixins import AuthMixin
from authentication.notifications import OAuthBindMessage
from common.utils import get_logger
-from common.utils.django import reverse, get_object_or_none
from common.utils.common import get_request_ip
+from common.utils.django import reverse, get_object_or_none
from users.models import User
from users.signal_handlers import check_only_allow_exist_user_auth
from .mixins import FlashMessageMixin
@@ -83,7 +83,15 @@ class BaseLoginCallbackView(AuthMixin, FlashMessageMixin, IMClientMixin, View):
if not self.verify_state():
return self.get_verify_state_failed_response(redirect_url)
- user_id, other_info = self.client.get_user_id_by_code(code)
+ try:
+ user_id, other_info = self.client.get_user_id_by_code(code)
+ except Exception:
+ response = self.get_failed_response(
+ login_url, title=self.msg_client_err,
+ msg=self.msg_not_found_user_from_client_err
+ )
+ return response
+
if not user_id:
# 正常流程不会出这个错误,hack 行为
err = self.msg_not_found_user_from_client_err
diff --git a/apps/authentication/views/feishu.py b/apps/authentication/views/feishu.py
index 0427c2b1b..c2cd07914 100644
--- a/apps/authentication/views/feishu.py
+++ b/apps/authentication/views/feishu.py
@@ -21,24 +21,45 @@ from .mixins import FlashMessageMixin
logger = get_logger(__file__)
-FEISHU_STATE_SESSION_KEY = '_feishu_state'
+
+class FeiShuEnableStartView(UserVerifyPasswordView):
+ category = 'feishu'
+
+ def get_success_url(self):
+ referer = self.request.META.get('HTTP_REFERER')
+ redirect_url = self.request.GET.get("redirect_url")
+
+ success_url = reverse(f'authentication:{self.category}-qr-bind')
+
+ success_url += '?' + urlencode({
+ 'redirect_url': redirect_url or referer
+ })
+
+ return success_url
class FeiShuQRMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMessageMixin, View):
+ category = 'feishu'
+ error = _('FeiShu Error')
+ error_msg = _('FeiShu is already bound')
+ state_session_key = f'_{category}_state'
+
+ @property
+ def url_object(self):
+ return URL()
+
def dispatch(self, request, *args, **kwargs):
try:
return super().dispatch(request, *args, **kwargs)
except APIException as e:
msg = str(e.detail)
return self.get_failed_response(
- '/',
- _('FeiShu Error'),
- msg
+ '/', self.error, msg
)
def verify_state(self):
state = self.request.GET.get('state')
- session_state = self.request.session.get(FEISHU_STATE_SESSION_KEY)
+ session_state = self.request.session.get(self.state_session_key)
if state != session_state:
return False
return True
@@ -49,19 +70,18 @@ class FeiShuQRMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMe
def get_qr_url(self, redirect_uri):
state = random_string(16)
- self.request.session[FEISHU_STATE_SESSION_KEY] = state
+ self.request.session[self.state_session_key] = state
params = {
- 'app_id': settings.FEISHU_APP_ID,
+ 'app_id': getattr(settings, f'{self.category}_APP_ID'.upper()),
'state': state,
'redirect_uri': redirect_uri,
}
- url = URL().authen + '?' + urlencode(params)
+ url = self.url_object.authen + '?' + urlencode(params)
return url
def get_already_bound_response(self, redirect_url):
- msg = _('FeiShu is already bound')
- response = self.get_failed_response(redirect_url, msg, msg)
+ response = self.get_failed_response(redirect_url, self.error_msg, self.error_msg)
return response
@@ -71,7 +91,7 @@ class FeiShuQRBindView(FeiShuQRMixin, View):
def get(self, request: HttpRequest):
redirect_url = request.GET.get('redirect_url')
- redirect_uri = reverse('authentication:feishu-qr-bind-callback', external=True)
+ redirect_uri = reverse(f'authentication:{self.category}-qr-bind-callback', external=True)
redirect_uri += '?' + urlencode({'redirect_url': redirect_url})
url = self.get_qr_url(redirect_uri)
@@ -81,25 +101,16 @@ class FeiShuQRBindView(FeiShuQRMixin, View):
class FeiShuQRBindCallbackView(FeiShuQRMixin, BaseBindCallbackView):
permission_classes = (IsAuthenticated,)
- client_type_path = 'common.sdk.im.feishu.FeiShu'
- client_auth_params = {'app_id': 'FEISHU_APP_ID', 'app_secret': 'FEISHU_APP_SECRET'}
auth_type = 'feishu'
auth_type_label = _('FeiShu')
+ client_type_path = f'common.sdk.im.{auth_type}.FeiShu'
-
-class FeiShuEnableStartView(UserVerifyPasswordView):
-
- def get_success_url(self):
- referer = self.request.META.get('HTTP_REFERER')
- redirect_url = self.request.GET.get("redirect_url")
-
- success_url = reverse('authentication:feishu-qr-bind')
-
- success_url += '?' + urlencode({
- 'redirect_url': redirect_url or referer
- })
-
- return success_url
+ @property
+ def client_auth_params(self):
+ return {
+ 'app_id': f'{self.auth_type}_APP_ID'.upper(),
+ 'app_secret': f'{self.auth_type}_APP_SECRET'.upper()
+ }
class FeiShuQRLoginView(FeiShuQRMixin, View):
@@ -107,7 +118,7 @@ class FeiShuQRLoginView(FeiShuQRMixin, View):
def get(self, request: HttpRequest):
redirect_url = request.GET.get('redirect_url') or reverse('index')
- redirect_uri = reverse('authentication:feishu-qr-login-callback', external=True)
+ redirect_uri = reverse(f'authentication:{self.category}-qr-login-callback', external=True)
redirect_uri += '?' + urlencode({
'redirect_url': redirect_url,
})
@@ -119,11 +130,19 @@ class FeiShuQRLoginView(FeiShuQRMixin, View):
class FeiShuQRLoginCallbackView(FeiShuQRMixin, BaseLoginCallbackView):
permission_classes = (AllowAny,)
- client_type_path = 'common.sdk.im.feishu.FeiShu'
- client_auth_params = {'app_id': 'FEISHU_APP_ID', 'app_secret': 'FEISHU_APP_SECRET'}
user_type = 'feishu'
- auth_backend = 'AUTH_BACKEND_FEISHU'
+ auth_type = user_type
+ client_type_path = f'common.sdk.im.{auth_type}.FeiShu'
msg_client_err = _('FeiShu Error')
msg_user_not_bound_err = _('FeiShu is not bound')
msg_not_found_user_from_client_err = _('Failed to get user from FeiShu')
+
+ auth_backend = f'AUTH_BACKEND_{auth_type}'.upper()
+
+ @property
+ def client_auth_params(self):
+ return {
+ 'app_id': f'{self.auth_type}_APP_ID'.upper(),
+ 'app_secret': f'{self.auth_type}_APP_SECRET'.upper()
+ }
diff --git a/apps/authentication/views/lark.py b/apps/authentication/views/lark.py
new file mode 100644
index 000000000..b2b3565b5
--- /dev/null
+++ b/apps/authentication/views/lark.py
@@ -0,0 +1,51 @@
+from django.utils.translation import gettext_lazy as _
+
+from common.sdk.im.lark import URL
+from common.utils import get_logger
+from .feishu import (
+ FeiShuEnableStartView, FeiShuQRBindView, FeiShuQRBindCallbackView,
+ FeiShuQRLoginView, FeiShuQRLoginCallbackView
+)
+
+logger = get_logger(__file__)
+
+
+class LarkEnableStartView(FeiShuEnableStartView):
+ category = 'lark'
+
+
+class BaseLarkQRMixin:
+ category = 'lark'
+ error = _('Lark Error')
+ error_msg = _('Lark is already bound')
+ state_session_key = f'_{category}_state'
+
+ @property
+ def url_object(self):
+ return URL()
+
+
+class LarkQRBindView(BaseLarkQRMixin, FeiShuQRBindView):
+ pass
+
+
+class LarkQRBindCallbackView(BaseLarkQRMixin, FeiShuQRBindCallbackView):
+ auth_type = 'lark'
+ auth_type_label = auth_type.capitalize()
+ client_type_path = f'common.sdk.im.{auth_type}.Lark'
+
+
+class LarkQRLoginView(BaseLarkQRMixin, FeiShuQRLoginView):
+ pass
+
+
+class LarkQRLoginCallbackView(BaseLarkQRMixin, FeiShuQRLoginCallbackView):
+ user_type = 'lark'
+ auth_type = user_type
+ client_type_path = f'common.sdk.im.{auth_type}.Lark'
+
+ msg_client_err = _('Lark Error')
+ msg_user_not_bound_err = _('Lark is not bound')
+ msg_not_found_user_from_client_err = _('Failed to get user from Lark')
+
+ auth_backend = f'AUTH_BACKEND_{auth_type}'.upper()
diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py
index 23ad4d814..2f48c21e2 100644
--- a/apps/authentication/views/login.py
+++ b/apps/authentication/views/login.py
@@ -91,6 +91,12 @@ class UserLoginContextMixin:
'url': reverse('authentication:feishu-qr-login'),
'logo': static('img/login_feishu_logo.png')
},
+ {
+ 'name': 'Lark',
+ 'enabled': settings.AUTH_LARK,
+ 'url': reverse('authentication:lark-qr-login'),
+ 'logo': static('img/login_lark_logo.png')
+ },
{
'name': _('Slack'),
'enabled': settings.AUTH_SLACK,
@@ -113,6 +119,10 @@ class UserLoginContextMixin:
'title': '中文(简体)',
'code': 'zh-hans'
},
+ {
+ 'title': '中文(繁體)',
+ 'code': 'zh-hant'
+ },
{
'title': 'English',
'code': 'en'
diff --git a/apps/common/api/mixin.py b/apps/common/api/mixin.py
index e8266d928..830e7b571 100644
--- a/apps/common/api/mixin.py
+++ b/apps/common/api/mixin.py
@@ -6,6 +6,7 @@ from typing import Callable
from django.db import models
from django.db.models.signals import m2m_changed
+from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.settings import api_settings
@@ -19,7 +20,7 @@ from .serializer import SerializerMixin
__all__ = [
'CommonApiMixin', 'PaginatedResponseMixin', 'RelationMixin',
- 'ExtraFilterFieldsMixin',
+ 'ExtraFilterFieldsMixin'
]
logger = get_logger(__name__)
@@ -89,6 +90,7 @@ class RelationMixin:
class QuerySetMixin:
action: str
+ request: Request
get_serializer_class: Callable
get_queryset: Callable
@@ -98,8 +100,18 @@ class QuerySetMixin:
return queryset
if self.action == 'metadata':
queryset = queryset.none()
+ queryset = self.setup_eager_loading(queryset)
return queryset
+ # Todo: 未来考虑自定义 pagination
+ def setup_eager_loading(self, queryset):
+ if self.request.query_params.get('format') not in ['csv', 'xlsx']:
+ return queryset
+ serializer_class = self.get_serializer_class()
+ if not serializer_class or not hasattr(serializer_class, 'setup_eager_loading'):
+ return queryset
+ return serializer_class.setup_eager_loading(queryset)
+
def paginate_queryset(self, queryset):
page = super().paginate_queryset(queryset)
serializer_class = self.get_serializer_class()
@@ -186,10 +198,7 @@ class OrderingFielderFieldsMixin:
model = self.queryset.model
else:
queryset = self.get_queryset()
- if isinstance(queryset, list):
- model = None
- else:
- model = queryset.model
+ model = None if isinstance(queryset, list) else queryset.model
if not model:
return []
diff --git a/apps/common/api/serializer.py b/apps/common/api/serializer.py
index 84cad19ff..539ff2868 100644
--- a/apps/common/api/serializer.py
+++ b/apps/common/api/serializer.py
@@ -27,6 +27,8 @@ class SerializerMixin:
return None
serializer_classes = dict(serializer_classes)
view_action = self.request.query_params.get('action') or self.action or 'list'
+ if self.request.query_params.get('format'):
+ view_action = 'retrieve'
serializer_class = serializer_classes.get(view_action)
if serializer_class is None:
diff --git a/apps/common/db/fields.py b/apps/common/db/fields.py
index 0a620e1f0..f5b1e4b83 100644
--- a/apps/common/db/fields.py
+++ b/apps/common/db/fields.py
@@ -469,7 +469,7 @@ class JSONManyToManyDescriptor:
rule_match = rule.get('match', 'exact')
custom_filter_q = None
- spec_attr_filter = getattr(to_model, "get_filter_{}_attr_q".format(rule['name']), None)
+ spec_attr_filter = getattr(to_model, "get_{}_filter_attr_q".format(rule['name']), None)
if spec_attr_filter:
custom_filter_q = spec_attr_filter(rule_value, rule_match)
elif custom_attr_filter:
@@ -478,59 +478,61 @@ class JSONManyToManyDescriptor:
custom_q &= custom_filter_q
continue
- if rule_match == 'in':
- res &= value in rule_value or '*' in rule_value
- elif rule_match == 'exact':
- res &= value == rule_value or rule_value == '*'
- elif rule_match == 'contains':
- res &= (rule_value in value)
- elif rule_match == 'startswith':
- res &= str(value).startswith(str(rule_value))
- elif rule_match == 'endswith':
- res &= str(value).endswith(str(rule_value))
- elif rule_match == 'regex':
- try:
- matched = bool(re.search(r'{}'.format(rule_value), value))
- except Exception as e:
- logging.error('Error regex match: %s', e)
- matched = False
- res &= matched
- elif rule_match == 'not':
- res &= value != rule_value
- elif rule['match'] == 'gte':
- res &= value >= rule_value
- elif rule['match'] == 'lte':
- res &= value <= rule_value
- elif rule['match'] == 'gt':
- res &= value > rule_value
- elif rule['match'] == 'lt':
- res &= value < rule_value
- elif rule['match'] == 'ip_in':
- if isinstance(rule_value, str):
- rule_value = [rule_value]
- res &= '*' in rule_value or contains_ip(value, rule_value)
- elif rule['match'].startswith('m2m'):
- if isinstance(value, Manager):
- value = value.values_list('id', flat=True)
- elif isinstance(value, QuerySet):
- value = value.values_list('id', flat=True)
- elif isinstance(value, models.Model):
- value = [value.id]
- if isinstance(rule_value, (str, int)):
- rule_value = [rule_value]
- value = set(map(str, value))
- rule_value = set(map(str, rule_value))
+ match rule_match:
+ case 'in':
+ res &= value in rule_value or '*' in rule_value
+ case 'exact':
+ res &= value == rule_value or rule_value == '*'
+ case 'contains':
+ res &= rule_value in value
+ case 'startswith':
+ res &= str(value).startswith(str(rule_value))
+ case 'endswith':
+ res &= str(value).endswith(str(rule_value))
+ case 'regex':
+ try:
+ matched = bool(re.search(r'{}'.format(rule_value), value))
+ except Exception as e:
+ logging.error('Error regex match: %s', e)
+ matched = False
+ res &= matched
+ case 'not':
+ res &= value != rule_value
+ case 'gte' | 'lte' | 'gt' | 'lt':
+ operations = {
+ 'gte': lambda x, y: x >= y,
+ 'lte': lambda x, y: x <= y,
+ 'gt': lambda x, y: x > y,
+ 'lt': lambda x, y: x < y
+ }
+ res &= operations[rule_match](value, rule_value)
+ case 'ip_in':
+ if isinstance(rule_value, str):
+ rule_value = [rule_value]
+ res &= '*' in rule_value or contains_ip(value, rule_value)
+ case rule_match if rule_match.startswith('m2m'):
+ if isinstance(value, Manager):
+ value = value.values_list('id', flat=True)
+ elif isinstance(value, QuerySet):
+ value = value.values_list('id', flat=True)
+ elif isinstance(value, models.Model):
+ value = [value.id]
+ if isinstance(rule_value, (str, int)):
+ rule_value = [rule_value]
+ value = set(map(str, value))
+ rule_value = set(map(str, rule_value))
- if rule['match'] == 'm2m_all':
- res &= rule_value.issubset(value)
- else:
- res &= bool(value & rule_value)
- else:
- logging.error("unknown match: {}".format(rule['match']))
- res &= False
+ if rule['match'] == 'm2m_all':
+ res &= rule_value.issubset(value)
+ else:
+ res &= bool(value & rule_value)
+ case __:
+ logging.error("unknown match: {}".format(rule['match']))
+ res &= False
if not res:
return res
+
if custom_q:
res &= to_model.objects.filter(custom_q).filter(id=obj.id).exists()
return res
diff --git a/apps/common/drf/filters.py b/apps/common/drf/filters.py
index 803849909..0ee0a0890 100644
--- a/apps/common/drf/filters.py
+++ b/apps/common/drf/filters.py
@@ -3,6 +3,7 @@
import base64
import json
import logging
+from collections import defaultdict
from django.core.cache import cache
from django.core.exceptions import ImproperlyConfigured
@@ -12,6 +13,7 @@ from rest_framework import filters
from rest_framework.compat import coreapi, coreschema
from rest_framework.fields import DateTimeField
from rest_framework.serializers import ValidationError
+from rest_framework.filters import OrderingFilter
from common import const
from common.db.fields import RelatedManager
@@ -23,6 +25,7 @@ __all__ = [
'IDInFilterBackend', "CustomFilterBackend",
"BaseFilterSet", 'IDNotFilterBackend',
'NotOrRelFilterBackend', 'LabelFilterBackend',
+ 'RewriteOrderingFilter'
]
@@ -180,7 +183,7 @@ class LabelFilterBackend(filters.BaseFilterBackend):
]
@staticmethod
- def parse_label_ids(labels_id):
+ def parse_labels(labels_id):
from labels.models import Label
label_ids = [i.strip() for i in labels_id.split(',')]
cleaned = []
@@ -201,8 +204,8 @@ class LabelFilterBackend(filters.BaseFilterBackend):
q = Q()
for kwarg in args:
q |= Q(**kwarg)
- ids = Label.objects.filter(q).values_list('id', flat=True)
- cleaned.extend(list(ids))
+ labels = Label.objects.filter(q)
+ cleaned.extend(list(labels))
return cleaned
def filter_queryset(self, request, queryset, view):
@@ -221,13 +224,23 @@ class LabelFilterBackend(filters.BaseFilterBackend):
app_label = model._meta.app_label
model_name = model._meta.model_name
- resources = labeled_resource_cls.objects.filter(
+ full_resources = labeled_resource_cls.objects.filter(
res_type__app_label=app_label, res_type__model=model_name,
)
- label_ids = self.parse_label_ids(labels_id)
- resources = model.filter_resources_by_labels(resources, label_ids)
- res_ids = resources.values_list('res_id', flat=True)
- queryset = queryset.filter(id__in=set(res_ids))
+ labels = self.parse_labels(labels_id)
+ grouped = defaultdict(set)
+ for label in labels:
+ grouped[label.name].add(label.id)
+
+ matched_ids = set()
+ for name, label_ids in grouped.items():
+ resources = model.filter_resources_by_labels(full_resources, label_ids, rel='any')
+ res_ids = resources.values_list('res_id', flat=True)
+ if not matched_ids:
+ matched_ids = set(res_ids)
+ else:
+ matched_ids &= set(res_ids)
+ queryset = queryset.filter(id__in=matched_ids)
return queryset
@@ -324,3 +337,17 @@ class NotOrRelFilterBackend(filters.BaseFilterBackend):
queryset.query.where.connector = 'OR'
queryset._result_cache = None
return queryset
+
+
+class RewriteOrderingFilter(OrderingFilter):
+ default_ordering_if_has = ('name', )
+
+ def get_default_ordering(self, view):
+ ordering = super().get_default_ordering(view)
+ # 如果 view.ordering = [] 表示不排序, 这样可以节约性能 (比如: 用户授权的资产)
+ if ordering is not None:
+ return ordering
+ ordering_fields = getattr(view, 'ordering_fields', self.ordering_fields)
+ if ordering_fields:
+ ordering = tuple([f for f in ordering_fields if f in self.default_ordering_if_has])
+ return ordering
diff --git a/apps/common/drf/renders/base.py b/apps/common/drf/renders/base.py
index d79232679..a6eae282e 100644
--- a/apps/common/drf/renders/base.py
+++ b/apps/common/drf/renders/base.py
@@ -4,6 +4,7 @@ import re
from datetime import datetime
import pyzipper
+from django.conf import settings
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.renderers import BaseRenderer
@@ -16,7 +17,7 @@ logger = get_logger(__file__)
class BaseFileRenderer(BaseRenderer):
- # 渲染模版标识, 导入、导出、更新模版: ['import', 'update', 'export']
+ # 渲染模板标识, 导入、导出、更新模板: ['import', 'update', 'export']
template = 'export'
serializer = None
@@ -77,7 +78,7 @@ class BaseFileRenderer(BaseRenderer):
results = [results[0]] if results else results
else:
# 限制数据数量
- results = results[:10000]
+ results = results[:settings.MAX_LIMIT_PER_PAGE]
# 会将一些 UUID 字段转化为 string
results = json.loads(json.dumps(results, cls=encoders.JSONEncoder))
return results
diff --git a/apps/common/drf/renders/csv.py b/apps/common/drf/renders/csv.py
index ba469a21f..88466f533 100644
--- a/apps/common/drf/renders/csv.py
+++ b/apps/common/drf/renders/csv.py
@@ -23,7 +23,14 @@ class CSVFileRenderer(BaseFileRenderer):
self.writer = csv_writer
def write_row(self, row):
- self.writer.writerow(row)
+ row_escape = []
+ for d in row:
+ if isinstance(d, str) and d.strip().startswith(('=', '@')):
+ d = "'{}".format(d)
+ row_escape.append(d)
+ else:
+ row_escape.append(d)
+ self.writer.writerow(row_escape)
def get_rendered_value(self):
value = self.buffer.getvalue()
diff --git a/apps/common/drf/renders/excel.py b/apps/common/drf/renders/excel.py
index 1b3f8dacf..13f70507a 100644
--- a/apps/common/drf/renders/excel.py
+++ b/apps/common/drf/renders/excel.py
@@ -25,7 +25,9 @@ class ExcelFileRenderer(BaseFileRenderer):
# 处理非法字符
column_count += 1
cell_value = ILLEGAL_CHARACTERS_RE.sub(r'', str(cell_value))
- self.ws.cell(row=self.row_count, column=column_count, value=str(cell_value))
+ cell = self.ws.cell(row=self.row_count, column=column_count, value=str(cell_value))
+ # 设置单元格格式为纯文本, 防止执行公式
+ cell.data_type = 's'
def after_render(self):
for col in self.ws.columns:
diff --git a/apps/common/management/commands/services/command.py b/apps/common/management/commands/services/command.py
index 487d9ce5f..34208bc22 100644
--- a/apps/common/management/commands/services/command.py
+++ b/apps/common/management/commands/services/command.py
@@ -27,7 +27,7 @@ class Services(TextChoices):
cls.flower: services.FlowerService,
cls.celery_default: services.CeleryDefaultService,
cls.celery_ansible: services.CeleryAnsibleService,
- cls.beat: services.BeatService
+ cls.beat: services.BeatService,
}
return services_map.get(name)
diff --git a/apps/common/management/commands/services/services/celery_base.py b/apps/common/management/commands/services/services/celery_base.py
index 4e63f4cbb..edfd7bb0e 100644
--- a/apps/common/management/commands/services/services/celery_base.py
+++ b/apps/common/management/commands/services/services/celery_base.py
@@ -12,8 +12,8 @@ class CeleryBaseService(BaseService):
@property
def cmd(self):
print('\n- Start Celery as Distributed Task Queue: {}'.format(self.queue.capitalize()))
- ansible_config_path = os.path.join(settings.APPS_DIR, 'ops', 'ansible', 'ansible.cfg')
- ansible_modules_path = os.path.join(settings.APPS_DIR, 'ops', 'ansible', 'modules')
+ ansible_config_path = os.path.join(settings.APPS_DIR, 'libs', 'ansible', 'ansible.cfg')
+ ansible_modules_path = os.path.join(settings.APPS_DIR, 'libs', 'ansible', 'modules')
os.environ.setdefault('LC_ALL', 'C.UTF-8')
os.environ.setdefault('PYTHONOPTIMIZE', '1')
os.environ.setdefault('ANSIBLE_FORCE_COLOR', 'True')
diff --git a/apps/common/sdk/im/feishu/__init__.py b/apps/common/sdk/im/feishu/__init__.py
index 99194e5c1..c7bf03a06 100644
--- a/apps/common/sdk/im/feishu/__init__.py
+++ b/apps/common/sdk/im/feishu/__init__.py
@@ -2,24 +2,18 @@ import json
from rest_framework.exceptions import APIException
-from django.conf import settings
-from users.utils import construct_user_email
-from common.utils.common import get_logger
-from common.sdk.im.utils import digest
from common.sdk.im.mixin import RequestMixin, BaseRequest
+from common.sdk.im.utils import digest
+from common.utils.common import get_logger
+from users.utils import construct_user_email
logger = get_logger(__name__)
class URL:
# https://open.feishu.cn/document/ukTMukTMukTM/uEDO4UjLxgDO14SM4gTN
- @property
- def host(self):
- if settings.FEISHU_VERSION == 'feishu':
- h = 'https://open.feishu.cn'
- else:
- h = 'https://open.larksuite.com'
- return h
+
+ host = 'https://open.feishu.cn'
@property
def authen(self):
@@ -87,12 +81,13 @@ class FeiShu(RequestMixin):
"""
非业务数据导致的错误直接抛异常,说明是系统配置错误,业务代码不用理会
"""
+ requests_cls = FeishuRequests
def __init__(self, app_id, app_secret, timeout=None):
self._app_id = app_id or ''
self._app_secret = app_secret or ''
- self._requests = FeishuRequests(
+ self._requests = self.requests_cls(
app_id=app_id,
app_secret=app_secret,
timeout=timeout
@@ -130,7 +125,7 @@ class FeiShu(RequestMixin):
body['receive_id'] = user_id
try:
- logger.info(f'Feishu send text: user_ids={user_ids} msg={msg}')
+ logger.info(f'{self.__class__.__name__} send text: user_ids={user_ids} msg={msg}')
self._requests.post(URL().send_message, params=params, json=body)
except APIException as e:
# 只处理可预知的错误
diff --git a/apps/common/sdk/im/lark/__init__.py b/apps/common/sdk/im/lark/__init__.py
new file mode 100644
index 000000000..dcfaa838a
--- /dev/null
+++ b/apps/common/sdk/im/lark/__init__.py
@@ -0,0 +1,16 @@
+from common.utils.common import get_logger
+from ..feishu import URL as FeiShuURL, FeishuRequests, FeiShu
+
+logger = get_logger(__name__)
+
+
+class URL(FeiShuURL):
+ host = 'https://open.larksuite.com'
+
+
+class LarkRequests(FeishuRequests):
+ pass
+
+
+class Lark(FeiShu):
+ requests_cls = LarkRequests
diff --git a/apps/common/sdk/sms/custom.py b/apps/common/sdk/sms/custom.py
index 2cfaca6ca..23a03762a 100644
--- a/apps/common/sdk/sms/custom.py
+++ b/apps/common/sdk/sms/custom.py
@@ -43,6 +43,7 @@ class CustomSMS(BaseSMSClient):
raise JMSException(detail=response.text, code=response.status_code)
except Exception as exc:
logger.error('Custom sms error: {}'.format(exc))
+ raise JMSException(exc)
client = CustomSMS
diff --git a/apps/common/sessions/cache.py b/apps/common/sessions/cache.py
index 805d6738a..43e064948 100644
--- a/apps/common/sessions/cache.py
+++ b/apps/common/sessions/cache.py
@@ -1,16 +1,19 @@
import re
+from importlib import import_module
+from django.conf import settings
from django.contrib.sessions.backends.cache import (
SessionStore as DjangoSessionStore
)
-from django.core.cache import cache
+from django.core.cache import cache, caches
from jumpserver.utils import get_current_request
class SessionStore(DjangoSessionStore):
ignore_urls = [
- r'^/api/v1/users/profile/'
+ r'^/api/v1/users/profile/',
+ r'^/api/v1/authentication/user-session/'
]
def __init__(self, *args, **kwargs):
@@ -32,10 +35,16 @@ class RedisUserSessionManager:
def add_or_increment(self, session_key):
self.client.hincrby(self.JMS_SESSION_KEY, session_key, 1)
- def decrement_or_remove(self, session_key):
- new_count = self.client.hincrby(self.JMS_SESSION_KEY, session_key, -1)
- if new_count <= 0:
+ def decrement(self, session_key):
+ self.client.hincrby(self.JMS_SESSION_KEY, session_key, -1)
+
+ def remove(self, session_key):
+ try:
self.client.hdel(self.JMS_SESSION_KEY, session_key)
+ session_store = import_module(settings.SESSION_ENGINE).SessionStore(session_key)
+ session_store.delete()
+ except Exception:
+ pass
def check_active(self, session_key):
count = self.client.hget(self.JMS_SESSION_KEY, session_key)
@@ -52,5 +61,12 @@ class RedisUserSessionManager:
session_keys.append(key)
return session_keys
+ @staticmethod
+ def get_keys():
+ session_store_cls = import_module(settings.SESSION_ENGINE).SessionStore
+ cache_key_prefix = session_store_cls.cache_key_prefix
+ keys = caches[settings.SESSION_CACHE_ALIAS].iter_keys('*')
+ return [k.replace(cache_key_prefix, '') for k in keys]
+
user_session_manager = RedisUserSessionManager()
diff --git a/apps/common/storage/replay.py b/apps/common/storage/replay.py
index 3d84a234b..63b58c6cd 100644
--- a/apps/common/storage/replay.py
+++ b/apps/common/storage/replay.py
@@ -12,7 +12,7 @@ class ReplayStorageHandler(BaseStorageHandler):
# 获取外部存储路径名
session_path = self.obj.find_ok_relative_path_in_storage(storage)
if not session_path:
- return None
+ return None, None
# 通过外部存储路径名后缀,构造真实的本地存储路径
return session_path, self.obj.get_local_path_by_relative_path(session_path)
diff --git a/apps/common/utils/verify_code.py b/apps/common/utils/verify_code.py
index f9a721c89..7b2589f7d 100644
--- a/apps/common/utils/verify_code.py
+++ b/apps/common/utils/verify_code.py
@@ -30,7 +30,7 @@ class SendAndVerifyCodeUtil(object):
self.other_args = kwargs
def gen_and_send_async(self):
- return send_async.delay(self)
+ return send_async.apply_async(kwargs={"sender": self}, priority=100)
def gen_and_send(self):
ttl = self.__ttl()
diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py
index 5b672d5f9..d2980b5fa 100644
--- a/apps/jumpserver/api.py
+++ b/apps/jumpserver/api.py
@@ -118,9 +118,14 @@ class DateTimeMixin:
return self.get_logs_queryset_filter(qs, 'date_start')
@lazyproperty
- def command_queryset(self):
- qs = Command.objects.all()
- return self.get_logs_queryset_filter(qs, 'timestamp', is_timestamp=True)
+ def command_type_queryset_tuple(self):
+ type_queryset_tuple = Command.get_all_type_queryset_tuple()
+ return (
+ (tp, self.get_logs_queryset_filter(
+ qs, 'timestamp', is_timestamp=True
+ ))
+ for tp, qs in type_queryset_tuple
+ )
@lazyproperty
def job_logs_queryset(self):
@@ -131,7 +136,7 @@ class DateTimeMixin:
class DatesLoginMetricMixin:
dates_list: list
date_start_end: tuple
- command_queryset: Command.objects
+ command_type_queryset_tuple: tuple
sessions_queryset: Session.objects
ftp_logs_queryset: FTPLog.objects
job_logs_queryset: JobLog.objects
@@ -229,13 +234,29 @@ class DatesLoginMetricMixin:
def change_password_logs_amount(self):
return self.password_change_logs_queryset.count()
+ @lazyproperty
+ def command_statistics(self):
+ from terminal.const import CommandStorageType
+ total_amount = 0
+ danger_amount = 0
+ for tp, qs in self.command_type_queryset_tuple:
+ if tp == CommandStorageType.es:
+ total_amount += qs.count(limit_to_max_result_window=False)
+ danger_amount += qs.filter(risk_level=RiskLevelChoices.reject).count(limit_to_max_result_window=False)
+ else:
+ total_amount += qs.count()
+ danger_amount += qs.filter(risk_level=RiskLevelChoices.reject).count()
+ return total_amount, danger_amount
+
@lazyproperty
def commands_amount(self):
- return self.command_queryset.count()
+ total_amount, __ = self.command_statistics
+ return total_amount
@lazyproperty
def commands_danger_amount(self):
- return self.command_queryset.filter(risk_level=RiskLevelChoices.reject).count()
+ __, danger_amount = self.command_statistics
+ return danger_amount
@lazyproperty
def job_logs_running_amount(self):
diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py
index d1caba0cd..7534683b2 100644
--- a/apps/jumpserver/conf.py
+++ b/apps/jumpserver/conf.py
@@ -277,6 +277,7 @@ class Config(dict):
'AUTH_LDAP_START_TLS': False,
'AUTH_LDAP_USER_ATTR_MAP': {"username": "cn", "name": "sn", "email": "mail"},
'AUTH_LDAP_CONNECT_TIMEOUT': 10,
+ 'AUTH_LDAP_CACHE_TIMEOUT': 3600 * 24 * 30,
'AUTH_LDAP_SEARCH_PAGED_SIZE': 1000,
'AUTH_LDAP_SYNC_IS_PERIODIC': False,
'AUTH_LDAP_SYNC_INTERVAL': None,
@@ -407,7 +408,11 @@ class Config(dict):
'AUTH_FEISHU': False,
'FEISHU_APP_ID': '',
'FEISHU_APP_SECRET': '',
- 'FEISHU_VERSION': 'feishu',
+
+ # Lark
+ 'AUTH_LARK': False,
+ 'LARK_APP_ID': '',
+ 'LARK_APP_SECRET': '',
# Slack
'AUTH_SLACK': False,
@@ -609,7 +614,11 @@ class Config(dict):
'FILE_UPLOAD_SIZE_LIMIT_MB': 200,
- 'TICKET_APPLY_ASSET_SCOPE': 'all'
+ 'TICKET_APPLY_ASSET_SCOPE': 'all',
+
+ # Ansible Receptor
+ 'ANSIBLE_RECEPTOR_ENABLE': True,
+ 'ANSIBLE_RECEPTOR_SOCK_PATH': '{}/data/share/control.sock'.format(PROJECT_DIR)
}
old_config_map = {
diff --git a/apps/jumpserver/context_processor.py b/apps/jumpserver/context_processor.py
index 24a96b619..96266605d 100644
--- a/apps/jumpserver/context_processor.py
+++ b/apps/jumpserver/context_processor.py
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
#
+import datetime
+
from django.conf import settings
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
@@ -12,17 +14,18 @@ default_interface = dict((
('login_title', _('JumpServer Open Source Bastion Host')),
('theme', 'classic_green'),
('theme_info', {}),
- ('beian_link', ''),
- ('beian_text', '')
+ ('footer_content', ''),
))
+current_year = datetime.datetime.now().year
+
default_context = {
'DEFAULT_PK': '00000000-0000-0000-0000-000000000000',
'LOGIN_CAS_logo_logout': static('img/login_cas_logo.png'),
'LOGIN_WECOM_logo_logout': static('img/login_wecom_logo.png'),
'LOGIN_DINGTALK_logo_logout': static('img/login_dingtalk_logo.png'),
'LOGIN_FEISHU_logo_logout': static('img/login_feishu_logo.png'),
- 'COPYRIGHT': 'FIT2CLOUD 飞致云' + ' © 2014-2023',
+ 'COPYRIGHT': f'FIT2CLOUD 飞致云 © 2014-{current_year}',
'INTERFACE': default_interface,
}
diff --git a/apps/jumpserver/rewriting/storage/permissions.py b/apps/jumpserver/rewriting/storage/permissions.py
index 7d91c246f..c6473df19 100644
--- a/apps/jumpserver/rewriting/storage/permissions.py
+++ b/apps/jumpserver/rewriting/storage/permissions.py
@@ -4,7 +4,7 @@ path_perms_map = {
'xpack': '*',
'settings': '*',
'img': '*',
- 'replay': 'default',
+ 'replay': 'terminal.view_sessionreplay',
'applets': 'terminal.view_applet',
'virtual_apps': 'terminal.view_virtualapp',
'playbooks': 'ops.view_playbook'
diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py
index 5e44a22a4..60d2aeab8 100644
--- a/apps/jumpserver/settings/auth.py
+++ b/apps/jumpserver/settings/auth.py
@@ -42,7 +42,7 @@ AUTH_LDAP_CONNECTION_OPTIONS = {
ldap.OPT_TIMEOUT: CONFIG.AUTH_LDAP_CONNECT_TIMEOUT,
ldap.OPT_NETWORK_TIMEOUT: CONFIG.AUTH_LDAP_CONNECT_TIMEOUT
}
-AUTH_LDAP_CACHE_TIMEOUT = 1
+AUTH_LDAP_CACHE_TIMEOUT = CONFIG.AUTH_LDAP_CACHE_TIMEOUT
AUTH_LDAP_ALWAYS_UPDATE_USER = True
AUTH_LDAP_SEARCH_PAGED_SIZE = CONFIG.AUTH_LDAP_SEARCH_PAGED_SIZE
@@ -141,7 +141,10 @@ DINGTALK_APPSECRET = CONFIG.DINGTALK_APPSECRET
AUTH_FEISHU = CONFIG.AUTH_FEISHU
FEISHU_APP_ID = CONFIG.FEISHU_APP_ID
FEISHU_APP_SECRET = CONFIG.FEISHU_APP_SECRET
-FEISHU_VERSION = CONFIG.FEISHU_VERSION
+
+AUTH_LARK = CONFIG.AUTH_LARK
+LARK_APP_ID = CONFIG.LARK_APP_ID
+LARK_APP_SECRET = CONFIG.LARK_APP_SECRET
# Slack auth
AUTH_SLACK = CONFIG.AUTH_SLACK
@@ -212,6 +215,7 @@ AUTH_BACKEND_SSO = 'authentication.backends.sso.SSOAuthentication'
AUTH_BACKEND_WECOM = 'authentication.backends.sso.WeComAuthentication'
AUTH_BACKEND_DINGTALK = 'authentication.backends.sso.DingTalkAuthentication'
AUTH_BACKEND_FEISHU = 'authentication.backends.sso.FeiShuAuthentication'
+AUTH_BACKEND_LARK = 'authentication.backends.sso.LarkAuthentication'
AUTH_BACKEND_SLACK = 'authentication.backends.sso.SlackAuthentication'
AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.sso.AuthorizationTokenAuthentication'
AUTH_BACKEND_SAML2 = 'authentication.backends.saml2.SAML2Backend'
@@ -228,7 +232,7 @@ AUTHENTICATION_BACKENDS = [
AUTH_BACKEND_CAS, AUTH_BACKEND_OIDC_PASSWORD, AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_SAML2,
AUTH_BACKEND_OAUTH2,
# 扫码模式
- AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU, AUTH_BACKEND_SLACK,
+ AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU, AUTH_BACKEND_LARK, AUTH_BACKEND_SLACK,
# Token模式
AUTH_BACKEND_AUTH_TOKEN, AUTH_BACKEND_SSO, AUTH_BACKEND_TEMP_TOKEN,
AUTH_BACKEND_PASSKEY
diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py
index 6503b2477..a17c48bfc 100644
--- a/apps/jumpserver/settings/base.py
+++ b/apps/jumpserver/settings/base.py
@@ -38,7 +38,8 @@ BASE_DIR = const.BASE_DIR
PROJECT_DIR = const.PROJECT_DIR
APPS_DIR = os.path.join(PROJECT_DIR, 'apps')
DATA_DIR = os.path.join(PROJECT_DIR, 'data')
-ANSIBLE_DIR = os.path.join(DATA_DIR, 'ansible')
+SHARE_DIR = os.path.join(DATA_DIR, 'share')
+ANSIBLE_DIR = os.path.join(SHARE_DIR, 'ansible')
CERTS_DIR = os.path.join(DATA_DIR, 'certs')
# Quick-start development settings - unsuitable for production
@@ -319,7 +320,6 @@ PRIVATE_STORAGE_AUTH_FUNCTION = 'jumpserver.rewriting.storage.permissions.allow_
PRIVATE_STORAGE_INTERNAL_URL = '/private-media/'
PRIVATE_STORAGE_SERVER = 'jumpserver.rewriting.storage.servers.StaticFileServer'
-
# Use django-bootstrap-form to format template, input max width arg
# BOOTSTRAP_COLUMN_COUNT = 11
diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py
index b564cba25..453648240 100644
--- a/apps/jumpserver/settings/custom.py
+++ b/apps/jumpserver/settings/custom.py
@@ -230,3 +230,7 @@ VIRTUAL_APP_ENABLED = CONFIG.VIRTUAL_APP_ENABLED
FILE_UPLOAD_SIZE_LIMIT_MB = CONFIG.FILE_UPLOAD_SIZE_LIMIT_MB
TICKET_APPLY_ASSET_SCOPE = CONFIG.TICKET_APPLY_ASSET_SCOPE
+
+# Ansible Receptor
+ANSIBLE_RECEPTOR_ENABLE = CONFIG.ANSIBLE_RECEPTOR_ENABLE
+ANSIBLE_RECEPTOR_SOCK_PATH = CONFIG.ANSIBLE_RECEPTOR_SOCK_PATH
diff --git a/apps/jumpserver/settings/libs.py b/apps/jumpserver/settings/libs.py
index f2ace1279..445fa4202 100644
--- a/apps/jumpserver/settings/libs.py
+++ b/apps/jumpserver/settings/libs.py
@@ -38,7 +38,7 @@ REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': (
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
- 'rest_framework.filters.OrderingFilter',
+ 'common.drf.filters.RewriteOrderingFilter',
),
'DEFAULT_METADATA_CLASS': 'common.drf.metadata.SimpleMetadataWithFilters',
'ORDERING_PARAM': "order",
diff --git a/apps/labels/api.py b/apps/labels/api.py
index 1c6965a43..eb34d3621 100644
--- a/apps/labels/api.py
+++ b/apps/labels/api.py
@@ -1,4 +1,7 @@
+from django.core.exceptions import ValidationError
from django.shortcuts import get_object_or_404
+from django.utils.translation import gettext_lazy as _
+from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
@@ -19,8 +22,7 @@ class ContentTypeViewSet(JMSModelViewSet):
serializer_class = ContentTypeSerializer
http_method_names = ['get', 'head', 'options']
rbac_perms = {
- 'default': 'labels.view_contenttype',
- 'resources': 'labels.view_contenttype',
+ 'resources': 'rbac.view_contenttype',
}
page_default_limit = None
can_labeled_content_type = []
@@ -80,6 +82,19 @@ class LabelContentTypeResourceViewSet(JMSModelViewSet):
queryset = content_type.filter_queryset(queryset, keyword)
return queryset
+ @staticmethod
+ def validate_res_ids(content_type: ContentType, res_ids: list):
+ model_cls = content_type.model_class()
+ pk_field = model_cls._meta.pk
+ pk_python_type = pk_field.to_python
+ invalid_ids = []
+ for _id in res_ids:
+ try:
+ pk_python_type(_id)
+ except ValidationError:
+ invalid_ids.append(_id)
+ return invalid_ids
+
def put(self, request, *args, **kwargs):
label_pk = self.kwargs.get('label')
res_type = self.kwargs.get('res_type')
@@ -87,6 +102,13 @@ class LabelContentTypeResourceViewSet(JMSModelViewSet):
label = get_object_or_404(Label, pk=label_pk)
res_ids = request.data.get('res_ids', [])
+ invalid_ids = self.validate_res_ids(content_type, res_ids)
+ if invalid_ids:
+ error = f'{_("Invalid data")}: {", ".join(invalid_ids)}'
+ return Response({
+ "code": 'invalid_data', "detail": error,
+ }, status=status.HTTP_400_BAD_REQUEST)
+
LabeledResource.objects \
.filter(res_type=content_type, label=label) \
.exclude(res_id__in=res_ids).delete()
diff --git a/apps/labels/migrations/0002_auto_20231103_1659.py b/apps/labels/migrations/0002_auto_20231103_1659.py
index 49386ed39..017ccf7ac 100644
--- a/apps/labels/migrations/0002_auto_20231103_1659.py
+++ b/apps/labels/migrations/0002_auto_20231103_1659.py
@@ -13,7 +13,7 @@ def migrate_assets_labels(apps, schema_editor):
new_labels = []
old_new_label_map = {}
for label in old_labels:
- new_label = new_label_model(name=label.name, value=label.value, org_id=label.org_id)
+ new_label = new_label_model(name=label.name, value=label.value, org_id=label.org_id, id=label.id)
old_new_label_map[label.id] = new_label
new_labels.append(new_label)
new_label_model.objects.bulk_create(new_labels, ignore_conflicts=True)
diff --git a/apps/labels/mixins.py b/apps/labels/mixins.py
index 33e73b60b..bb059721d 100644
--- a/apps/labels/mixins.py
+++ b/apps/labels/mixins.py
@@ -1,4 +1,5 @@
from django.contrib.contenttypes.fields import GenericRelation
+from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models import OneToOneField, Count
@@ -38,8 +39,11 @@ class LabeledMixin(models.Model):
self.real.labels.set(value, bulk=False)
@classmethod
- def filter_resources_by_labels(cls, resources, label_ids):
- return cls._get_filter_res_by_labels_m2m_all(resources, label_ids)
+ def filter_resources_by_labels(cls, resources, label_ids, rel='all'):
+ if rel == 'all':
+ return cls._get_filter_res_by_labels_m2m_all(resources, label_ids)
+ else:
+ return cls._get_filter_res_by_labels_m2m_in(resources, label_ids)
@classmethod
def _get_filter_res_by_labels_m2m_in(cls, resources, label_ids):
@@ -60,7 +64,8 @@ class LabeledMixin(models.Model):
@classmethod
def get_labels_filter_attr_q(cls, value, match):
- resources = LabeledResource.objects.all()
+ res_type = ContentType.objects.get_for_model(cls.label_model())
+ resources = LabeledResource.objects.all().filter(res_type=res_type)
if not value:
return None
diff --git a/apps/ops/ansible/modules/__init__.py b/apps/libs/__init__.py
similarity index 100%
rename from apps/ops/ansible/modules/__init__.py
rename to apps/libs/__init__.py
diff --git a/apps/ops/ansible/modules_utils/__init__.py b/apps/libs/ansible/__init__.py
similarity index 100%
rename from apps/ops/ansible/modules_utils/__init__.py
rename to apps/libs/ansible/__init__.py
diff --git a/apps/ops/ansible/ansible.cfg b/apps/libs/ansible/ansible.cfg
similarity index 75%
rename from apps/ops/ansible/ansible.cfg
rename to apps/libs/ansible/ansible.cfg
index 8e0d9918e..d8b66b880 100644
--- a/apps/ops/ansible/ansible.cfg
+++ b/apps/libs/ansible/ansible.cfg
@@ -1,7 +1,7 @@
[defaults]
forks = 10
host_key_checking = False
-library = /opt/jumpserver/apps/ops/ansible/modules:./modules
+library = /opt/jumpserver/apps/libs/ansible/modules:./modules
timeout = 65
[inventory]
[privilege_escalation]
diff --git a/apps/ops/celery/beat/__init__.py b/apps/libs/ansible/modules/__init__.py
similarity index 100%
rename from apps/ops/celery/beat/__init__.py
rename to apps/libs/ansible/modules/__init__.py
diff --git a/apps/ops/ansible/modules/custom_command.py b/apps/libs/ansible/modules/custom_command.py
similarity index 98%
rename from apps/ops/ansible/modules/custom_command.py
rename to apps/libs/ansible/modules/custom_command.py
index 947da2d31..55d0537ab 100644
--- a/apps/ops/ansible/modules/custom_command.py
+++ b/apps/libs/ansible/modules/custom_command.py
@@ -63,7 +63,7 @@ name:
from ansible.module_utils.basic import AnsibleModule
-from ops.ansible.modules_utils.custom_common import (
+from libs.ansible.modules_utils.custom_common import (
SSHClient, common_argument_spec
)
diff --git a/apps/ops/ansible/modules/mongodb_ping.py b/apps/libs/ansible/modules/mongodb_ping.py
similarity index 100%
rename from apps/ops/ansible/modules/mongodb_ping.py
rename to apps/libs/ansible/modules/mongodb_ping.py
diff --git a/apps/ops/ansible/modules/mongodb_user.py b/apps/libs/ansible/modules/mongodb_user.py
similarity index 100%
rename from apps/ops/ansible/modules/mongodb_user.py
rename to apps/libs/ansible/modules/mongodb_user.py
diff --git a/apps/ops/ansible/modules/oracle_info.py b/apps/libs/ansible/modules/oracle_info.py
similarity index 99%
rename from apps/ops/ansible/modules/oracle_info.py
rename to apps/libs/ansible/modules/oracle_info.py
index 005206be9..301f4d3fc 100644
--- a/apps/ops/ansible/modules/oracle_info.py
+++ b/apps/libs/ansible/modules/oracle_info.py
@@ -91,7 +91,7 @@ users:
from ansible.module_utils.basic import AnsibleModule
-from ops.ansible.modules_utils.oracle_common import (
+from libs.ansible.modules_utils.oracle_common import (
OracleClient, oracle_common_argument_spec
)
diff --git a/apps/ops/ansible/modules/oracle_ping.py b/apps/libs/ansible/modules/oracle_ping.py
similarity index 97%
rename from apps/ops/ansible/modules/oracle_ping.py
rename to apps/libs/ansible/modules/oracle_ping.py
index df1069d11..668c16ea6 100644
--- a/apps/ops/ansible/modules/oracle_ping.py
+++ b/apps/libs/ansible/modules/oracle_ping.py
@@ -46,7 +46,7 @@ conn_err_msg:
'''
from ansible.module_utils.basic import AnsibleModule
-from ops.ansible.modules_utils.oracle_common import (
+from libs.ansible.modules_utils.oracle_common import (
OracleClient, oracle_common_argument_spec
)
diff --git a/apps/ops/ansible/modules/oracle_user.py b/apps/libs/ansible/modules/oracle_user.py
similarity index 99%
rename from apps/ops/ansible/modules/oracle_user.py
rename to apps/libs/ansible/modules/oracle_user.py
index c1d485e40..8a70b78e9 100644
--- a/apps/ops/ansible/modules/oracle_user.py
+++ b/apps/libs/ansible/modules/oracle_user.py
@@ -93,7 +93,7 @@ name:
from ansible.module_utils.basic import AnsibleModule
-from ops.ansible.modules_utils.oracle_common import (
+from libs.ansible.modules_utils.oracle_common import (
OracleClient, oracle_common_argument_spec
)
diff --git a/apps/ops/ansible/modules/rdp_ping.py b/apps/libs/ansible/modules/rdp_ping.py
similarity index 100%
rename from apps/ops/ansible/modules/rdp_ping.py
rename to apps/libs/ansible/modules/rdp_ping.py
diff --git a/apps/ops/ansible/modules/ssh_ping.py b/apps/libs/ansible/modules/ssh_ping.py
similarity index 82%
rename from apps/ops/ansible/modules/ssh_ping.py
rename to apps/libs/ansible/modules/ssh_ping.py
index 700291e24..f5b08c63b 100644
--- a/apps/ops/ansible/modules/ssh_ping.py
+++ b/apps/libs/ansible/modules/ssh_ping.py
@@ -7,7 +7,7 @@ __metaclass__ = type
DOCUMENTATION = '''
---
-module: custom_ssh_ping
+module: ssh_ping
short_description: Use ssh to probe whether an asset is connectable
description:
- Use ssh to probe whether an asset is connectable
@@ -16,7 +16,7 @@ description:
EXAMPLES = '''
- name: >
Ping asset server.
- custom_ssh_ping:
+ ssh_ping:
login_host: 127.0.0.1
login_port: 22
login_user: jms
@@ -25,21 +25,16 @@ EXAMPLES = '''
RETURN = '''
is_available:
- description: MongoDB server availability.
+ description: Ping server availability.
returned: always
type: bool
sample: true
-conn_err_msg:
- description: Connection error message.
- returned: always
- type: str
- sample: ''
'''
from ansible.module_utils.basic import AnsibleModule
-from ops.ansible.modules_utils.custom_common import (
+from libs.ansible.modules_utils.custom_common import (
SSHClient, common_argument_spec
)
diff --git a/apps/libs/ansible/modules/telnet_ping.py b/apps/libs/ansible/modules/telnet_ping.py
new file mode 100644
index 000000000..15f07c62e
--- /dev/null
+++ b/apps/libs/ansible/modules/telnet_ping.py
@@ -0,0 +1,70 @@
+#!/usr/bin/python
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+module: telnet_ping
+short_description: Use telnet to probe whether an asset is connectable
+description:
+ - Use telnet to probe whether an asset is connectable
+'''
+
+EXAMPLES = '''
+- name: >
+ Telnet asset server.
+ telnet_ping:
+ login_host: localhost
+ login_port: 22
+'''
+
+RETURN = '''
+is_available:
+ description: Telnet server availability.
+ returned: always
+ type: bool
+ sample: true
+'''
+
+import telnetlib
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+# =========================================
+# Module execution.
+#
+
+def common_argument_spec():
+ options = dict(
+ login_host=dict(type='str', required=False, default='localhost'),
+ login_port=dict(type='int', required=False, default=22),
+ timeout=dict(type='int', required=False, default=10),
+ )
+ return options
+
+
+def main():
+ options = common_argument_spec()
+ module = AnsibleModule(argument_spec=options, supports_check_mode=True, )
+
+ result = {
+ 'changed': False, 'is_available': True
+ }
+ host = module.params['login_host']
+ port = module.params['login_port']
+ timeout = module.params['timeout']
+ try:
+ client = telnetlib.Telnet(host, port, timeout=timeout)
+ client.close()
+ except Exception as err: # noqa
+ result['is_available'] = False
+ module.fail_json(msg='Unable to connect to asset: %s' % err)
+
+ return module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/apps/libs/ansible/modules_utils/__init__.py b/apps/libs/ansible/modules_utils/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/ops/ansible/modules_utils/custom_common.py b/apps/libs/ansible/modules_utils/custom_common.py
similarity index 96%
rename from apps/ops/ansible/modules_utils/custom_common.py
rename to apps/libs/ansible/modules_utils/custom_common.py
index 7015ff2f9..235a65588 100644
--- a/apps/ops/ansible/modules_utils/custom_common.py
+++ b/apps/libs/ansible/modules_utils/custom_common.py
@@ -4,9 +4,8 @@ import time
import paramiko
from sshtunnel import SSHTunnelForwarder
-from packaging import version
-if version.parse(paramiko.__version__) > version.parse("2.8.1"):
+class OldSSHTransport(paramiko.transport.Transport):
_preferred_pubkeys = (
"ssh-ed25519",
"ecdsa-sha2-nistp256",
@@ -17,7 +16,6 @@ if version.parse(paramiko.__version__) > version.parse("2.8.1"):
"rsa-sha2-512",
"ssh-dss",
)
- paramiko.transport.Transport._preferred_pubkeys = _preferred_pubkeys
def common_argument_spec():
@@ -36,6 +34,8 @@ def common_argument_spec():
become_user=dict(type='str', required=False),
become_password=dict(type='str', required=False, no_log=True),
become_private_key_path=dict(type='str', required=False, no_log=True),
+
+ old_ssh_version=dict(type='bool', default=False, required=False),
)
return options
@@ -69,6 +69,8 @@ class SSHClient:
params['username'] = self.module.params['login_user']
params['password'] = self.module.params['login_password']
params['key_filename'] = self.module.params['login_private_key_path'] or None
+ if self.module.params['old_ssh_version']:
+ params['transport_factory'] = OldSSHTransport
return params
def _get_channel(self):
diff --git a/apps/ops/ansible/modules_utils/oracle_common.py b/apps/libs/ansible/modules_utils/oracle_common.py
similarity index 99%
rename from apps/ops/ansible/modules_utils/oracle_common.py
rename to apps/libs/ansible/modules_utils/oracle_common.py
index a89543bc1..024272a1f 100644
--- a/apps/ops/ansible/modules_utils/oracle_common.py
+++ b/apps/libs/ansible/modules_utils/oracle_common.py
@@ -91,4 +91,3 @@ class OracleClient(object):
self._conn.close()
except:
pass
-
diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo
index b9f102de9..2c10fe2d5 100644
--- a/apps/locale/ja/LC_MESSAGES/django.mo
+++ b/apps/locale/ja/LC_MESSAGES/django.mo
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d04781f4f0b0de3ac5f707febb222e239553d6103bca0cec41ab2fd5ab044571
-size 173799
+oid sha256:3758c63d71648cda3c278bf8ae285f688c2d0ea77f5d81fbdb916b01f51f149e
+size 177373
diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po
index 12f6425f0..dc8373e0e 100644
--- a/apps/locale/ja/LC_MESSAGES/django.po
+++ b/apps/locale/ja/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-02-27 16:09+0800\n"
+"POT-Creation-Date: 2024-04-15 17:54+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -22,21 +22,21 @@ msgstr ""
msgid "The parameter 'action' must be [{}]"
msgstr "パラメータ 'action' は [{}] でなければなりません。"
-#: accounts/automations/change_secret/manager.py:201
+#: accounts/automations/change_secret/manager.py:225
#, python-format
msgid "Success: %s, Failed: %s, Total: %s"
msgstr "成功: %s、失敗: %s、合計: %s"
#: accounts/const/account.py:6
-#: accounts/serializers/automations/change_secret.py:32
+#: accounts/serializers/automations/change_secret.py:34
#: assets/models/_user.py:24 audits/signal_handlers/login_log.py:34
#: authentication/confirm/password.py:9 authentication/confirm/password.py:24
-#: authentication/confirm/password.py:26 authentication/forms.py:32
+#: authentication/confirm/password.py:26 authentication/forms.py:28
#: authentication/templates/authentication/login.html:330
#: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47
#: settings/serializers/msg.py:35 terminal/serializers/storage.py:123
-#: terminal/serializers/storage.py:142 users/forms/profile.py:22
-#: users/serializers/user.py:106
+#: terminal/serializers/storage.py:142 users/forms/profile.py:21
+#: users/serializers/user.py:109
#: users/templates/users/_msg_user_created.html:13
#: users/templates/users/user_password_verify.html:18
#: xpack/plugins/cloud/serializers/account_attrs.py:28
@@ -44,7 +44,7 @@ msgid "Password"
msgstr "パスワード"
#: accounts/const/account.py:7
-#: accounts/serializers/automations/change_secret.py:33
+#: accounts/serializers/automations/change_secret.py:35
#: terminal/serializers/storage.py:124
msgid "SSH key"
msgstr "SSH キー"
@@ -80,32 +80,36 @@ msgstr "動的コード"
msgid "Anonymous account"
msgstr "匿名ユーザー"
-#: accounts/const/account.py:25 users/models/user.py:742
+#: accounts/const/account.py:18
+msgid "Specified account"
+msgstr "特定のアカウント"
+
+#: accounts/const/account.py:26 users/models/user.py:752
msgid "Local"
msgstr "ローカル"
-#: accounts/const/account.py:26
+#: accounts/const/account.py:27
msgid "Collected"
msgstr "集めました"
-#: accounts/const/account.py:27 accounts/serializers/account/account.py:28
+#: accounts/const/account.py:28 accounts/serializers/account/account.py:28
#: settings/serializers/auth/sms.py:79
msgid "Template"
msgstr "テンプレート"
-#: accounts/const/account.py:31 ops/const.py:46
+#: accounts/const/account.py:32 ops/const.py:46
msgid "Skip"
msgstr "スキップ"
-#: accounts/const/account.py:32 audits/const.py:24 rbac/tree.py:239
+#: accounts/const/account.py:33 audits/const.py:24 rbac/tree.py:239
#: templates/_csv_import_export.html:18 templates/_csv_update_modal.html:6
msgid "Update"
msgstr "更新"
-#: accounts/const/account.py:33
-#: accounts/serializers/automations/change_secret.py:150 audits/const.py:62
+#: accounts/const/account.py:34 accounts/const/automation.py:109
+#: accounts/serializers/automations/change_secret.py:164 audits/const.py:62
#: audits/signal_handlers/activity_log.py:33 common/const/choices.py:19
-#: ops/const.py:75 terminal/const.py:79 xpack/plugins/cloud/const.py:46
+#: ops/const.py:76 terminal/const.py:79 xpack/plugins/cloud/const.py:47
msgid "Failed"
msgstr "失敗しました"
@@ -206,9 +210,9 @@ msgstr "作成のみ"
#: authentication/serializers/password_mfa.py:16
#: authentication/serializers/password_mfa.py:24
#: notifications/backends/__init__.py:10 settings/serializers/msg.py:22
-#: settings/serializers/msg.py:64 users/forms/profile.py:102
-#: users/forms/profile.py:109 users/models/user.py:802
-#: users/templates/users/forgot_password.html:160
+#: settings/serializers/msg.py:64 users/forms/profile.py:100
+#: users/forms/profile.py:108 users/models/user.py:816
+#: users/templates/users/forgot_password.html:162
#: users/views/profile/reset.py:94
msgid "Email"
msgstr "メール"
@@ -217,6 +221,20 @@ msgstr "メール"
msgid "SFTP"
msgstr "SFTP"
+#: accounts/const/automation.py:110
+#: accounts/serializers/automations/change_secret.py:163 audits/const.py:61
+#: audits/models.py:64 audits/signal_handlers/activity_log.py:33
+#: common/const/choices.py:18 ops/const.py:74 ops/serializers/celery.py:46
+#: terminal/const.py:78 terminal/models/session/sharing.py:121
+#: tickets/views/approve.py:128
+msgid "Success"
+msgstr "成功"
+
+#: accounts/const/automation.py:111 common/const/choices.py:16
+#: terminal/const.py:77 tickets/const.py:29 tickets/const.py:38
+msgid "Pending"
+msgstr "未定"
+
#: accounts/const/vault.py:8 assets/const/category.py:12
#: assets/models/asset/database.py:9 assets/models/asset/database.py:24
msgid "Database"
@@ -249,19 +267,20 @@ msgstr "ユーザー %s がパスワードを閲覧/導き出しました"
#: accounts/serializers/account/account.py:213
#: accounts/serializers/account/account.py:258
#: accounts/serializers/account/gathered_account.py:10
-#: accounts/serializers/automations/change_secret.py:106
-#: accounts/serializers/automations/change_secret.py:126
+#: accounts/serializers/automations/change_secret.py:108
+#: accounts/serializers/automations/change_secret.py:140
#: accounts/templates/accounts/asset_account_change_info.html:7
+#: accounts/templates/accounts/change_secret_failed_info.html:11
#: acls/serializers/base.py:123 assets/models/asset/common.py:95
#: assets/models/asset/common.py:350 assets/models/cmd_filter.py:36
#: audits/models.py:58 authentication/models/connection_token.py:36
#: perms/models/asset_permission.py:69 perms/serializers/permission.py:36
-#: terminal/backends/command/models.py:17 terminal/models/session/session.py:31
+#: terminal/backends/command/models.py:17 terminal/models/session/session.py:32
#: terminal/notifications.py:155 terminal/serializers/command.py:17
#: terminal/serializers/session.py:28
#: terminal/templates/terminal/_msg_command_warning.html:4
#: terminal/templates/terminal/_msg_session_sharing.html:4
-#: tickets/models/ticket/apply_asset.py:16 xpack/plugins/cloud/models.py:256
+#: tickets/models/ticket/apply_asset.py:16 xpack/plugins/cloud/models.py:252
msgid "Asset"
msgstr "資産"
@@ -273,15 +292,14 @@ msgstr "資産"
msgid "Su from"
msgstr "から切り替え"
-#: accounts/models/account.py:55 assets/const/protocol.py:169
-#: settings/serializers/auth/cas.py:20 settings/serializers/auth/feishu.py:20
-#: terminal/models/applet/applet.py:35
+#: accounts/models/account.py:55 assets/const/protocol.py:177
+#: settings/serializers/auth/cas.py:20 terminal/models/applet/applet.py:35
#: terminal/models/virtualapp/virtualapp.py:21
msgid "Version"
msgstr "バージョン"
#: accounts/models/account.py:57 accounts/serializers/account/account.py:215
-#: users/models/user.py:845
+#: users/models/user.py:859
msgid "Source"
msgstr "ソース"
@@ -290,17 +308,18 @@ msgid "Source ID"
msgstr "ソース ID"
#: accounts/models/account.py:61
-#: accounts/serializers/automations/change_secret.py:107
-#: accounts/serializers/automations/change_secret.py:127
+#: accounts/serializers/automations/change_secret.py:110
+#: accounts/serializers/automations/change_secret.py:141
+#: accounts/templates/accounts/change_secret_failed_info.html:12
#: acls/serializers/base.py:124 acls/templates/acls/asset_login_reminder.html:7
#: assets/serializers/asset/common.py:128 assets/serializers/gateway.py:28
-#: audits/models.py:59 authentication/api/connection_token.py:405
+#: audits/models.py:59 authentication/api/connection_token.py:411
#: ops/models/base.py:18 perms/models/asset_permission.py:75
#: perms/serializers/permission.py:41 settings/serializers/msg.py:33
-#: terminal/backends/command/models.py:18 terminal/models/session/session.py:33
+#: terminal/backends/command/models.py:18 terminal/models/session/session.py:34
#: terminal/templates/terminal/_msg_command_warning.html:8
#: terminal/templates/terminal/_msg_session_sharing.html:8
-#: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:89
+#: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:85
msgid "Account"
msgstr "アカウント"
@@ -365,11 +384,11 @@ msgstr "アカウントバックアップ計画"
#: accounts/models/automations/backup_account.py:119
#: assets/models/automations/base.py:115 audits/models.py:65
-#: ops/models/base.py:55 ops/models/celery.py:86 ops/models/job.py:236
+#: ops/models/base.py:55 ops/models/celery.py:88 ops/models/job.py:237
#: ops/templates/ops/celery_task_log.html:75
#: perms/models/asset_permission.py:78
#: settings/templates/ldap/_msg_import_ldap_user.html:5
-#: terminal/models/applet/host.py:141 terminal/models/session/session.py:44
+#: terminal/models/applet/host.py:141 terminal/models/session/session.py:45
#: tickets/models/ticket/apply_application.py:30
#: tickets/models/ticket/apply_asset.py:19
msgid "Date start"
@@ -388,21 +407,21 @@ msgstr "アカウントのバックアップスナップショット"
#: accounts/models/automations/backup_account.py:130
#: accounts/serializers/account/backup.py:49
-#: accounts/serializers/automations/base.py:55
+#: accounts/serializers/automations/base.py:56
#: assets/models/automations/base.py:122
#: assets/serializers/automations/base.py:40
msgid "Trigger mode"
msgstr "トリガーモード"
#: accounts/models/automations/backup_account.py:133 audits/models.py:203
-#: terminal/models/session/sharing.py:125 xpack/plugins/cloud/models.py:208
+#: terminal/models/session/sharing.py:125 xpack/plugins/cloud/models.py:204
msgid "Reason"
msgstr "理由"
#: accounts/models/automations/backup_account.py:135
-#: accounts/serializers/automations/change_secret.py:105
-#: accounts/serializers/automations/change_secret.py:128
-#: ops/serializers/job.py:71 terminal/serializers/session.py:51
+#: accounts/serializers/automations/change_secret.py:107
+#: accounts/serializers/automations/change_secret.py:142
+#: ops/serializers/job.py:71 terminal/serializers/session.py:52
msgid "Is success"
msgstr "成功は"
@@ -453,8 +472,8 @@ msgstr "SSHキープッシュ方式"
#: accounts/models/automations/change_secret.py:15
#: accounts/models/automations/gather_account.py:58
#: accounts/serializers/account/backup.py:41
-#: accounts/serializers/automations/change_secret.py:56
-#: settings/serializers/auth/ldap.py:81
+#: accounts/serializers/automations/change_secret.py:58
+#: settings/serializers/auth/ldap.py:90
msgid "Recipient"
msgstr "受信者"
@@ -476,27 +495,29 @@ msgstr "開始日"
#: accounts/models/automations/change_secret.py:42
#: assets/models/automations/base.py:116 ops/models/base.py:56
-#: ops/models/celery.py:87 ops/models/job.py:237
+#: ops/models/celery.py:89 ops/models/job.py:238
#: terminal/models/applet/host.py:142
msgid "Date finished"
msgstr "終了日"
-#: accounts/models/automations/change_secret.py:43
+#: accounts/models/automations/change_secret.py:44
#: assets/models/automations/base.py:113 audits/models.py:208
-#: audits/serializers.py:54 ops/models/base.py:49 ops/models/job.py:228
+#: audits/serializers.py:54 ops/models/base.py:49 ops/models/job.py:229
#: terminal/models/applet/applet.py:320 terminal/models/applet/host.py:140
#: terminal/models/component/status.py:30
#: terminal/models/virtualapp/virtualapp.py:99
#: terminal/serializers/applet.py:18 terminal/serializers/applet_host.py:136
-#: terminal/serializers/virtualapp.py:35 tickets/models/ticket/general.py:283
+#: terminal/serializers/virtualapp.py:35 tickets/models/ticket/general.py:281
#: tickets/serializers/super_ticket.py:13
-#: tickets/serializers/ticket/ticket.py:20 xpack/plugins/cloud/models.py:204
-#: xpack/plugins/cloud/models.py:260
+#: tickets/serializers/ticket/ticket.py:20 xpack/plugins/cloud/models.py:200
+#: xpack/plugins/cloud/models.py:256
msgid "Status"
msgstr "ステータス"
-#: accounts/models/automations/change_secret.py:44
-#: accounts/serializers/account/account.py:260 assets/const/automation.py:8
+#: accounts/models/automations/change_secret.py:47
+#: accounts/serializers/account/account.py:260
+#: accounts/templates/accounts/change_secret_failed_info.html:13
+#: assets/const/automation.py:8
#: authentication/templates/authentication/passkey.html:173
#: authentication/views/base.py:42 authentication/views/base.py:43
#: authentication/views/base.py:44 common/const/choices.py:20
@@ -504,7 +525,7 @@ msgstr "ステータス"
msgid "Error"
msgstr "間違い"
-#: accounts/models/automations/change_secret.py:48
+#: accounts/models/automations/change_secret.py:51
msgid "Change secret record"
msgstr "パスワード レコードの変更"
@@ -520,12 +541,12 @@ msgstr "最終ログイン日"
#: accounts/models/automations/push_account.py:15 accounts/models/base.py:65
#: accounts/serializers/account/virtual.py:21 acls/serializers/base.py:19
#: acls/serializers/base.py:50 assets/models/_user.py:23 audits/models.py:188
-#: authentication/forms.py:25 authentication/forms.py:27
+#: authentication/forms.py:21 authentication/forms.py:23
#: authentication/models/temp_token.py:9
#: authentication/templates/authentication/_msg_different_city.html:9
#: authentication/templates/authentication/_msg_oauth_bind.html:9
-#: terminal/serializers/storage.py:136 users/forms/profile.py:32
-#: users/forms/profile.py:115 users/models/user.py:798
+#: terminal/serializers/storage.py:136 users/forms/profile.py:31
+#: users/forms/profile.py:114 users/models/user.py:812
#: users/templates/users/_msg_user_created.html:12
#: xpack/plugins/cloud/serializers/account_attrs.py:26
msgid "Username"
@@ -557,6 +578,7 @@ msgstr "トリガー方式"
#: audits/models.py:92 audits/serializers.py:84
#: authentication/serializers/connect_token_secret.py:119
#: authentication/templates/authentication/_access_key_modal.html:34
+#: tickets/serializers/ticket/ticket.py:21
msgid "Action"
msgstr "アクション"
@@ -571,7 +593,7 @@ msgstr "アカウントの確認"
#: accounts/models/base.py:37 accounts/models/base.py:67
#: accounts/serializers/account/account.py:440
#: accounts/serializers/account/base.py:17
-#: accounts/serializers/automations/change_secret.py:45
+#: accounts/serializers/automations/change_secret.py:47
#: authentication/serializers/connect_token_secret.py:42
#: authentication/serializers/connect_token_secret.py:51
#: terminal/serializers/storage.py:140
@@ -587,12 +609,12 @@ msgid "Secret"
msgstr "ひみつ"
#: accounts/models/base.py:42
-#: accounts/serializers/automations/change_secret.py:39
+#: accounts/serializers/automations/change_secret.py:41
msgid "Secret strategy"
msgstr "鍵ポリシー"
#: accounts/models/base.py:44 accounts/serializers/account/template.py:24
-#: accounts/serializers/automations/change_secret.py:44
+#: accounts/serializers/automations/change_secret.py:46
msgid "Password rules"
msgstr "パスワードルール"
@@ -609,7 +631,7 @@ msgstr "パスワードルール"
#: authentication/serializers/connect_token_secret.py:113
#: authentication/serializers/connect_token_secret.py:168 labels/models.py:11
#: ops/mixin.py:21 ops/models/adhoc.py:20 ops/models/celery.py:15
-#: ops/models/celery.py:80 ops/models/job.py:137 ops/models/playbook.py:28
+#: ops/models/celery.py:80 ops/models/job.py:138 ops/models/playbook.py:28
#: ops/serializers/job.py:18 orgs/models.py:82
#: perms/models/asset_permission.py:61 rbac/models/role.py:29
#: settings/models.py:33 settings/models.py:181 settings/serializers/msg.py:89
@@ -619,9 +641,9 @@ msgstr "パスワードルール"
#: terminal/models/component/terminal.py:85
#: terminal/models/virtualapp/provider.py:10
#: terminal/models/virtualapp/virtualapp.py:19 tickets/api/ticket.py:87
-#: users/forms/profile.py:33 users/models/group.py:13
-#: users/models/preference.py:11 users/models/user.py:800
-#: xpack/plugins/cloud/models.py:32 xpack/plugins/cloud/models.py:276
+#: users/forms/profile.py:32 users/models/group.py:13
+#: users/models/preference.py:11 users/models/user.py:814
+#: xpack/plugins/cloud/models.py:32 xpack/plugins/cloud/models.py:272
#: xpack/plugins/cloud/serializers/task.py:70
msgid "Name"
msgstr "名前"
@@ -636,7 +658,7 @@ msgstr "特権アカウント"
#: authentication/serializers/connect_token_secret.py:117
#: terminal/models/applet/applet.py:40
#: terminal/models/component/endpoint.py:120
-#: terminal/models/virtualapp/virtualapp.py:23 users/serializers/user.py:169
+#: terminal/models/virtualapp/virtualapp.py:23 users/serializers/user.py:173
msgid "Is active"
msgstr "アクティブです。"
@@ -652,7 +674,7 @@ msgstr "プラットフォーム"
msgid "Push params"
msgstr "パラメータをプッシュする"
-#: accounts/models/template.py:26 xpack/plugins/cloud/models.py:333
+#: accounts/models/template.py:26 xpack/plugins/cloud/models.py:329
msgid "Account template"
msgstr "アカウント テンプレート"
@@ -693,11 +715,11 @@ msgstr ""
"ユーザー名とパスワードを使用せずにアセットに接続します。Webベースとカスタムタ"
"イプのアセットのみをサポートします"
-#: accounts/notifications.py:11 accounts/notifications.py:36
+#: accounts/notifications.py:12 accounts/notifications.py:37
msgid "Notification of account backup route task results"
msgstr "アカウントバックアップルートタスクの結果の通知"
-#: accounts/notifications.py:21 accounts/notifications.py:45
+#: accounts/notifications.py:22 accounts/notifications.py:46
msgid ""
"{} - The account backup passage task has been completed. See the attachment "
"for details"
@@ -705,7 +727,7 @@ msgstr ""
"{} -アカウントバックアップの通過タスクが完了しました。詳細は添付ファイルをご"
"覧ください"
-#: accounts/notifications.py:24
+#: accounts/notifications.py:25
msgid ""
"{} - The account backup passage task has been completed: the encryption "
"password has not been set - please go to personal information -> Basic file "
@@ -715,17 +737,17 @@ msgstr ""
"されていません-個人情報にアクセスしてください-> プリファレンス設定の基本的な"
"ファイル暗号化パスワードの設定"
-#: accounts/notifications.py:55
+#: accounts/notifications.py:56
msgid "Notification of implementation result of encryption change plan"
msgstr "暗号化変更プランの実装結果の通知"
-#: accounts/notifications.py:66
+#: accounts/notifications.py:67
msgid ""
"{} - The encryption change task has been completed. See the attachment for "
"details"
msgstr "{} -暗号化変更タスクが完了しました。詳細は添付ファイルをご覧ください"
-#: accounts/notifications.py:70
+#: accounts/notifications.py:71
msgid ""
"{} - The encryption change task has been completed: the encryption password "
"has not been set - please go to personal information -> set encryption "
@@ -734,10 +756,15 @@ msgstr ""
"{} -暗号化変更タスクが完了しました: 暗号化パスワードが設定されていません-個人"
"情報にアクセスしてください-> 環境設定で暗号化パスワードを設定する"
-#: accounts/notifications.py:82
+#: accounts/notifications.py:83
+#: accounts/templates/accounts/asset_account_change_info.html:3
msgid "Gather account change information"
msgstr "アカウント変更情報"
+#: accounts/notifications.py:105
+msgid "Change secret or push account failed information"
+msgstr "パスワード変更またはアカウントプッシュ失敗情報"
+
#: accounts/serializers/account/account.py:31
msgid "Push now"
msgstr "今すぐプッシュ"
@@ -756,21 +783,21 @@ msgid "Category"
msgstr "カテゴリ"
#: accounts/serializers/account/account.py:194
-#: accounts/serializers/automations/base.py:54 acls/models/command_acl.py:24
+#: accounts/serializers/automations/base.py:55 acls/models/command_acl.py:24
#: acls/serializers/command_acl.py:19 applications/models.py:14
#: assets/models/_user.py:50 assets/models/automations/base.py:20
#: assets/models/cmd_filter.py:74 assets/models/platform.py:97
#: assets/serializers/asset/common.py:126 assets/serializers/platform.py:120
#: assets/serializers/platform.py:139 audits/serializers.py:53
#: audits/serializers.py:170
-#: authentication/serializers/connect_token_secret.py:126 ops/models/job.py:145
+#: authentication/serializers/connect_token_secret.py:126 ops/models/job.py:146
#: perms/serializers/user_permission.py:27 terminal/models/applet/applet.py:39
#: terminal/models/component/storage.py:57
#: terminal/models/component/storage.py:146 terminal/serializers/applet.py:29
#: terminal/serializers/session.py:23 terminal/serializers/storage.py:264
#: terminal/serializers/storage.py:276 tickets/models/comment.py:26
#: tickets/models/flow.py:56 tickets/models/ticket/apply_application.py:16
-#: tickets/models/ticket/general.py:275 tickets/serializers/flow.py:53
+#: tickets/models/ticket/general.py:273 tickets/serializers/flow.py:53
#: tickets/serializers/ticket/ticket.py:19
msgid "Type"
msgstr "タイプ"
@@ -784,9 +811,8 @@ msgid "Has secret"
msgstr "エスクローされたパスワード"
#: accounts/serializers/account/account.py:259 ops/models/celery.py:83
-#: tickets/models/comment.py:13 tickets/models/ticket/general.py:45
-#: tickets/models/ticket/general.py:279 tickets/serializers/super_ticket.py:14
-#: tickets/serializers/ticket/ticket.py:21
+#: tickets/models/comment.py:13 tickets/models/ticket/general.py:46
+#: tickets/models/ticket/general.py:277 tickets/serializers/super_ticket.py:14
msgid "State"
msgstr "状態"
@@ -799,8 +825,8 @@ msgstr "編集済み"
#: acls/templates/acls/asset_login_reminder.html:6
#: assets/models/automations/base.py:19
#: assets/serializers/automations/base.py:20
-#: authentication/api/connection_token.py:404 ops/models/base.py:17
-#: ops/models/job.py:147 ops/serializers/job.py:19
+#: authentication/api/connection_token.py:410 ops/models/base.py:17
+#: ops/models/job.py:148 ops/serializers/job.py:19
#: terminal/templates/terminal/_msg_command_execute_alert.html:16
msgid "Assets"
msgstr "資産"
@@ -836,13 +862,13 @@ msgstr "ID"
#: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:63
#: perms/serializers/permission.py:32 rbac/builtin.py:124
#: rbac/models/rolebinding.py:49 rbac/serializers/rolebinding.py:17
-#: terminal/backends/command/models.py:16 terminal/models/session/session.py:29
+#: terminal/backends/command/models.py:16 terminal/models/session/session.py:30
#: terminal/models/session/sharing.py:34 terminal/notifications.py:156
#: terminal/notifications.py:205 terminal/serializers/command.py:16
#: terminal/templates/terminal/_msg_command_warning.html:6
#: terminal/templates/terminal/_msg_session_sharing.html:6
-#: tickets/models/comment.py:21 users/const.py:14 users/models/user.py:1004
-#: users/models/user.py:1041 users/serializers/group.py:21
+#: tickets/models/comment.py:21 users/const.py:14 users/models/user.py:1019
+#: users/models/user.py:1057 users/serializers/group.py:21
msgid "User"
msgstr "ユーザー"
@@ -853,19 +879,20 @@ msgid "Date"
msgstr "日付"
#: accounts/serializers/account/backup.py:38
-#: accounts/serializers/automations/base.py:36
+#: accounts/serializers/automations/base.py:24
+#: accounts/serializers/automations/base.py:37
#: assets/serializers/automations/base.py:34 ops/mixin.py:23 ops/mixin.py:104
#: settings/serializers/auth/ldap.py:66
msgid "Periodic perform"
msgstr "定期的なパフォーマンス"
#: accounts/serializers/account/backup.py:39
-#: accounts/serializers/automations/base.py:37
+#: accounts/serializers/automations/base.py:38
msgid "Executed amount"
msgstr "実行回数"
#: accounts/serializers/account/backup.py:42
-#: accounts/serializers/automations/change_secret.py:57
+#: accounts/serializers/automations/change_secret.py:59
msgid "Currently only mail sending is supported"
msgstr "現在、メール送信のみがサポートされています"
@@ -931,15 +958,15 @@ msgstr "关联平台,可以配置推送参数,如果不关联,则使用默
#: accounts/serializers/account/virtual.py:19 assets/models/_user.py:27
#: assets/models/cmd_filter.py:40 assets/models/cmd_filter.py:88
#: assets/models/group.py:20 common/db/models.py:36 ops/models/adhoc.py:26
-#: ops/models/job.py:153 ops/models/playbook.py:31 rbac/models/role.py:37
+#: ops/models/job.py:154 ops/models/playbook.py:31 rbac/models/role.py:37
#: settings/models.py:38 terminal/models/applet/applet.py:45
#: terminal/models/applet/applet.py:321 terminal/models/applet/host.py:143
#: terminal/models/component/endpoint.py:25
#: terminal/models/component/endpoint.py:119
-#: terminal/models/session/session.py:46
+#: terminal/models/session/session.py:47
#: terminal/models/virtualapp/virtualapp.py:28 tickets/models/comment.py:32
-#: tickets/models/ticket/general.py:297 users/models/user.py:836
-#: xpack/plugins/cloud/models.py:39 xpack/plugins/cloud/models.py:110
+#: tickets/models/ticket/general.py:295 users/models/user.py:850
+#: xpack/plugins/cloud/models.py:39 xpack/plugins/cloud/models.py:106
msgid "Comment"
msgstr "コメント"
@@ -959,41 +986,33 @@ msgstr ""
msgid "Nodes"
msgstr "ノード"
-#: accounts/serializers/automations/base.py:44
+#: accounts/serializers/automations/base.py:45
msgid "Name already exists"
msgstr "名前は既に存在します。"
-#: accounts/serializers/automations/base.py:53
+#: accounts/serializers/automations/base.py:54
#: assets/models/automations/base.py:118
#: assets/serializers/automations/base.py:39
msgid "Automation snapshot"
msgstr "自動スナップショット"
-#: accounts/serializers/automations/change_secret.py:42
+#: accounts/serializers/automations/change_secret.py:44
msgid "SSH Key strategy"
msgstr "SSHキー戦略"
-#: accounts/serializers/automations/change_secret.py:79
+#: accounts/serializers/automations/change_secret.py:81
msgid "* Please enter the correct password length"
msgstr "* 正しいパスワードの長さを入力してください"
-#: accounts/serializers/automations/change_secret.py:83
+#: accounts/serializers/automations/change_secret.py:85
msgid "* Password length range 6-30 bits"
msgstr "* パスワードの長さの範囲6-30ビット"
-#: accounts/serializers/automations/change_secret.py:109
+#: accounts/serializers/automations/change_secret.py:114
#: assets/models/automations/base.py:127
msgid "Automation task execution"
msgstr "自動タスク実行履歴"
-#: accounts/serializers/automations/change_secret.py:149 audits/const.py:61
-#: audits/models.py:64 audits/signal_handlers/activity_log.py:33
-#: common/const/choices.py:18 ops/const.py:73 ops/serializers/celery.py:46
-#: terminal/const.py:78 terminal/models/session/sharing.py:121
-#: tickets/views/approve.py:128
-msgid "Success"
-msgstr "成功"
-
#: accounts/signal_handlers.py:47
#, python-format
msgid "Push related accounts to assets: %s, by system"
@@ -1013,7 +1032,7 @@ msgstr "アカウントを削除: %s"
msgid "Account execute automation"
msgstr "アカウント実行の自動化"
-#: accounts/tasks/automation.py:51 accounts/tasks/automation.py:62
+#: accounts/tasks/automation.py:51 accounts/tasks/automation.py:56
msgid "Execute automation record"
msgstr "自動化レコードを実行する"
@@ -1061,6 +1080,29 @@ msgstr "新規アカウント"
msgid "Deleted account"
msgstr "アカウントの削除"
+#: accounts/templates/accounts/change_secret_failed_info.html:3
+#: ops/templates/ops/celery_task_log.html:71 terminal/serializers/task.py:10
+msgid "Task name"
+msgstr "タスク名"
+
+#: accounts/templates/accounts/change_secret_failed_info.html:4
+msgid "Task execution id"
+msgstr "タスク実行ID"
+
+#: accounts/templates/accounts/change_secret_failed_info.html:5
+#: acls/templates/acls/asset_login_reminder.html:3
+#: acls/templates/acls/user_login_reminder.html:3
+msgid "Respectful"
+msgstr "尊敬する"
+
+#: accounts/templates/accounts/change_secret_failed_info.html:6
+msgid ""
+"Hello! The following is the failure of changing the password of your assets "
+"or pushing the account. Please check and handle it in time."
+msgstr ""
+"こんにちは! アセットの変更またはアカウントのプッシュが失敗する状況は次のとお"
+"りです。 時間内に確認して対処してください。"
+
#: accounts/utils.py:52
msgid ""
"If the password starts with {{` and ends with }} `, then the password is not "
@@ -1076,7 +1118,7 @@ msgstr "秘密鍵が無効またはpassphraseエラー"
msgid "Acls"
msgstr "Acls"
-#: acls/const.py:6 audits/const.py:36 terminal/const.py:11 tickets/const.py:46
+#: acls/const.py:6 audits/const.py:36 terminal/const.py:11 tickets/const.py:44
#: tickets/templates/tickets/approve_check_password.html:47
msgid "Reject"
msgstr "拒否"
@@ -1099,13 +1141,13 @@ msgstr "通知"
#: acls/models/base.py:37 assets/models/_user.py:51
#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:112
-#: xpack/plugins/cloud/models.py:282
+#: xpack/plugins/cloud/models.py:278
msgid "Priority"
msgstr "優先順位"
#: acls/models/base.py:38 assets/models/_user.py:51
#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:113
-#: xpack/plugins/cloud/models.py:283
+#: xpack/plugins/cloud/models.py:279
msgid "1-100, the lower the value will be match first"
msgstr "1-100、低い値は最初に一致します"
@@ -1118,7 +1160,7 @@ msgstr "レビュー担当者"
#: authentication/models/connection_token.py:53
#: authentication/templates/authentication/_access_key_modal.html:32
#: perms/models/asset_permission.py:82 terminal/models/session/sharing.py:29
-#: tickets/const.py:38
+#: tickets/const.py:36
msgid "Active"
msgstr "アクティブ"
@@ -1134,7 +1176,7 @@ msgstr "アカウント"
#: acls/models/command_acl.py:16 assets/models/cmd_filter.py:60
#: ops/serializers/job.py:70 terminal/const.py:86
-#: terminal/models/session/session.py:42 terminal/serializers/command.py:18
+#: terminal/models/session/session.py:43 terminal/serializers/command.py:18
#: terminal/templates/terminal/_msg_command_alert.html:12
#: terminal/templates/terminal/_msg_command_execute_alert.html:10
#: terminal/templates/terminal/_msg_command_warning.html:23
@@ -1142,7 +1184,7 @@ msgid "Command"
msgstr "コマンド"
#: acls/models/command_acl.py:17 assets/models/cmd_filter.py:59
-#: xpack/plugins/cloud/models.py:299
+#: xpack/plugins/cloud/models.py:295
msgid "Regex"
msgstr "正規情報"
@@ -1272,11 +1314,6 @@ msgstr "IP"
msgid "Time Period"
msgstr "期間"
-#: acls/templates/acls/asset_login_reminder.html:3
-#: acls/templates/acls/user_login_reminder.html:3
-msgid "Respectful"
-msgstr "尊敬する"
-
#: acls/templates/acls/asset_login_reminder.html:10
msgid ""
"The user has just logged in to the asset. Please ensure that this is an "
@@ -1318,7 +1355,7 @@ msgid "Applications"
msgstr "アプリケーション"
#: applications/models.py:16 xpack/plugins/cloud/models.py:37
-#: xpack/plugins/cloud/serializers/account.py:67
+#: xpack/plugins/cloud/serializers/account.py:68
msgid "Attrs"
msgstr "ツールバーの"
@@ -1336,7 +1373,7 @@ msgstr ""
"資産を直接作成することはできません。ホストまたはその他を作成する必要がありま"
"す"
-#: assets/api/domain.py:64
+#: assets/api/domain.py:67
msgid "Number required"
msgstr "必要な数"
@@ -1369,7 +1406,7 @@ msgid " - Platform {} ansible disabled"
msgstr " - プラットフォーム {} ansible 無効"
#: assets/automations/ping_gateway/manager.py:33
-#: authentication/models/connection_token.py:128
+#: authentication/models/connection_token.py:131
msgid "No account"
msgstr "アカウントなし"
@@ -1383,7 +1420,8 @@ msgid "Unable to connect to port {port} on {address}"
msgstr "{port} のポート {address} に接続できません"
#: assets/automations/ping_gateway/manager.py:58
-#: authentication/middleware.py:93 xpack/plugins/cloud/providers/fc.py:47
+#: authentication/backends/oauth2/views.py:60 authentication/middleware.py:93
+#: xpack/plugins/cloud/providers/fc.py:47
msgid "Authentication failed"
msgstr "認証に失敗しました"
@@ -1422,11 +1460,11 @@ msgstr "無効"
#: assets/const/base.py:33 settings/serializers/basic.py:8
#: users/serializers/preference/koko.py:19
#: users/serializers/preference/lina.py:39
-#: users/serializers/preference/luna.py:73
+#: users/serializers/preference/luna.py:77
msgid "Basic"
msgstr "基本"
-#: assets/const/base.py:34 assets/const/protocol.py:252
+#: assets/const/base.py:34 assets/const/protocol.py:268
#: assets/models/asset/web.py:13
msgid "Script"
msgstr "脚本"
@@ -1449,7 +1487,7 @@ msgstr "クラウド サービス"
#: assets/const/category.py:14 assets/models/asset/gpt.py:11
#: assets/models/asset/web.py:16 audits/const.py:42
-#: terminal/models/applet/applet.py:27 users/const.py:59
+#: terminal/models/applet/applet.py:27 users/const.py:64
msgid "Web"
msgstr "Web"
@@ -1494,11 +1532,19 @@ msgstr "ChatGPT"
msgid "Other"
msgstr "その他"
-#: assets/const/protocol.py:49
+#: assets/const/protocol.py:45
+msgid "Old SSH version"
+msgstr "古いSSHバージョン"
+
+#: assets/const/protocol.py:46
+msgid "Old SSH version like openssh 5.x or 6.x"
+msgstr "openssh 5.x または 6.x などの古い SSH バージョン"
+
+#: assets/const/protocol.py:57
msgid "SFTP root"
msgstr "SFTPルート"
-#: assets/const/protocol.py:51
+#: assets/const/protocol.py:59
#, python-brace-format
msgid ""
"SFTP root directory, Support variable:
- ${ACCOUNT} The connected "
@@ -1509,81 +1555,89 @@ msgstr ""
"ユーザー名
-${HOME}接続されたアカウントのホームディレクトリ
-${USER}"
"ユーザーのユーザー名"
-#: assets/const/protocol.py:66
+#: assets/const/protocol.py:74
msgid "Console"
msgstr "Console"
-#: assets/const/protocol.py:67
+#: assets/const/protocol.py:75
msgid "Connect to console session"
msgstr "コンソールセッションに接続"
-#: assets/const/protocol.py:71
+#: assets/const/protocol.py:79
msgid "Any"
msgstr "任意"
-#: assets/const/protocol.py:73 settings/serializers/security.py:228
+#: assets/const/protocol.py:81 settings/serializers/security.py:232
msgid "Security"
msgstr "セキュリティ"
-#: assets/const/protocol.py:74
+#: assets/const/protocol.py:82
msgid "Security layer to use for the connection"
msgstr "接続に使用するセキュリティ レイヤー"
-#: assets/const/protocol.py:80
+#: assets/const/protocol.py:88
msgid "AD domain"
msgstr "AD ドメイン"
-#: assets/const/protocol.py:95
+#: assets/const/protocol.py:103
msgid "Username prompt"
msgstr "ユーザー名プロンプト"
-#: assets/const/protocol.py:96
+#: assets/const/protocol.py:104
msgid "We will send username when we see this prompt"
msgstr "このプロンプトが表示されたらユーザー名を送信します"
-#: assets/const/protocol.py:101
+#: assets/const/protocol.py:109
msgid "Password prompt"
msgstr "パスワードプロンプト"
-#: assets/const/protocol.py:102
+#: assets/const/protocol.py:110
msgid "We will send password when we see this prompt"
msgstr "このプロンプトが表示されたらパスワードを送信します"
-#: assets/const/protocol.py:107
+#: assets/const/protocol.py:115
msgid "Success prompt"
msgstr "成功プロンプト"
-#: assets/const/protocol.py:108
+#: assets/const/protocol.py:116
msgid "We will consider login success when we see this prompt"
msgstr "このプロンプトが表示されたらログイン成功とみなします"
-#: assets/const/protocol.py:119 assets/models/asset/database.py:10
+#: assets/const/protocol.py:127 assets/models/asset/database.py:10
#: settings/serializers/msg.py:47
msgid "Use SSL"
msgstr "SSLの使用"
-#: assets/const/protocol.py:154
+#: assets/const/protocol.py:162
msgid "SYSDBA"
msgstr "SYSDBA"
-#: assets/const/protocol.py:155
+#: assets/const/protocol.py:163
msgid "Connect as SYSDBA"
msgstr "SYSDBA として接続"
-#: assets/const/protocol.py:170
+#: assets/const/protocol.py:178
msgid ""
"SQL Server version, Different versions have different connection drivers"
msgstr "SQL Server のバージョン。バージョンによって接続ドライバが異なります"
-#: assets/const/protocol.py:199
+#: assets/const/protocol.py:202
+msgid "Auth source"
+msgstr "認証データベース"
+
+#: assets/const/protocol.py:203
+msgid "The database to authenticate against"
+msgstr "認証するデータベース"
+
+#: assets/const/protocol.py:215
msgid "Auth username"
msgstr "ユーザー名で認証する"
-#: assets/const/protocol.py:222
+#: assets/const/protocol.py:238
msgid "Safe mode"
msgstr "安全モード"
-#: assets/const/protocol.py:224
+#: assets/const/protocol.py:240
msgid ""
"When safe mode is enabled, some operations will be disabled, such as: New "
"tab, right click, visit other website, etc."
@@ -1591,24 +1645,24 @@ msgstr ""
"安全モードが有効になっている場合、新しいタブ、右クリック、他のウェブサイトへ"
"のアクセスなど、一部の操作が無効になります"
-#: assets/const/protocol.py:229 assets/models/asset/web.py:9
+#: assets/const/protocol.py:245 assets/models/asset/web.py:9
#: assets/serializers/asset/info/spec.py:16
msgid "Autofill"
msgstr "自動充填"
-#: assets/const/protocol.py:237 assets/models/asset/web.py:10
+#: assets/const/protocol.py:253 assets/models/asset/web.py:10
msgid "Username selector"
msgstr "ユーザー名ピッカー"
-#: assets/const/protocol.py:242 assets/models/asset/web.py:11
+#: assets/const/protocol.py:258 assets/models/asset/web.py:11
msgid "Password selector"
msgstr "パスワードセレクター"
-#: assets/const/protocol.py:247 assets/models/asset/web.py:12
+#: assets/const/protocol.py:263 assets/models/asset/web.py:12
msgid "Submit selector"
msgstr "ボタンセレクターを確認する"
-#: assets/const/protocol.py:270
+#: assets/const/protocol.py:286
msgid "API mode"
msgstr "APIモード"
@@ -1635,18 +1689,18 @@ msgstr "SSHパブリックキー"
#: assets/models/_user.py:28 assets/models/automations/base.py:114
#: assets/models/cmd_filter.py:41 assets/models/group.py:19
#: audits/models.py:267 common/db/models.py:34 ops/models/base.py:54
-#: ops/models/job.py:235 users/models/user.py:1042
+#: ops/models/job.py:236 users/models/user.py:1058
msgid "Date created"
msgstr "作成された日付"
#: assets/models/_user.py:29 assets/models/cmd_filter.py:42
-#: common/db/models.py:35 users/models/user.py:854
+#: common/db/models.py:35 users/models/user.py:868
msgid "Date updated"
msgstr "更新日"
#: assets/models/_user.py:30 assets/models/cmd_filter.py:44
#: assets/models/cmd_filter.py:91 assets/models/group.py:18
-#: common/db/models.py:32 users/models/user.py:843
+#: common/db/models.py:32 users/models/user.py:857
#: users/serializers/group.py:32
msgid "Created by"
msgstr "によって作成された"
@@ -1675,7 +1729,7 @@ msgstr "ユーザーと同じユーザー名"
#: authentication/serializers/connect_token_secret.py:114
#: settings/serializers/msg.py:29 terminal/models/applet/applet.py:42
#: terminal/models/virtualapp/virtualapp.py:24
-#: terminal/serializers/session.py:21 terminal/serializers/session.py:47
+#: terminal/serializers/session.py:21 terminal/serializers/session.py:48
#: terminal/serializers/storage.py:71
msgid "Protocol"
msgstr "プロトコル"
@@ -1684,7 +1738,7 @@ msgstr "プロトコル"
msgid "Sudo"
msgstr "すど"
-#: assets/models/_user.py:55 ops/const.py:50 ops/const.py:60
+#: assets/models/_user.py:55 ops/const.py:50 ops/const.py:61
msgid "Shell"
msgstr "シェル"
@@ -1738,20 +1792,20 @@ msgstr "アドレス"
#: assets/models/asset/common.py:161 assets/models/platform.py:126
#: authentication/backends/passkey/models.py:12
#: authentication/serializers/connect_token_secret.py:118
-#: perms/serializers/user_permission.py:25 xpack/plugins/cloud/models.py:329
+#: perms/serializers/user_permission.py:25 xpack/plugins/cloud/models.py:325
msgid "Platform"
msgstr "プラットフォーム"
#: assets/models/asset/common.py:163 assets/models/domain.py:22
#: authentication/serializers/connect_token_secret.py:136
-#: perms/serializers/user_permission.py:28 xpack/plugins/cloud/models.py:331
+#: perms/serializers/user_permission.py:28 xpack/plugins/cloud/models.py:327
msgid "Domain"
msgstr "ドメイン"
#: assets/models/asset/common.py:165 assets/models/automations/base.py:18
-#: assets/models/cmd_filter.py:32 assets/models/node.py:549
+#: assets/models/cmd_filter.py:32 assets/models/node.py:553
#: perms/models/asset_permission.py:72 perms/serializers/permission.py:37
-#: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:330
+#: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:326
msgid "Node"
msgstr "ノード"
@@ -1804,7 +1858,7 @@ msgstr "証明書チェックを無視"
msgid "Proxy"
msgstr "プロキシー"
-#: assets/models/automations/base.py:22 ops/models/job.py:231
+#: assets/models/automations/base.py:22 ops/models/job.py:232
#: settings/serializers/auth/sms.py:103
msgid "Parameters"
msgstr "パラメータ"
@@ -1835,7 +1889,7 @@ msgstr "確認済みの日付"
#: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:66
#: perms/serializers/permission.py:34 users/models/group.py:25
-#: users/models/user.py:806
+#: users/models/user.py:820
msgid "User group"
msgstr "ユーザーグループ"
@@ -1867,7 +1921,7 @@ msgstr "コマンドフィルタルール"
msgid "Favorite asset"
msgstr "お気に入りのアセット"
-#: assets/models/gateway.py:34 assets/serializers/domain.py:18
+#: assets/models/gateway.py:34 assets/serializers/domain.py:19
msgid "Gateway"
msgstr "ゲートウェイ"
@@ -1885,11 +1939,11 @@ msgstr "デフォルト"
msgid "Default asset group"
msgstr "デフォルトアセットグループ"
-#: assets/models/label.py:15 rbac/const.py:6 users/models/user.py:1027
+#: assets/models/label.py:15 rbac/const.py:6 users/models/user.py:1043
msgid "System"
msgstr "システム"
-#: assets/models/label.py:19 assets/models/node.py:535
+#: assets/models/label.py:19 assets/models/node.py:539
#: assets/serializers/cagegory.py:11 assets/serializers/cagegory.py:18
#: assets/serializers/cagegory.py:24
#: authentication/models/connection_token.py:29
@@ -1908,27 +1962,27 @@ msgstr "値"
msgid "Label"
msgstr "ラベル"
-#: assets/models/node.py:165
+#: assets/models/node.py:169
msgid "New node"
msgstr "新しいノード"
-#: assets/models/node.py:463 audits/backends/db.py:65 audits/backends/db.py:66
+#: assets/models/node.py:467 audits/backends/db.py:65 audits/backends/db.py:66
msgid "empty"
msgstr "空"
-#: assets/models/node.py:534 perms/models/perm_node.py:28
+#: assets/models/node.py:538 perms/models/perm_node.py:28
msgid "Key"
msgstr "キー"
-#: assets/models/node.py:536 assets/serializers/node.py:20
+#: assets/models/node.py:540 assets/serializers/node.py:20
msgid "Full value"
msgstr "フルバリュー"
-#: assets/models/node.py:540 perms/models/perm_node.py:30
+#: assets/models/node.py:544 perms/models/perm_node.py:30
msgid "Parent key"
msgstr "親キー"
-#: assets/models/node.py:552
+#: assets/models/node.py:556
msgid "Can match node"
msgstr "ノードを一致させることができます"
@@ -1945,7 +1999,7 @@ msgid "Public"
msgstr "開ける"
#: assets/models/platform.py:22 assets/serializers/platform.py:49
-#: settings/serializers/settings.py:66
+#: settings/serializers/settings.py:95
#: users/templates/users/reset_password.html:29
msgid "Setting"
msgstr "設定"
@@ -2034,7 +2088,7 @@ msgstr "アカウントの削除方法"
msgid "Remove account params"
msgstr "アカウント削除パラメータ"
-#: assets/models/platform.py:98 tickets/models/ticket/general.py:300
+#: assets/models/platform.py:98 tickets/models/ticket/general.py:298
msgid "Meta"
msgstr "メタ"
@@ -2079,7 +2133,7 @@ msgstr ""
#: authentication/serializers/connect_token_secret.py:30
#: authentication/serializers/connect_token_secret.py:75
#: perms/models/asset_permission.py:76 perms/serializers/permission.py:42
-#: perms/serializers/user_permission.py:74 xpack/plugins/cloud/models.py:332
+#: perms/serializers/user_permission.py:74 xpack/plugins/cloud/models.py:328
#: xpack/plugins/cloud/serializers/task.py:33
msgid "Protocols"
msgstr "プロトコル"
@@ -2139,7 +2193,7 @@ msgid "Model"
msgstr "モデル"
#: assets/serializers/asset/info/gathered.py:8
-#: tickets/models/ticket/general.py:299
+#: tickets/models/ticket/general.py:297
msgid "Serial number"
msgstr "シリアル番号"
@@ -2188,7 +2242,7 @@ msgstr "制約"
msgid "Types"
msgstr "タイプ"
-#: assets/serializers/domain.py:53 perms/serializers/permission.py:188
+#: assets/serializers/domain.py:62 perms/serializers/permission.py:188
msgid "Assets amount"
msgstr "資産数量"
@@ -2397,7 +2451,7 @@ msgstr "接続"
#: audits/const.py:30 authentication/templates/authentication/login.html:296
#: authentication/templates/authentication/login.html:369
-#: templates/_header_bar.html:95
+#: templates/_header_bar.html:101
msgid "Login"
msgstr "ログイン"
@@ -2405,21 +2459,21 @@ msgstr "ログイン"
msgid "Change password"
msgstr "パスワードを変更する"
-#: audits/const.py:37 tickets/const.py:47
+#: audits/const.py:37 tickets/const.py:45
msgid "Approve"
msgstr "承認"
#: audits/const.py:38
#: authentication/templates/authentication/_access_key_modal.html:155
#: authentication/templates/authentication/_mfa_confirm_modal.html:53
-#: templates/_modal.html:22 tickets/const.py:45
+#: templates/_modal.html:22 tickets/const.py:43
msgid "Close"
msgstr "閉じる"
#: audits/const.py:43 settings/serializers/terminal.py:6
#: terminal/models/applet/host.py:26 terminal/models/component/terminal.py:175
-#: terminal/models/virtualapp/provider.py:14 terminal/serializers/session.py:54
-#: terminal/serializers/session.py:68
+#: terminal/models/virtualapp/provider.py:14 terminal/serializers/session.py:55
+#: terminal/serializers/session.py:69
msgid "Terminal"
msgstr "ターミナル"
@@ -2444,11 +2498,11 @@ msgstr "タスク"
msgid "-"
msgstr "-"
-#: audits/handler.py:117
+#: audits/handler.py:116
msgid "Yes"
msgstr "是"
-#: audits/handler.py:117
+#: audits/handler.py:116
msgid "No"
msgstr "否"
@@ -2457,7 +2511,7 @@ msgid "Job audit log"
msgstr "ジョブ監査ログ"
#: audits/models.py:56 audits/models.py:100 audits/models.py:175
-#: terminal/models/session/session.py:38 terminal/models/session/sharing.py:113
+#: terminal/models/session/session.py:39 terminal/models/session/sharing.py:113
msgid "Remote addr"
msgstr "リモートaddr"
@@ -2535,7 +2589,7 @@ msgstr "ログインIP"
#: audits/models.py:200 audits/serializers.py:52
#: authentication/templates/authentication/_mfa_confirm_modal.html:14
-#: users/forms/profile.py:65 users/models/user.py:823
+#: users/forms/profile.py:63 users/models/user.py:837
#: users/serializers/profile.py:102
msgid "MFA"
msgstr "MFA"
@@ -2557,17 +2611,18 @@ msgstr "ユーザーログインログ"
msgid "Session key"
msgstr "セッションID"
-#: audits/models.py:305
+#: audits/models.py:298
msgid "User session"
msgstr "ユーザーセッション"
-#: audits/models.py:307
+#: audits/models.py:300
msgid "Offline user session"
msgstr "オフラインユーザセッション"
#: audits/serializers.py:33 ops/models/adhoc.py:25 ops/models/base.py:16
-#: ops/models/base.py:53 ops/models/job.py:146 ops/models/job.py:234
-#: ops/models/playbook.py:30 terminal/models/session/sharing.py:25
+#: ops/models/base.py:53 ops/models/celery.py:86 ops/models/job.py:147
+#: ops/models/job.py:235 ops/models/playbook.py:30
+#: terminal/models/session/sharing.py:25
msgid "Creator"
msgstr "作成者"
@@ -2583,7 +2638,7 @@ msgstr "ユーザー %s %s が現在のリソースをサブスクライブし
#: audits/serializers.py:172 authentication/models/connection_token.py:47
#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:80
#: tickets/models/ticket/apply_application.py:31
-#: tickets/models/ticket/apply_asset.py:20 users/models/user.py:841
+#: tickets/models/ticket/apply_asset.py:20 users/models/user.py:855
msgid "Date expired"
msgstr "期限切れの日付"
@@ -2616,48 +2671,47 @@ msgstr "認証トークン"
#: audits/signal_handlers/login_log.py:37 authentication/notifications.py:73
#: authentication/views/login.py:77 notifications/backends/__init__.py:11
-#: settings/serializers/auth/wecom.py:10 users/models/user.py:749
-#: users/models/user.py:855
+#: settings/serializers/auth/wecom.py:10 users/models/user.py:759
+#: users/models/user.py:869
msgid "WeCom"
msgstr "企業微信"
-#: audits/signal_handlers/login_log.py:38 authentication/views/feishu.py:87
+#: audits/signal_handlers/login_log.py:38 authentication/views/feishu.py:105
#: authentication/views/login.py:89 notifications/backends/__init__.py:14
-#: settings/serializers/auth/feishu.py:10
-#: settings/serializers/auth/feishu.py:13 users/models/user.py:751
-#: users/models/user.py:857
+#: settings/serializers/auth/feishu.py:10 users/models/user.py:761
+#: users/models/user.py:871
msgid "FeiShu"
msgstr "本を飛ばす"
-#: audits/signal_handlers/login_log.py:39 authentication/views/login.py:95
-#: authentication/views/slack.py:87 notifications/backends/__init__.py:15
-#: settings/serializers/auth/slack.py:10 users/models/user.py:752
-#: users/models/user.py:858
+#: audits/signal_handlers/login_log.py:40 authentication/views/login.py:101
+#: authentication/views/slack.py:87 notifications/backends/__init__.py:16
+#: settings/serializers/auth/slack.py:10 users/models/user.py:763
+#: users/models/user.py:873
msgid "Slack"
msgstr ""
-#: audits/signal_handlers/login_log.py:40 authentication/views/dingtalk.py:161
+#: audits/signal_handlers/login_log.py:41 authentication/views/dingtalk.py:161
#: authentication/views/login.py:83 notifications/backends/__init__.py:12
-#: settings/serializers/auth/dingtalk.py:10 users/models/user.py:750
-#: users/models/user.py:856
+#: settings/serializers/auth/dingtalk.py:10 users/models/user.py:760
+#: users/models/user.py:870
msgid "DingTalk"
msgstr "DingTalk"
-#: audits/signal_handlers/login_log.py:41
+#: audits/signal_handlers/login_log.py:42
#: authentication/models/temp_token.py:16
msgid "Temporary token"
msgstr "仮パスワード"
-#: audits/signal_handlers/login_log.py:42 authentication/views/login.py:101
+#: audits/signal_handlers/login_log.py:43 authentication/views/login.py:107
#: settings/serializers/auth/passkey.py:8
msgid "Passkey"
msgstr "Passkey"
-#: audits/tasks.py:109
+#: audits/tasks.py:117
msgid "Clean audits session task log"
msgstr "資産監査セッションタスクログのクリーンアップ"
-#: audits/tasks.py:123
+#: audits/tasks.py:130
msgid "Upload FTP file to external storage"
msgstr "外部ストレージへのFTPファイルのアップロード"
@@ -2674,29 +2728,29 @@ msgstr "パラメータの値には必ず %s が含まれます"
msgid "This action require verify your MFA"
msgstr "この操作には、MFAを検証する必要があります"
-#: authentication/api/connection_token.py:260
+#: authentication/api/connection_token.py:265
msgid "Reusable connection token is not allowed, global setting not enabled"
msgstr ""
"再使用可能な接続トークンの使用は許可されていません。グローバル設定は有効に"
"なっていません"
-#: authentication/api/connection_token.py:374
+#: authentication/api/connection_token.py:379
msgid "Anonymous account is not supported for this asset"
msgstr "匿名アカウントはこのプロパティではサポートされていません"
-#: authentication/api/connection_token.py:393
+#: authentication/api/connection_token.py:399
msgid "Account not found"
msgstr "アカウントが見つかりません"
-#: authentication/api/connection_token.py:396
+#: authentication/api/connection_token.py:402
msgid "Permission expired"
msgstr "承認の有効期限が切れています"
-#: authentication/api/connection_token.py:426
+#: authentication/api/connection_token.py:435
msgid "ACL action is reject: {}({})"
msgstr "ACL アクションは拒否です: {}({})"
-#: authentication/api/connection_token.py:430
+#: authentication/api/connection_token.py:439
msgid "ACL action is review"
msgstr "ACL アクションはレビューです"
@@ -2709,7 +2763,7 @@ msgstr "現在のユーザーはmfaタイプをサポートしていません: {
msgid "User does not exist: {}"
msgstr "ユーザーが存在しない: {}"
-#: authentication/api/password.py:33 users/views/profile/reset.py:164
+#: authentication/api/password.py:33 users/views/profile/reset.py:166
msgid "No user matched"
msgstr "ユーザーにマッチしなかった"
@@ -2899,19 +2953,19 @@ msgstr "受け入れのためのログイン確認チケットを待つ"
msgid "Login confirm ticket was {}"
msgstr "ログイン確認チケットは {} でした"
-#: authentication/errors/failed.py:145
+#: authentication/errors/failed.py:149
msgid "Current IP and Time period is not allowed"
msgstr "現在の IP と期間はログインを許可されていません"
-#: authentication/errors/failed.py:150
+#: authentication/errors/failed.py:154
msgid "Please enter MFA code"
msgstr "MFAコードを入力してください"
-#: authentication/errors/failed.py:155
+#: authentication/errors/failed.py:159
msgid "Please enter SMS code"
msgstr "SMSコードを入力してください"
-#: authentication/errors/failed.py:160 users/exceptions.py:15
+#: authentication/errors/failed.py:164 users/exceptions.py:15
msgid "Phone not set"
msgstr "電話が設定されていない"
@@ -2933,19 +2987,23 @@ msgstr "企業の微信をバインドしていません"
msgid "DingTalk is not bound"
msgstr "DingTalkはバインドされていません"
-#: authentication/errors/mfa.py:33 authentication/views/feishu.py:128
+#: authentication/errors/mfa.py:33 authentication/views/feishu.py:138
msgid "FeiShu is not bound"
msgstr "本を飛ばすは拘束されていません"
-#: authentication/errors/mfa.py:38 authentication/views/slack.py:127
-msgid "Slack is not bound"
-msgstr "Slackはバインドされていません"
+#: authentication/errors/mfa.py:38 authentication/views/lark.py:48
+msgid "Lark is not bound"
+msgstr "Lark はバインドされていません"
-#: authentication/errors/mfa.py:43
+#: authentication/errors/mfa.py:43 authentication/views/slack.py:127
+msgid "Slack is not bound"
+msgstr "Slack はバインドされていません"
+
+#: authentication/errors/mfa.py:48
msgid "Your password is invalid"
msgstr "パスワードが無効です"
-#: authentication/errors/mfa.py:48
+#: authentication/errors/mfa.py:53
#, python-format
msgid "Please wait for %s seconds before retry"
msgstr "%s 秒後に再試行してください"
@@ -2963,28 +3021,28 @@ msgid "Your password has expired, please reset before logging in"
msgstr ""
"パスワードの有効期限が切れました。ログインする前にリセットしてください。"
-#: authentication/forms.py:45
-msgid "{} days auto login"
-msgstr "{} 日自動ログイン"
+#: authentication/forms.py:39
+msgid "Auto login"
+msgstr "自動ログイン"
-#: authentication/forms.py:56
+#: authentication/forms.py:52
msgid "MFA Code"
msgstr "MFAコード"
-#: authentication/forms.py:57
+#: authentication/forms.py:53
msgid "MFA type"
msgstr "MFAタイプ"
-#: authentication/forms.py:65
+#: authentication/forms.py:61
#: authentication/templates/authentication/_captcha_field.html:15
msgid "Captcha"
msgstr "キャプチャ"
-#: authentication/forms.py:70 users/forms/profile.py:28
+#: authentication/forms.py:66 users/forms/profile.py:27
msgid "MFA code"
msgstr "MFAコード"
-#: authentication/forms.py:72
+#: authentication/forms.py:68
msgid "Dynamic code"
msgstr "動的コード"
@@ -3039,8 +3097,8 @@ msgstr "メッセージ検証コードが無効"
#: authentication/mfa/sms.py:12 authentication/serializers/password_mfa.py:16
#: authentication/serializers/password_mfa.py:24
-#: settings/serializers/auth/sms.py:32 users/forms/profile.py:104
-#: users/forms/profile.py:109 users/templates/users/forgot_password.html:155
+#: settings/serializers/auth/sms.py:32 users/forms/profile.py:103
+#: users/forms/profile.py:108 users/templates/users/forgot_password.html:157
#: users/views/profile/reset.py:100
msgid "SMS"
msgstr "メッセージ"
@@ -3137,27 +3195,27 @@ msgstr "接続トークンを再利用できます"
msgid "Connection token"
msgstr "接続トークン"
-#: authentication/models/connection_token.py:115
+#: authentication/models/connection_token.py:118
msgid "Connection token inactive"
msgstr "接続トークンがアクティブ化されていません"
-#: authentication/models/connection_token.py:119
+#: authentication/models/connection_token.py:122
msgid "Connection token expired at: {}"
msgstr "接続トークンの有効期限: {}"
-#: authentication/models/connection_token.py:122
+#: authentication/models/connection_token.py:125
msgid "No user or invalid user"
msgstr "ユーザーなしまたは期限切れのユーザー"
-#: authentication/models/connection_token.py:125
+#: authentication/models/connection_token.py:128
msgid "No asset or inactive asset"
msgstr "アセットがないか、有効化されていないアセット"
-#: authentication/models/connection_token.py:269
+#: authentication/models/connection_token.py:272
msgid "Can view super connection token secret"
msgstr "スーパー接続トークンのシークレットを表示できます"
-#: authentication/models/connection_token.py:271
+#: authentication/models/connection_token.py:274
msgid "Super connection token"
msgstr "スーパー接続トークン"
@@ -3234,12 +3292,12 @@ msgstr "アクション"
#: authentication/serializers/connection_token.py:42
#: perms/serializers/permission.py:40 perms/serializers/permission.py:60
-#: users/serializers/user.py:97 users/serializers/user.py:173
+#: users/serializers/user.py:100 users/serializers/user.py:177
msgid "Is expired"
msgstr "期限切れです"
#: authentication/serializers/password_mfa.py:29
-#: users/templates/users/forgot_password.html:151
+#: users/templates/users/forgot_password.html:153
msgid "The {} cannot be empty"
msgstr "{} 空にしてはならない"
@@ -3248,8 +3306,8 @@ msgid "Access IP"
msgstr "Access IP"
#: authentication/serializers/token.py:92 perms/serializers/permission.py:39
-#: perms/serializers/permission.py:61 users/serializers/user.py:98
-#: users/serializers/user.py:170
+#: perms/serializers/permission.py:61 users/serializers/user.py:101
+#: users/serializers/user.py:174
msgid "Is valid"
msgstr "有効です"
@@ -3274,13 +3332,13 @@ msgid "Show"
msgstr "表示"
#: authentication/templates/authentication/_access_key_modal.html:66
-#: users/const.py:37 users/models/user.py:644 users/serializers/profile.py:92
+#: users/const.py:42 users/models/user.py:654 users/serializers/profile.py:92
#: users/templates/users/user_verify_mfa.html:36
msgid "Disable"
msgstr "無効化"
#: authentication/templates/authentication/_access_key_modal.html:67
-#: users/const.py:38 users/models/user.py:645 users/serializers/profile.py:93
+#: users/const.py:43 users/models/user.py:655 users/serializers/profile.py:93
#: users/templates/users/mfa_setting.html:26
#: users/templates/users/mfa_setting.html:68
msgid "Enable"
@@ -3319,7 +3377,7 @@ msgstr "コードエラー"
#: authentication/templates/authentication/_msg_reset_password_code.html:9
#: authentication/templates/authentication/_msg_rest_password_success.html:2
#: authentication/templates/authentication/_msg_rest_public_key_success.html:2
-#: jumpserver/conf.py:460
+#: jumpserver/conf.py:465
#: perms/templates/perms/_msg_item_permissions_expire.html:3
#: perms/templates/perms/_msg_permed_items_expire.html:3
#: tickets/templates/tickets/approve_check_password.html:32
@@ -3379,7 +3437,7 @@ msgstr "新しいものを要求する"
#: authentication/templates/authentication/_msg_reset_password_code.html:12
#: terminal/models/session/sharing.py:27 terminal/models/session/sharing.py:97
#: terminal/templates/terminal/_msg_session_sharing.html:12
-#: users/forms/profile.py:107 users/templates/users/forgot_password.html:97
+#: users/forms/profile.py:106 users/templates/users/forgot_password.html:98
msgid "Verify code"
msgstr "コードの確認"
@@ -3492,7 +3550,7 @@ msgid "Do you want to retry ?"
msgstr "再試行しますか?"
#: authentication/utils.py:24 common/utils/ip/geoip/utils.py:24
-#: xpack/plugins/cloud/const.py:32
+#: xpack/plugins/cloud/const.py:33
msgid "LAN"
msgstr "ローカルエリアネットワーク"
@@ -3501,17 +3559,17 @@ msgstr "ローカルエリアネットワーク"
msgid "If you have any question, please contact the administrator"
msgstr "質問があったら、管理者に連絡して下さい"
-#: authentication/views/base.py:138
+#: authentication/views/base.py:146
#, python-format
msgid "%s query user failed"
msgstr "%sユーザーのクエリに失敗しました"
-#: authentication/views/base.py:147
+#: authentication/views/base.py:155
#, python-format
msgid "The %s is already bound to another user"
msgstr "%sが別のユーザーにバインドされています。"
-#: authentication/views/base.py:154
+#: authentication/views/base.py:162
#, python-format
msgid "Binding %s successfully"
msgstr "バインド%s成功"
@@ -3524,7 +3582,7 @@ msgstr "DingTalkエラー、システム管理者に連絡してください"
msgid "DingTalk Error"
msgstr "DingTalkエラー"
-#: authentication/views/dingtalk.py:57 authentication/views/feishu.py:47
+#: authentication/views/dingtalk.py:57 authentication/views/feishu.py:68
#: authentication/views/slack.py:47 authentication/views/wecom.py:55
msgid ""
"The system configuration is incorrect. Please contact your administrator"
@@ -3558,35 +3616,47 @@ msgstr "DingTalkからユーザーを取得できませんでした"
msgid "Please login with a password and then bind the DingTalk"
msgstr "パスワードでログインし、DingTalkをバインドしてください"
-#: authentication/views/feishu.py:35 authentication/views/feishu.py:127
+#: authentication/views/feishu.py:43 authentication/views/feishu.py:137
msgid "FeiShu Error"
msgstr "FeiShuエラー"
-#: authentication/views/feishu.py:63
+#: authentication/views/feishu.py:44
msgid "FeiShu is already bound"
msgstr "FeiShuはすでにバインドされています"
-#: authentication/views/feishu.py:129
+#: authentication/views/feishu.py:139
msgid "Failed to get user from FeiShu"
msgstr "本を飛ばすからユーザーを取得できませんでした"
-#: authentication/views/login.py:217
+#: authentication/views/lark.py:19 authentication/views/lark.py:47
+msgid "Lark Error"
+msgstr "Lark エラー"
+
+#: authentication/views/lark.py:20
+msgid "Lark is already bound"
+msgstr "Lark はすでにバインドされています"
+
+#: authentication/views/lark.py:49
+msgid "Failed to get user from Lark"
+msgstr "Lark からユーザーを取得できませんでした"
+
+#: authentication/views/login.py:227
msgid "Redirecting"
msgstr "リダイレクト"
-#: authentication/views/login.py:218
+#: authentication/views/login.py:228
msgid "Redirecting to {} authentication"
msgstr "{} 認証へのリダイレクト"
-#: authentication/views/login.py:241
+#: authentication/views/login.py:251
msgid "Login timeout, please try again."
msgstr "ログインタイムアウト、もう一度お試しください"
-#: authentication/views/login.py:284
+#: authentication/views/login.py:294
msgid "User email already exists ({})"
msgstr "ユーザー メールボックスは既に存在します ({})"
-#: authentication/views/login.py:362
+#: authentication/views/login.py:372
msgid ""
"Wait for {} confirm, You also can copy link to her/him
\n"
" Don't close this page"
@@ -3594,15 +3664,15 @@ msgstr ""
"{} 確認を待ちます。彼女/彼へのリンクをコピーすることもできます
\n"
" このページを閉じないでください"
-#: authentication/views/login.py:367
+#: authentication/views/login.py:377
msgid "No ticket found"
msgstr "チケットが見つかりません"
-#: authentication/views/login.py:403
+#: authentication/views/login.py:413
msgid "Logout success"
msgstr "ログアウト成功"
-#: authentication/views/login.py:404
+#: authentication/views/login.py:414
msgid "Logout success, return login page"
msgstr "ログアウト成功、ログインページを返す"
@@ -3654,12 +3724,7 @@ msgstr "タイミングトリガー"
msgid "Ready"
msgstr "の準備を"
-#: common/const/choices.py:16 terminal/const.py:77 tickets/const.py:30
-#: tickets/const.py:40
-msgid "Pending"
-msgstr "未定"
-
-#: common/const/choices.py:17 ops/const.py:72
+#: common/const/choices.py:17 ops/const.py:73
msgid "Running"
msgstr "ランニング"
@@ -3709,7 +3774,7 @@ msgstr "テキストフィールドへのマーシャルデータ"
msgid "Encrypt field using Secret Key"
msgstr "Secret Keyを使用したフィールドの暗号化"
-#: common/db/fields.py:580
+#: common/db/fields.py:582
msgid ""
"Invalid JSON data for JSONManyToManyField, should be like {'type': 'all'} or "
"{'type': 'ids', 'ids': []} or {'type': 'attrs', 'attrs': [{'name': 'ip', "
@@ -3719,15 +3784,15 @@ msgstr ""
"{'type':'ids','ids':[]}或 #タイプ:属性、属性:[#名前:ip、照合:正確、"
"値:1.1.1.1}"
-#: common/db/fields.py:587
+#: common/db/fields.py:589
msgid "Invalid type, should be \"all\", \"ids\" or \"attrs\""
msgstr "無効なタイプです。all、ids、またはattrsでなければなりません"
-#: common/db/fields.py:590
+#: common/db/fields.py:592
msgid "Invalid ids for ids, should be a list"
msgstr "無効なID、リストでなければなりません"
-#: common/db/fields.py:592 common/db/fields.py:597
+#: common/db/fields.py:594 common/db/fields.py:599
#: common/serializers/fields.py:133 tickets/serializers/ticket/common.py:58
#: xpack/plugins/cloud/serializers/account_attrs.py:56
#: xpack/plugins/cloud/serializers/account_attrs.py:79
@@ -3735,11 +3800,11 @@ msgstr "無効なID、リストでなければなりません"
msgid "This field is required."
msgstr "このフィールドは必須です。"
-#: common/db/fields.py:595 common/db/fields.py:600
+#: common/db/fields.py:597 common/db/fields.py:602
msgid "Invalid attrs, should be a list of dict"
msgstr "無効な属性、dictリストでなければなりません"
-#: common/db/fields.py:602
+#: common/db/fields.py:604
msgid "Invalid attrs, should be has name and value"
msgstr "名前と値が必要な無効な属性"
@@ -3751,7 +3816,7 @@ msgstr "は破棄されます"
msgid "discard time"
msgstr "時間を捨てる"
-#: common/db/models.py:33 users/models/user.py:844
+#: common/db/models.py:33 users/models/user.py:858
msgid "Updated by"
msgstr "によって更新"
@@ -3779,7 +3844,7 @@ msgstr "解析ファイルエラー: {}"
msgid "Invalid excel file"
msgstr "無効 excel 書類"
-#: common/drf/renders/base.py:207
+#: common/drf/renders/base.py:208
msgid ""
"{} - The encryption password has not been set - please go to personal "
"information -> file encryption password to set the encryption password"
@@ -3931,7 +3996,7 @@ msgstr "メールを送る"
msgid "Send email attachment"
msgstr "メールの添付ファイルを送信"
-#: common/tasks.py:80 terminal/tasks.py:62
+#: common/tasks.py:80 terminal/tasks.py:58
msgid "Upload session replay to external storage"
msgstr "セッションの記録を外部ストレージにアップロードする"
@@ -3960,20 +4025,20 @@ msgstr "特殊文字を含むべきではない"
msgid "The mobile phone number format is incorrect"
msgstr "携帯電話番号の形式が正しくありません"
-#: jumpserver/conf.py:454
+#: jumpserver/conf.py:459
#, python-brace-format
msgid "The verification code is: {code}"
msgstr "認証コードは: {code}"
-#: jumpserver/conf.py:459
+#: jumpserver/conf.py:464
msgid "Create account successfully"
msgstr "アカウントを正常に作成"
-#: jumpserver/conf.py:461
+#: jumpserver/conf.py:466
msgid "Your account has been created successfully"
msgstr "アカウントが正常に作成されました"
-#: jumpserver/context_processor.py:12
+#: jumpserver/context_processor.py:14
msgid "JumpServer Open Source Bastion Host"
msgstr "JumpServer オープンソースの要塞ホスト"
@@ -4053,15 +4118,15 @@ msgstr "システムメッセージ"
msgid "Publish the station message"
msgstr "投稿サイトニュース"
-#: ops/ansible/inventory.py:97 ops/models/job.py:62
+#: ops/ansible/inventory.py:106 ops/models/job.py:63
msgid "No account available"
msgstr "利用可能なアカウントがありません"
-#: ops/ansible/inventory.py:264
+#: ops/ansible/inventory.py:280
msgid "Ansible disabled"
msgstr "Ansible 無効"
-#: ops/ansible/inventory.py:280
+#: ops/ansible/inventory.py:296
msgid "Skip hosts below:"
msgstr "次のホストをスキップします: "
@@ -4069,19 +4134,39 @@ msgstr "次のホストをスキップします: "
msgid "Waiting task start"
msgstr "タスク開始待ち"
-#: ops/api/celery.py:246
+#: ops/api/celery.py:262
msgid "Task {} not found"
msgstr "タスクは存在しません"
-#: ops/api/celery.py:251
+#: ops/api/celery.py:267
msgid "Task {} args or kwargs error"
msgstr "タスク実行パラメータエラー"
-#: ops/api/job.py:135
+#: ops/api/job.py:81
+#, python-brace-format
+msgid ""
+"Asset ({asset}) must have at least one of the following protocols added: "
+"SSH, SFTP, or WinRM"
+msgstr ""
+"資産({asset})には、少なくともSSH、SFTP、WinRMのいずれか一つのプロトコルを追加"
+"する必要があります"
+
+#: ops/api/job.py:82
+#, python-brace-format
+msgid "Asset ({asset}) authorization is missing SSH, SFTP, or WinRM protocol"
+msgstr ""
+"資産({asset})の認証にはSSH、SFTP、またはWinRMプロトコルが不足しています"
+
+#: ops/api/job.py:83
+#, python-brace-format
+msgid "Asset ({asset}) authorization lacks upload permissions"
+msgstr "資産({asset})の認証にはアップロード権限が不足しています"
+
+#: ops/api/job.py:168
msgid "Duplicate file exists"
msgstr "重複したファイルが存在する"
-#: ops/api/job.py:140
+#: ops/api/job.py:173
#, python-brace-format
msgid ""
"File size exceeds maximum limit. Please select a file smaller than {limit}MB"
@@ -4089,7 +4174,7 @@ msgstr ""
"ファイルサイズが最大制限を超えています。{limit}MB より小さいファイルを選択し"
"てください。"
-#: ops/api/job.py:204
+#: ops/api/job.py:237
msgid ""
"The task is being created and cannot be interrupted. Please try again later."
msgstr "タスクを作成中で、中断できません。後でもう一度お試しください。"
@@ -4098,31 +4183,31 @@ msgstr "タスクを作成中で、中断できません。後でもう一度お
msgid "Currently playbook is being used in a job"
msgstr "現在プレイブックは1つのジョブで使用されています"
-#: ops/api/playbook.py:93
+#: ops/api/playbook.py:96
msgid "Unsupported file content"
msgstr "サポートされていないファイルの内容"
-#: ops/api/playbook.py:95 ops/api/playbook.py:141 ops/api/playbook.py:189
+#: ops/api/playbook.py:98 ops/api/playbook.py:144 ops/api/playbook.py:192
msgid "Invalid file path"
msgstr "無効なファイルパス"
-#: ops/api/playbook.py:167
+#: ops/api/playbook.py:170
msgid "This file can not be rename"
msgstr "ファイル名を変更することはできません"
-#: ops/api/playbook.py:186
+#: ops/api/playbook.py:189
msgid "File already exists"
msgstr "ファイルは既に存在します。"
-#: ops/api/playbook.py:204
+#: ops/api/playbook.py:207
msgid "File key is required"
msgstr "ファイルキーこのフィールドは必須です"
-#: ops/api/playbook.py:207
+#: ops/api/playbook.py:210
msgid "This file can not be delete"
msgstr "このファイルを削除できません"
-#: ops/apps.py:9 ops/notifications.py:17 rbac/tree.py:57
+#: ops/apps.py:9 ops/notifications.py:18 rbac/tree.py:57
msgid "App ops"
msgstr "アプリ操作"
@@ -4162,7 +4247,7 @@ msgstr "VCS"
msgid "Adhoc"
msgstr "コマンド#コマンド#"
-#: ops/const.py:39 ops/models/job.py:144
+#: ops/const.py:39 ops/models/job.py:145
msgid "Playbook"
msgstr "Playbook"
@@ -4178,39 +4263,43 @@ msgstr "特権アカウントのみ"
msgid "Privileged First"
msgstr "特権アカウント優先"
-#: ops/const.py:51 ops/const.py:61
+#: ops/const.py:51 ops/const.py:62
msgid "Powershell"
msgstr "PowerShell"
-#: ops/const.py:52 ops/const.py:62
+#: ops/const.py:52 ops/const.py:63
msgid "Python"
msgstr "Python"
-#: ops/const.py:53 ops/const.py:63
+#: ops/const.py:53 ops/const.py:64
msgid "MySQL"
msgstr "MySQL"
-#: ops/const.py:54 ops/const.py:65
+#: ops/const.py:54 ops/const.py:66
msgid "PostgreSQL"
msgstr "PostgreSQL"
-#: ops/const.py:55 ops/const.py:66
+#: ops/const.py:55 ops/const.py:67
msgid "SQLServer"
msgstr "SQLServer"
-#: ops/const.py:56 ops/const.py:68
+#: ops/const.py:56 ops/const.py:69
msgid "Raw"
msgstr ""
-#: ops/const.py:64
+#: ops/const.py:57
+msgid "HUAWEI"
+msgstr ""
+
+#: ops/const.py:65
msgid "MariaDB"
msgstr "MariaDB"
-#: ops/const.py:67
+#: ops/const.py:68
msgid "Oracle"
msgstr "Oracle"
-#: ops/const.py:74
+#: ops/const.py:75
msgid "Timeout"
msgstr "タイムアウト"
@@ -4247,11 +4336,11 @@ msgstr "定期的または定期的に設定を行う必要があります"
msgid "Pattern"
msgstr "パターン"
-#: ops/models/adhoc.py:23 ops/models/job.py:141
+#: ops/models/adhoc.py:23 ops/models/job.py:142
msgid "Module"
msgstr "モジュール"
-#: ops/models/adhoc.py:24 ops/models/celery.py:81 ops/models/job.py:139
+#: ops/models/adhoc.py:24 ops/models/celery.py:81 ops/models/job.py:140
#: terminal/models/component/task.py:14
msgid "Args"
msgstr "アルグ"
@@ -4270,12 +4359,12 @@ msgstr "最後の実行"
msgid "Date last run"
msgstr "最終実行日"
-#: ops/models/base.py:51 ops/models/job.py:232
-#: xpack/plugins/cloud/models.py:202
+#: ops/models/base.py:51 ops/models/job.py:233
+#: xpack/plugins/cloud/models.py:198
msgid "Result"
msgstr "結果"
-#: ops/models/base.py:52 ops/models/job.py:233
+#: ops/models/base.py:52 ops/models/job.py:234
msgid "Summary"
msgstr "概要"
@@ -4296,55 +4385,55 @@ msgid "Kwargs"
msgstr "クワーグ"
#: ops/models/celery.py:84 terminal/models/session/sharing.py:128
-#: tickets/const.py:26
+#: tickets/const.py:25
msgid "Finished"
msgstr "終了"
-#: ops/models/celery.py:85
+#: ops/models/celery.py:87
msgid "Date published"
msgstr "発売日"
-#: ops/models/celery.py:110
+#: ops/models/celery.py:112
msgid "Celery Task Execution"
msgstr "Celery タスク実行"
-#: ops/models/job.py:142
+#: ops/models/job.py:143
msgid "Chdir"
msgstr "実行ディレクトリ"
-#: ops/models/job.py:143
+#: ops/models/job.py:144
msgid "Timeout (Seconds)"
msgstr "タイムアウト(秒)"
-#: ops/models/job.py:148
+#: ops/models/job.py:149
msgid "Use Parameter Define"
msgstr "パラメータ定義を使用する"
-#: ops/models/job.py:149
+#: ops/models/job.py:150
msgid "Parameters define"
msgstr "パラメータ定義"
-#: ops/models/job.py:150
+#: ops/models/job.py:151
msgid "Runas"
msgstr "ユーザーとして実行"
-#: ops/models/job.py:152
+#: ops/models/job.py:153
msgid "Runas policy"
msgstr "ユーザー ポリシー"
-#: ops/models/job.py:216
+#: ops/models/job.py:217
msgid "Job"
msgstr "ジョブ#ジョブ#"
-#: ops/models/job.py:239
+#: ops/models/job.py:240
msgid "Material"
msgstr "Material"
-#: ops/models/job.py:241
+#: ops/models/job.py:242
msgid "Material Type"
msgstr "Material を選択してオプションを設定します。"
-#: ops/models/job.py:567
+#: ops/models/job.py:539
msgid "Job Execution"
msgstr "ジョブ実行"
@@ -4356,30 +4445,30 @@ msgstr "创建方式"
msgid "VCS URL"
msgstr "VCS URL"
-#: ops/notifications.py:18
+#: ops/notifications.py:19
msgid "Server performance"
msgstr "サーバーのパフォーマンス"
-#: ops/notifications.py:24
+#: ops/notifications.py:25
msgid "Terminal health check warning"
msgstr "ターミナルヘルスチェックの警告"
-#: ops/notifications.py:69
+#: ops/notifications.py:70
#, python-brace-format
msgid "The terminal is offline: {name}"
msgstr "ターミナルはオフラインです: {name}"
-#: ops/notifications.py:74
+#: ops/notifications.py:75
#, python-brace-format
msgid "Disk used more than {max_threshold}%: => {value}"
msgstr "{max_threshold}%: => {value} を超えるディスクを使用"
-#: ops/notifications.py:79
+#: ops/notifications.py:80
#, python-brace-format
msgid "Memory used more than {max_threshold}%: => {value}"
msgstr "{max_threshold}%: => {value} を超える使用メモリ"
-#: ops/notifications.py:84
+#: ops/notifications.py:85
#, python-brace-format
msgid "CPU load more than {max_threshold}: => {value}"
msgstr "{max_threshold} を超えるCPUロード: => {value}"
@@ -4392,7 +4481,7 @@ msgstr "保存後に実行"
msgid "Job type"
msgstr "タスクの種類"
-#: ops/serializers/job.py:72 terminal/serializers/session.py:55
+#: ops/serializers/job.py:72 terminal/serializers/session.py:56
msgid "Is finished"
msgstr "終了しました"
@@ -4401,31 +4490,35 @@ msgstr "終了しました"
msgid "Time cost"
msgstr "時を過ごす"
-#: ops/tasks.py:37
+#: ops/serializers/job.py:87
+msgid "You do not have permission for the current job."
+msgstr "あなたは現在のジョブの権限を持っていません。"
+
+#: ops/tasks.py:38
msgid "Run ansible task"
msgstr "Ansible タスクを実行する"
-#: ops/tasks.py:71
+#: ops/tasks.py:72
msgid "Run ansible task execution"
msgstr "Ansible タスクの実行を開始する"
-#: ops/tasks.py:93
+#: ops/tasks.py:94
msgid "Clear celery periodic tasks"
msgstr "タスクログを定期的にクリアする"
-#: ops/tasks.py:114
+#: ops/tasks.py:115
msgid "Create or update periodic tasks"
msgstr "定期的なタスクの作成または更新"
-#: ops/tasks.py:122
+#: ops/tasks.py:123
msgid "Periodic check service performance"
msgstr "サービスのパフォーマンスを定期的に確認する"
-#: ops/tasks.py:128
+#: ops/tasks.py:129
msgid "Clean up unexpected jobs"
msgstr "例外ジョブのクリーンアップ"
-#: ops/tasks.py:135
+#: ops/tasks.py:136
msgid "Clean job_execution db record"
msgstr "ジョブセンター実行履歴のクリーンアップ"
@@ -4433,10 +4526,6 @@ msgstr "ジョブセンター実行履歴のクリーンアップ"
msgid "Task log"
msgstr "タスクログ"
-#: ops/templates/ops/celery_task_log.html:71 terminal/serializers/task.py:10
-msgid "Task name"
-msgstr "タスク名"
-
#: ops/variables.py:24
msgid "The current user`s username of JumpServer"
msgstr "JumpServerの現在のユーザーのユーザー名"
@@ -4473,18 +4562,18 @@ msgstr "ジョブのID"
msgid "Name of the job"
msgstr "ジョブの名前"
-#: orgs/api.py:62
+#: orgs/api.py:61
msgid "The current organization ({}) cannot be deleted"
msgstr "現在の組織 ({}) は削除できません"
-#: orgs/api.py:67
+#: orgs/api.py:66
msgid ""
"LDAP synchronization is set to the current organization. Please switch to "
"another organization before deleting"
msgstr ""
"LDAP 同期は現在の組織に設定されます。削除する前に別の組織に切り替えてください"
-#: orgs/api.py:77
+#: orgs/api.py:76
msgid "The organization have resource ({}) cannot be deleted"
msgstr "組織のリソース ({}) は削除できません"
@@ -4497,7 +4586,7 @@ msgstr "アプリ組織"
#: rbac/serializers/rolebinding.py:44 settings/serializers/auth/ldap.py:63
#: terminal/templates/terminal/_msg_command_warning.html:21
#: terminal/templates/terminal/_msg_session_sharing.html:14
-#: tickets/models/ticket/general.py:302 tickets/serializers/ticket/ticket.py:60
+#: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:60
msgid "Organization"
msgstr "組織"
@@ -4676,7 +4765,7 @@ msgstr "内部の役割は、破壊することはできません"
msgid "The role has been bound to users, can't be destroy"
msgstr "ロールはユーザーにバインドされており、破壊することはできません"
-#: rbac/api/role.py:102
+#: rbac/api/role.py:105
msgid "Internal role, can't be update"
msgstr "内部ロール、更新できません"
@@ -4750,7 +4839,7 @@ msgid "Scope"
msgstr "スコープ"
#: rbac/models/role.py:46 rbac/models/rolebinding.py:52
-#: users/models/user.py:810
+#: users/models/user.py:824
msgid "Role"
msgstr "ロール"
@@ -4762,7 +4851,7 @@ msgstr "システムの役割"
msgid "Organization role"
msgstr "組織の役割"
-#: rbac/models/rolebinding.py:61
+#: rbac/models/rolebinding.py:62
msgid "Role binding"
msgstr "ロールバインディング"
@@ -4770,18 +4859,18 @@ msgstr "ロールバインディング"
msgid "All organizations"
msgstr "全ての組織"
-#: rbac/models/rolebinding.py:190
+#: rbac/models/rolebinding.py:193
msgid ""
"User last role in org, can not be delete, you can remove user from org "
"instead"
msgstr ""
"ユーザーの最後のロールは削除できません。ユーザーを組織から削除できます。"
-#: rbac/models/rolebinding.py:197
+#: rbac/models/rolebinding.py:200
msgid "Organization role binding"
msgstr "組織の役割バインディング"
-#: rbac/models/rolebinding.py:212
+#: rbac/models/rolebinding.py:215
msgid "System role binding"
msgstr "システムロールバインディング"
@@ -4861,7 +4950,7 @@ msgid "Ticket comment"
msgstr "チケットコメント"
#: rbac/tree.py:130 settings/serializers/feature.py:109
-#: tickets/models/ticket/general.py:307
+#: tickets/models/ticket/general.py:305
msgid "Ticket"
msgstr "チケット"
@@ -5015,26 +5104,30 @@ msgid "FeiShu Auth"
msgstr "飛本 認証"
#: settings/serializers/auth/base.py:20
+msgid "Lark Auth"
+msgstr "Lark 認証"
+
+#: settings/serializers/auth/base.py:21
msgid "Slack Auth"
msgstr "Slack 認証"
-#: settings/serializers/auth/base.py:21
+#: settings/serializers/auth/base.py:22
msgid "WeCom Auth"
msgstr "企業微信 認証"
-#: settings/serializers/auth/base.py:22
+#: settings/serializers/auth/base.py:23
msgid "SSO Auth"
msgstr "SSO Token 認証"
-#: settings/serializers/auth/base.py:23
+#: settings/serializers/auth/base.py:24
msgid "Passkey Auth"
msgstr "Passkey 認証"
-#: settings/serializers/auth/base.py:26
+#: settings/serializers/auth/base.py:27
msgid "Forgot password url"
msgstr "パスワードのURLを忘れた"
-#: settings/serializers/auth/base.py:29
+#: settings/serializers/auth/base.py:30
msgid "Enable login redirect msg"
msgstr "ログインリダイレクトの有効化msg"
@@ -5079,10 +5172,14 @@ msgstr "そうでない場合はユーザーを作成"
msgid "Enable DingTalk Auth"
msgstr "ピン認証の有効化"
-#: settings/serializers/auth/feishu.py:16
+#: settings/serializers/auth/feishu.py:12
msgid "Enable FeiShu Auth"
msgstr "飛本認証の有効化"
+#: settings/serializers/auth/lark.py:12
+msgid "Enable Lark Auth"
+msgstr "Lark 認証の有効化"
+
#: settings/serializers/auth/ldap.py:39
msgid "LDAP"
msgstr "LDAP"
@@ -5133,11 +5230,23 @@ msgstr ""
msgid "Connect timeout (s)"
msgstr "接続タイムアウト (秒)"
-#: settings/serializers/auth/ldap.py:79
+#: settings/serializers/auth/ldap.py:82
+msgid "User DN cache timeout (s)"
+msgstr "User DN キャッシュの有効期限 (秒)"
+
+#: settings/serializers/auth/ldap.py:84
+msgid ""
+"Caching the User DN obtained during user login authentication can "
+"effectivelyimprove the speed of user authentication., 0 means no cache"
+msgstr ""
+"ユーザーログイン認証時に取得したユーザー DN をキャッシュすることで、ユーザー"
+"認証の速度を効果的に向上させることができます"
+
+#: settings/serializers/auth/ldap.py:88
msgid "Search paged size (piece)"
msgstr "ページサイズを検索 (じょう)"
-#: settings/serializers/auth/ldap.py:84
+#: settings/serializers/auth/ldap.py:93
msgid "Enable LDAP auth"
msgstr "LDAP認証の有効化"
@@ -5588,7 +5697,7 @@ msgstr "チャットAIを起動する"
msgid "Base Url"
msgstr "基本的なUrl"
-#: settings/serializers/feature.py:81 templates/_header_bar.html:90
+#: settings/serializers/feature.py:81 templates/_header_bar.html:96
msgid "API Key"
msgstr "API Key"
@@ -6002,41 +6111,49 @@ msgstr "接続最大アイドル時間(分)"
msgid "If idle time more than it, disconnect connection."
msgstr "この設定以上の操作がない場合、接続は切断されます"
+#: settings/serializers/security.py:200
+msgid "Session expire at browser closed"
+msgstr "ブラウザを閉じるとセッションが期限切れになります"
+
#: settings/serializers/security.py:201
+msgid "Whether to expire the session when the user closes their browser."
+msgstr "ユーザーがブラウザを閉じたときにセッションを期限切れにするかどうか。"
+
+#: settings/serializers/security.py:205
msgid "Session max connection time (hour)"
msgstr "セッション最大接続時間(時間)"
-#: settings/serializers/security.py:202
+#: settings/serializers/security.py:206
msgid "If session connection time more than it, disconnect connection."
msgstr "セッション接続時間がこれを超えると、接続が切断されます"
-#: settings/serializers/security.py:205
+#: settings/serializers/security.py:209
msgid "Remember manual auth"
msgstr "手動入力パスワードの保存"
-#: settings/serializers/security.py:208
+#: settings/serializers/security.py:212
#: terminal/templates/terminal/_msg_session_sharing.html:10
msgid "Session share"
msgstr "セッション共有"
-#: settings/serializers/security.py:209
+#: settings/serializers/security.py:213
msgid "Enabled, Allows user active session to be shared with other users"
msgstr ""
"ユーザーのアクティブなセッションを他のユーザーと共有できるようにします。"
-#: settings/serializers/security.py:215
+#: settings/serializers/security.py:219
msgid "Insecure command alert"
msgstr "安全でないコマンドアラート"
-#: settings/serializers/security.py:218
+#: settings/serializers/security.py:222
msgid "Email recipient"
msgstr "メール受信者"
-#: settings/serializers/security.py:219
+#: settings/serializers/security.py:223
msgid "Multiple user using , split"
msgstr "複数のユーザーを使用して、分割"
-#: settings/serializers/settings.py:70
+#: settings/serializers/settings.py:62
#, python-format
msgid "[%s] %s"
msgstr "[%s] %s"
@@ -6110,7 +6227,7 @@ msgid "Sync task Finish"
msgstr "同期タスクが完了しました"
#: settings/templates/ldap/_msg_import_ldap_user.html:6
-#: terminal/models/session/session.py:45
+#: terminal/models/session/session.py:46
msgid "Date end"
msgstr "終了日"
@@ -6227,11 +6344,11 @@ msgstr "認証に失敗しました (不明): {}"
msgid "Authentication success: {}"
msgstr "認証成功: {}"
-#: settings/ws.py:236
+#: settings/ws.py:195
msgid "Get ldap users is None"
msgstr "Ldapユーザーを取得するにはNone"
-#: settings/ws.py:246
+#: settings/ws.py:205
msgid "Imported {} users successfully (Organization: {})"
msgstr "{} 人のユーザーを正常にインポートしました (組織: {})"
@@ -6283,19 +6400,19 @@ msgstr "ドキュメント"
msgid "Commercial support"
msgstr "商用サポート"
-#: templates/_header_bar.html:79 users/forms/profile.py:44
+#: templates/_header_bar.html:85 users/forms/profile.py:43
msgid "Profile"
msgstr "プロフィール"
-#: templates/_header_bar.html:83
+#: templates/_header_bar.html:89
msgid "Admin page"
msgstr "ページの管理"
-#: templates/_header_bar.html:86
+#: templates/_header_bar.html:92
msgid "User page"
msgstr "ユーザーページ"
-#: templates/_header_bar.html:91
+#: templates/_header_bar.html:97
msgid "Logout"
msgstr "ログアウト"
@@ -6377,13 +6494,13 @@ msgstr ""
msgid "Send verification code"
msgstr "確認コードを送信"
-#: templates/_mfa_login_field.html:106
-#: users/templates/users/forgot_password.html:174
+#: templates/_mfa_login_field.html:107
+#: users/templates/users/forgot_password.html:176
msgid "Wait: "
msgstr "待つ:"
-#: templates/_mfa_login_field.html:116
-#: users/templates/users/forgot_password.html:190
+#: templates/_mfa_login_field.html:117
+#: users/templates/users/forgot_password.html:192
msgid "The verification code has been sent"
msgstr "確認コードが送信されました"
@@ -6392,7 +6509,7 @@ msgid "Home page"
msgstr "ホームページ"
#: templates/resource_download.html:18 templates/resource_download.html:33
-#: users/const.py:60
+#: users/const.py:65
msgid "Client"
msgstr "クライアント"
@@ -6451,31 +6568,31 @@ msgstr "これはエンタープライズ版アプレットです"
msgid "Not found protocol query params"
msgstr "プロトコルクエリパラメータが見つかりません"
-#: terminal/api/component/storage.py:30
+#: terminal/api/component/storage.py:31
msgid "Deleting the default storage is not allowed"
msgstr "デフォルトのストレージの削除は許可されていません"
-#: terminal/api/component/storage.py:33
+#: terminal/api/component/storage.py:34
msgid "Cannot delete storage that is being used"
msgstr "使用中のストレージを削除できません"
-#: terminal/api/component/storage.py:74 terminal/api/component/storage.py:75
+#: terminal/api/component/storage.py:75 terminal/api/component/storage.py:76
msgid "Command storages"
msgstr "コマンドストア"
-#: terminal/api/component/storage.py:81
+#: terminal/api/component/storage.py:82
msgid "Invalid"
msgstr "無効"
-#: terminal/api/component/storage.py:129 terminal/tasks.py:142
+#: terminal/api/component/storage.py:130 terminal/tasks.py:149
msgid "Test failure: {}"
msgstr "テスト失敗: {}"
-#: terminal/api/component/storage.py:132
+#: terminal/api/component/storage.py:133
msgid "Test successful"
msgstr "テスト成功"
-#: terminal/api/component/storage.py:134
+#: terminal/api/component/storage.py:135
msgid "Test failure: Please check configuration"
msgstr "テストに失敗しました:構成を確認してください"
@@ -6545,6 +6662,10 @@ msgstr "データベース クライアント"
msgid "Remote Desktop"
msgstr "リモートデスクトップ"
+#: terminal/connect_methods.py:37
+msgid "RDP Guide"
+msgstr "RDP 接続ウィザード"
+
#: terminal/const.py:12
msgid "Review & Reject"
msgstr "レビューと拒否"
@@ -6652,7 +6773,8 @@ msgstr "同時実行可能"
msgid "Tags"
msgstr "ラベル"
-#: terminal/models/applet/applet.py:48 terminal/serializers/storage.py:197
+#: terminal/models/applet/applet.py:48 terminal/serializers/applet_host.py:167
+#: terminal/serializers/storage.py:197
msgid "Hosts"
msgstr "ホスト"
@@ -6816,7 +6938,7 @@ msgstr "ユーザーの適用"
msgid "Can view terminal config"
msgstr "ターミナル構成を表示できます"
-#: terminal/models/session/command.py:66
+#: terminal/models/session/command.py:76
msgid "Command record"
msgstr "コマンドレコード"
@@ -6832,43 +6954,43 @@ msgstr "セッションのリプレイをアップロードできます"
msgid "Can download session replay"
msgstr "セッション再生をダウンロードできます"
-#: terminal/models/session/session.py:34
+#: terminal/models/session/session.py:35
msgid "Account id"
msgstr "アカウント ID"
-#: terminal/models/session/session.py:36 terminal/models/session/sharing.py:118
+#: terminal/models/session/session.py:37 terminal/models/session/sharing.py:118
msgid "Login from"
msgstr "ログイン元"
-#: terminal/models/session/session.py:41
+#: terminal/models/session/session.py:42
msgid "Replay"
msgstr "リプレイ"
-#: terminal/models/session/session.py:47 terminal/serializers/session.py:67
+#: terminal/models/session/session.py:48 terminal/serializers/session.py:68
msgid "Command amount"
msgstr "コマンド量"
-#: terminal/models/session/session.py:48 terminal/serializers/session.py:30
+#: terminal/models/session/session.py:49 terminal/serializers/session.py:30
msgid "Error reason"
msgstr "間違った理由"
-#: terminal/models/session/session.py:282
+#: terminal/models/session/session.py:290
msgid "Session record"
msgstr "セッション記録"
-#: terminal/models/session/session.py:284
+#: terminal/models/session/session.py:292
msgid "Can monitor session"
msgstr "セッションを監視できます"
-#: terminal/models/session/session.py:285
+#: terminal/models/session/session.py:293
msgid "Can share session"
msgstr "セッションを共有できます"
-#: terminal/models/session/session.py:286
+#: terminal/models/session/session.py:294
msgid "Can terminate session"
msgstr "セッションを終了できます"
-#: terminal/models/session/session.py:287
+#: terminal/models/session/session.py:295
msgid "Can validate session action perm"
msgstr "セッションアクションのパーマを検証できます"
@@ -6971,7 +7093,7 @@ msgstr "コマンド及び録画記憶"
msgid "Connectivity alarm"
msgstr "接続性アラーム"
-#: terminal/notifications.py:240 terminal/tasks.py:146
+#: terminal/notifications.py:240 terminal/tasks.py:153
msgid "Test failure: Account invalid"
msgstr "テスト失敗: アカウントが無効"
@@ -7092,6 +7214,18 @@ msgstr ""
"目 CACHE_LOGIN_PASSWORD_ENABLED=true を設定してサービスを再起動して有効にして"
"ください。"
+#: terminal/serializers/applet_host.py:137
+msgid "Install applets"
+msgstr "アプリをインストールする"
+
+#: terminal/serializers/applet_host.py:167
+msgid "Host ID"
+msgstr "ホスト ID"
+
+#: terminal/serializers/applet_host.py:168
+msgid "Applet ID"
+msgstr "リモートアプリケーション ID"
+
#: terminal/serializers/command.py:19
msgid "Session ID"
msgstr "セッションID"
@@ -7167,31 +7301,35 @@ msgstr ""
msgid "Asset IP"
msgstr "資産 IP"
-#: terminal/serializers/session.py:25 terminal/serializers/session.py:52
+#: terminal/serializers/session.py:25 terminal/serializers/session.py:53
msgid "Can replay"
msgstr "再生できます"
-#: terminal/serializers/session.py:26 terminal/serializers/session.py:53
+#: terminal/serializers/session.py:26 terminal/serializers/session.py:54
msgid "Can join"
msgstr "参加できます"
-#: terminal/serializers/session.py:27 terminal/serializers/session.py:56
+#: terminal/serializers/session.py:27 terminal/serializers/session.py:57
msgid "Can terminate"
msgstr "終了できます"
-#: terminal/serializers/session.py:48
+#: terminal/serializers/session.py:47
+msgid "Duration"
+msgstr "きかん"
+
+#: terminal/serializers/session.py:49
msgid "User ID"
msgstr "ユーザーID"
-#: terminal/serializers/session.py:49
+#: terminal/serializers/session.py:50
msgid "Asset ID"
msgstr "資産ID"
-#: terminal/serializers/session.py:50
+#: terminal/serializers/session.py:51
msgid "Login from display"
msgstr "表示からのログイン"
-#: terminal/serializers/session.py:57
+#: terminal/serializers/session.py:58
msgid "Terminal display"
msgstr "ターミナルディスプレイ"
@@ -7213,7 +7351,7 @@ msgstr "アクセスキー"
msgid "Access key secret"
msgstr "アクセスキーシークレット"
-#: terminal/serializers/storage.py:68 xpack/plugins/cloud/models.py:253
+#: terminal/serializers/storage.py:68 xpack/plugins/cloud/models.py:249
msgid "Region"
msgstr "リージョン"
@@ -7233,7 +7371,7 @@ msgstr "エンドポイントサフィックス"
msgid "HOST"
msgstr "ホスト"
-#: terminal/serializers/storage.py:146 users/models/user.py:830
+#: terminal/serializers/storage.py:146 users/models/user.py:844
#: xpack/plugins/cloud/serializers/account_attrs.py:213
msgid "Private key"
msgstr "ssh秘密鍵"
@@ -7391,27 +7529,31 @@ msgstr "許可が期限切れです"
msgid "storage is null"
msgstr "ストレージが空です"
-#: terminal/tasks.py:33
+#: terminal/tasks.py:31
msgid "Periodic delete terminal status"
msgstr "端末の状態を定期的にクリーンアップする"
-#: terminal/tasks.py:42
+#: terminal/tasks.py:39
msgid "Clean orphan session"
msgstr "オフライン セッションをクリアする"
-#: terminal/tasks.py:91
+#: terminal/tasks.py:87
msgid "Run applet host deployment"
msgstr "アプリケーション マシンの展開を実行する"
-#: terminal/tasks.py:101
+#: terminal/tasks.py:97
msgid "Install applet"
msgstr "アプリをインストールする"
-#: terminal/tasks.py:112
+#: terminal/tasks.py:108
+msgid "Uninstall applet"
+msgstr "アプリをアンインストールする"
+
+#: terminal/tasks.py:119
msgid "Generate applet host accounts"
msgstr "リモートアプリケーション上のアカウントを収集する"
-#: terminal/tasks.py:124
+#: terminal/tasks.py:131
msgid "Check command replay storage connectivity"
msgstr "チェックコマンドと録画ストレージの接続性"
@@ -7441,7 +7583,7 @@ msgstr ""
msgid "All available port count: {}, Already use port count: {}"
msgstr "使用可能なすべてのポート数: {}、すでに使用しているポート数: {}"
-#: tickets/api/ticket.py:88 tickets/models/ticket/general.py:288
+#: tickets/api/ticket.py:88 tickets/models/ticket/general.py:286
msgid "Applicant"
msgstr "応募者"
@@ -7453,59 +7595,55 @@ msgstr "チケット"
msgid "Apply for asset"
msgstr "資産の申請"
-#: tickets/const.py:17 tickets/const.py:25 tickets/const.py:44
+#: tickets/const.py:17 tickets/const.py:24 tickets/const.py:42
msgid "Open"
msgstr "オープン"
-#: tickets/const.py:19 tickets/const.py:32
-msgid "Reopen"
-msgstr "再オープン"
-
-#: tickets/const.py:20 tickets/const.py:33
+#: tickets/const.py:19 tickets/const.py:31
msgid "Approved"
msgstr "承認済み"
-#: tickets/const.py:21 tickets/const.py:34
+#: tickets/const.py:20 tickets/const.py:32
msgid "Rejected"
msgstr "拒否"
-#: tickets/const.py:31 tickets/const.py:39
+#: tickets/const.py:30 tickets/const.py:37
msgid "Closed"
msgstr "クローズ"
-#: tickets/const.py:51
+#: tickets/const.py:49
msgid "One level"
msgstr "1つのレベル"
-#: tickets/const.py:52
+#: tickets/const.py:50
msgid "Two level"
msgstr "2つのレベル"
-#: tickets/const.py:56
+#: tickets/const.py:54
msgid "Org admin"
msgstr "Org admin"
-#: tickets/const.py:57
+#: tickets/const.py:55
msgid "Custom user"
msgstr "カスタムユーザー"
-#: tickets/const.py:58
+#: tickets/const.py:56
msgid "Super admin"
msgstr "スーパー管理者"
-#: tickets/const.py:59
+#: tickets/const.py:57
msgid "Super admin and org admin"
msgstr "スーパーadminとorg admin"
-#: tickets/const.py:63
+#: tickets/const.py:61
msgid "All assets"
msgstr "すべての資産"
-#: tickets/const.py:64
+#: tickets/const.py:62
msgid "Permed assets"
msgstr "許可された資産"
-#: tickets/const.py:65
+#: tickets/const.py:63
msgid "Permed valid assets"
msgstr "有効な許可を受けた資産"
@@ -7550,7 +7688,7 @@ msgid "Body"
msgstr "ボディ"
#: tickets/models/flow.py:19 tickets/models/flow.py:61
-#: tickets/models/ticket/general.py:41
+#: tickets/models/ticket/general.py:42
msgid "Approve level"
msgstr "レベルを承認する"
@@ -7620,35 +7758,35 @@ msgstr "コマンド フィルタ"
msgid "Apply Command Ticket"
msgstr "製造オーダの検討を命令"
-#: tickets/models/ticket/general.py:76
+#: tickets/models/ticket/general.py:77
msgid "Ticket step"
msgstr "チケットステップ"
-#: tickets/models/ticket/general.py:94
+#: tickets/models/ticket/general.py:95
msgid "Ticket assignee"
msgstr "割り当てられたチケット"
-#: tickets/models/ticket/general.py:272
+#: tickets/models/ticket/general.py:270
msgid "Title"
msgstr "タイトル"
-#: tickets/models/ticket/general.py:292
+#: tickets/models/ticket/general.py:290
msgid "TicketFlow"
msgstr "作業指示プロセス"
-#: tickets/models/ticket/general.py:295
+#: tickets/models/ticket/general.py:293
msgid "Approval step"
msgstr "承認ステップ"
-#: tickets/models/ticket/general.py:298
+#: tickets/models/ticket/general.py:296
msgid "Relation snapshot"
msgstr "製造オーダスナップショット"
-#: tickets/models/ticket/general.py:401
+#: tickets/models/ticket/general.py:399
msgid "Please try again"
msgstr "もう一度お試しください"
-#: tickets/models/ticket/general.py:470
+#: tickets/models/ticket/general.py:475
msgid "Super ticket"
msgstr "スーパーチケット"
@@ -7793,11 +7931,11 @@ msgstr "無効な承認アクション"
msgid "This user is not authorized to approve this ticket"
msgstr "このユーザーはこの作業指示を承認する権限がありません"
-#: users/api/user.py:137
+#: users/api/user.py:155
msgid "Can not invite self"
msgstr "自分自身を招待することはできません"
-#: users/api/user.py:190
+#: users/api/user.py:208
msgid "Could not reset self otp, use profile reset instead"
msgstr "自己otpをリセットできませんでした、代わりにプロファイルリセットを使用"
@@ -7841,11 +7979,27 @@ msgstr "マルチスクリーンディスプレイ"
msgid "Drives redirect"
msgstr "ディスクマウント"
-#: users/const.py:64
+#: users/const.py:37
+msgid "Current window"
+msgstr "現在のウィンドウ"
+
+#: users/const.py:38
+msgid "New window"
+msgstr "新しいウィンドウ"
+
+#: users/const.py:47
+msgid "High(32 bit)"
+msgstr "高い(32 bit)"
+
+#: users/const.py:48
+msgid "Medium(16 bit)"
+msgstr "真ん中(16 bit)"
+
+#: users/const.py:69
msgid "Replace"
msgstr "置換"
-#: users/const.py:65
+#: users/const.py:70
msgid "Suffix"
msgstr "接尾辞を付ける"
@@ -7857,7 +8011,7 @@ msgstr "MFAが有効化されていません"
msgid "Unable to delete all users"
msgstr "すべてのユーザーを削除できません"
-#: users/forms/profile.py:50
+#: users/forms/profile.py:48
msgid ""
"When enabled, you will enter the MFA binding process the next time you log "
"in. you can also directly bind in \"personal information -> quick "
@@ -7866,11 +8020,11 @@ msgstr ""
"有効にすると、次回のログイン時にマルチファクタ認証バインドプロセスに入りま"
"す。(個人情報->クイック修正->MFAマルチファクタ認証の設定)で直接バインド!"
-#: users/forms/profile.py:61
+#: users/forms/profile.py:59
msgid "* Enable MFA to make the account more secure."
msgstr "* アカウントをより安全にするためにMFAを有効にします。"
-#: users/forms/profile.py:70
+#: users/forms/profile.py:68
msgid ""
"In order to protect you and your company, please keep your account, password "
"and key sensitive information properly. (for example: setting complex "
@@ -7879,136 +8033,140 @@ msgstr ""
"あなたとあなたの会社を保護するために、アカウント、パスワード、キーの機密情報"
"を適切に保管してください。(例: 複雑なパスワードの設定、MFAの有効化)"
-#: users/forms/profile.py:77
+#: users/forms/profile.py:75
msgid "Finish"
msgstr "仕上げ"
-#: users/forms/profile.py:84
+#: users/forms/profile.py:82
msgid "New password"
msgstr "新しいパスワード"
-#: users/forms/profile.py:89
+#: users/forms/profile.py:87
msgid "Confirm password"
msgstr "パスワードの確認"
-#: users/forms/profile.py:97
+#: users/forms/profile.py:95
msgid "Password does not match"
msgstr "パスワードが一致しない"
-#: users/forms/profile.py:105
+#: users/forms/profile.py:104
msgid "The phone number must contain an area code, for example, +86"
msgstr "電話番号には市外局番が必要です例えば「+86」"
-#: users/forms/profile.py:121
+#: users/forms/profile.py:120
msgid "Old password"
msgstr "古いパスワード"
-#: users/forms/profile.py:131
+#: users/forms/profile.py:130
msgid "Old password error"
msgstr "古いパスワードエラー"
-#: users/forms/profile.py:141
+#: users/forms/profile.py:140
msgid "Automatically configure and download the SSH key"
msgstr "SSHキーの自動設定とダウンロード"
-#: users/forms/profile.py:143
+#: users/forms/profile.py:142
msgid "ssh public key"
msgstr "ssh公開キー"
-#: users/forms/profile.py:144
+#: users/forms/profile.py:143
msgid "ssh-rsa AAAA..."
msgstr "ssh-rsa AAAA.."
-#: users/forms/profile.py:145
+#: users/forms/profile.py:144
msgid "Paste your id_rsa.pub here."
msgstr "ここにid_rsa.pubを貼り付けます。"
-#: users/forms/profile.py:158
+#: users/forms/profile.py:157
msgid "Public key should not be the same as your old one."
msgstr "公開鍵は古いものと同じであってはなりません。"
-#: users/forms/profile.py:162 users/serializers/profile.py:76
+#: users/forms/profile.py:161 users/serializers/profile.py:76
#: users/serializers/profile.py:164 users/serializers/profile.py:191
msgid "Not a valid ssh public key"
msgstr "有効なssh公開鍵ではありません"
-#: users/forms/profile.py:173 users/models/user.py:833
+#: users/forms/profile.py:172 users/models/user.py:847
#: xpack/plugins/cloud/serializers/account_attrs.py:210
msgid "Public key"
msgstr "公開キー"
-#: users/models/preference.py:38
+#: users/models/preference.py:38 users/serializers/preference/preference.py:19
msgid "Preference"
msgstr "ユーザー設定"
-#: users/models/user.py:646 users/serializers/profile.py:94
+#: users/models/user.py:656 users/serializers/profile.py:94
msgid "Force enable"
msgstr "強制有効"
-#: users/models/user.py:812 users/serializers/user.py:171
+#: users/models/user.py:762
+msgid "Lark"
+msgstr ""
+
+#: users/models/user.py:826 users/serializers/user.py:175
msgid "Is service account"
msgstr "サービスアカウントです"
-#: users/models/user.py:814
+#: users/models/user.py:828
msgid "Avatar"
msgstr "アバター"
-#: users/models/user.py:817
+#: users/models/user.py:831
msgid "Wechat"
msgstr "微信"
-#: users/models/user.py:820 users/serializers/user.py:108
+#: users/models/user.py:834 users/serializers/user.py:111
msgid "Phone"
msgstr "電話"
-#: users/models/user.py:826
+#: users/models/user.py:840
msgid "OTP secret key"
msgstr "OTP 秘密"
# msgid "Private key"
# msgstr "ssh秘密鍵"
-#: users/models/user.py:838 users/serializers/profile.py:128
-#: users/serializers/user.py:168
+#: users/models/user.py:852 users/serializers/profile.py:128
+#: users/serializers/user.py:172
msgid "Is first login"
msgstr "最初のログインです"
-#: users/models/user.py:848
+#: users/models/user.py:862
msgid "Date password last updated"
msgstr "最終更新日パスワード"
-#: users/models/user.py:851
+#: users/models/user.py:865
msgid "Need update password"
msgstr "更新パスワードが必要"
-#: users/models/user.py:853
+#: users/models/user.py:867
msgid "Date api key used"
msgstr "Api key 最後に使用した日付"
-#: users/models/user.py:985
+#: users/models/user.py:1000
msgid "Can not delete admin user"
msgstr "管理者ユーザーを削除できませんでした"
-#: users/models/user.py:1012
+#: users/models/user.py:1028
msgid "Can invite user"
msgstr "ユーザーを招待できます"
-#: users/models/user.py:1013
+#: users/models/user.py:1029
msgid "Can remove user"
msgstr "ユーザーを削除できます"
-#: users/models/user.py:1014
+#: users/models/user.py:1030
msgid "Can match user"
msgstr "ユーザーに一致できます"
-#: users/models/user.py:1023
+#: users/models/user.py:1039
msgid "Administrator"
msgstr "管理者"
-#: users/models/user.py:1026
+#: users/models/user.py:1042
msgid "Administrator is the super user of system"
msgstr "管理者はシステムのスーパーユーザーです"
-#: users/models/user.py:1051
+#: users/models/user.py:1067
msgid "User password history"
msgstr "ユーザーパスワード履歴"
@@ -8019,7 +8177,7 @@ msgstr "ユーザーパスワード履歴"
msgid "Reset password"
msgstr "パスワードのリセット"
-#: users/notifications.py:85 users/views/profile/reset.py:231
+#: users/notifications.py:85 users/views/profile/reset.py:233
msgid "Reset password success"
msgstr "パスワードのリセット成功"
@@ -8067,27 +8225,31 @@ msgstr "新しく設定されたパスワードが一致しない"
msgid "Async loading of asset tree"
msgstr "非同期ロード資産ツリー"
-#: users/serializers/preference/luna.py:33
+#: users/serializers/preference/luna.py:30
+msgid "Connect default open method"
+msgstr "デフォルトの接続オープン方法"
+
+#: users/serializers/preference/luna.py:37
msgid "RDP resolution"
msgstr "RDP 解像度"
-#: users/serializers/preference/luna.py:37
+#: users/serializers/preference/luna.py:41
msgid "Keyboard layout"
msgstr "キーボードレイアウト"
-#: users/serializers/preference/luna.py:41
+#: users/serializers/preference/luna.py:45
msgid "RDP client option"
msgstr "RDPクライアントオプション"
-#: users/serializers/preference/luna.py:45
-msgid "RDP color quality"
-msgstr ""
-
#: users/serializers/preference/luna.py:49
-msgid "Rdp smart size"
-msgstr "RDPインテリジェントサイズ"
+msgid "RDP color quality"
+msgstr "RDP の色品質"
-#: users/serializers/preference/luna.py:50
+#: users/serializers/preference/luna.py:53
+msgid "RDP smart size"
+msgstr "RDP スマート サイズ"
+
+#: users/serializers/preference/luna.py:54
msgid ""
"Determines whether the client computer should scale the content on the "
"remote computer to fit the window size of the client computer when the "
@@ -8102,27 +8264,27 @@ msgstr ""
# "ウィンドウサイズを変更するときにクライアントコンピュータがクライアントコン"
# "ピュータのウィンドウサイズに合わせるためにリモートコンピュータ上のコンテンツ"
# "をスケーリングすべきかどうかを判断する"
-#: users/serializers/preference/luna.py:55
+#: users/serializers/preference/luna.py:59
msgid "Remote application connection method"
msgstr "リモートアプリケーション接続方式"
-#: users/serializers/preference/luna.py:62
+#: users/serializers/preference/luna.py:66
msgid "Character terminal font size"
msgstr "文字終端フォントサイズ"
-#: users/serializers/preference/luna.py:65
+#: users/serializers/preference/luna.py:69
msgid "Backspace as Ctrl+H"
msgstr "文字終端Backspace As Ctrl+H"
-#: users/serializers/preference/luna.py:68
+#: users/serializers/preference/luna.py:72
msgid "Right click quickly paste"
msgstr "右クリックでクイック貼り付け"
-#: users/serializers/preference/luna.py:74
+#: users/serializers/preference/luna.py:78
msgid "Graphics"
msgstr "図形化"
-#: users/serializers/preference/luna.py:75
+#: users/serializers/preference/luna.py:79
msgid "Command line"
msgstr "コマンドライン"
@@ -8138,71 +8300,75 @@ msgstr "パスワードがセキュリティルールと一致しない"
msgid "The new password cannot be the last {} passwords"
msgstr "新しいパスワードを最後の {} 個のパスワードにすることはできません"
-#: users/serializers/user.py:42
+#: users/serializers/user.py:44
msgid "System roles"
msgstr "システムの役割"
-#: users/serializers/user.py:46
+#: users/serializers/user.py:48
msgid "Org roles"
msgstr "組織ロール"
-#: users/serializers/user.py:90
+#: users/serializers/user.py:51
+msgid "Organizations and roles"
+msgstr "そしきとやくわり"
+
+#: users/serializers/user.py:93
msgid "Password strategy"
msgstr "パスワード戦略"
-#: users/serializers/user.py:92
+#: users/serializers/user.py:95
msgid "MFA enabled"
msgstr "MFA有効化"
-#: users/serializers/user.py:94
+#: users/serializers/user.py:97
msgid "MFA force enabled"
msgstr "MFAフォース有効化"
-#: users/serializers/user.py:96
+#: users/serializers/user.py:99
msgid "Login blocked"
msgstr "ログインがロックされました"
-#: users/serializers/user.py:99 users/serializers/user.py:177
+#: users/serializers/user.py:102 users/serializers/user.py:181
msgid "Is OTP bound"
msgstr "仮想MFAがバインドされているか"
-#: users/serializers/user.py:100
+#: users/serializers/user.py:103
msgid "Super Administrator"
msgstr "スーパーアドミニストレーター"
-#: users/serializers/user.py:101
+#: users/serializers/user.py:104
msgid "Organization Administrator"
msgstr "組織管理者"
-#: users/serializers/user.py:103
+#: users/serializers/user.py:106
msgid "Can public key authentication"
msgstr "公開鍵認証が可能"
-#: users/serializers/user.py:172
+#: users/serializers/user.py:176
msgid "Is org admin"
msgstr "組織管理者です"
-#: users/serializers/user.py:174
+#: users/serializers/user.py:178
msgid "Avatar url"
msgstr "アバターURL"
-#: users/serializers/user.py:178
+#: users/serializers/user.py:182
msgid "MFA level"
msgstr "MFA レベル"
-#: users/serializers/user.py:289
+#: users/serializers/user.py:304
msgid "Select users"
msgstr "ユーザーの選択"
-#: users/serializers/user.py:290
+#: users/serializers/user.py:305
msgid "For security, only list several users"
msgstr "セキュリティのために、複数のユーザーのみをリストします"
-#: users/serializers/user.py:323
+#: users/serializers/user.py:338
msgid "name not unique"
msgstr "名前が一意ではない"
-#: users/signal_handlers.py:32
+#: users/signal_handlers.py:33
msgid ""
"The administrator has enabled \"Only allow existing users to log in\", \n"
" and the current user is not in the user list. Please contact the "
@@ -8211,7 +8377,7 @@ msgstr ""
"管理者は「既存のユーザーのみログインを許可」をオンにしており、現在のユーザー"
"はユーザーリストにありません。管理者に連絡してください。"
-#: users/signal_handlers.py:166
+#: users/signal_handlers.py:167
msgid "Clean up expired user sessions"
msgstr "期限切れのユーザー・セッションのパージ"
@@ -8306,15 +8472,15 @@ msgstr "携帯電話番号を入力すると、認証コードが携帯電話に
msgid "Email account"
msgstr "メールアドレス"
-#: users/templates/users/forgot_password.html:92
+#: users/templates/users/forgot_password.html:93
msgid "Mobile number"
msgstr "携帯番号"
-#: users/templates/users/forgot_password.html:100
+#: users/templates/users/forgot_password.html:101
msgid "Send"
msgstr "送信"
-#: users/templates/users/forgot_password.html:104
+#: users/templates/users/forgot_password.html:105
#: users/templates/users/forgot_password_previewing.html:30
msgid "Submit"
msgstr "送信"
@@ -8477,23 +8643,23 @@ msgstr ""
"ローカル以外のユーザーは、サードパーティ プラットフォームからのログインのみが"
"許可され、パスワードの変更はサポートされていません: {}"
-#: users/views/profile/reset.py:186 users/views/profile/reset.py:197
+#: users/views/profile/reset.py:188 users/views/profile/reset.py:199
msgid "Token invalid or expired"
msgstr "トークンが無効または期限切れ"
-#: users/views/profile/reset.py:202
+#: users/views/profile/reset.py:204
msgid "User auth from {}, go there change password"
msgstr "ユーザー認証ソース {}, 対応するシステムにパスワードを変更してください"
-#: users/views/profile/reset.py:209
+#: users/views/profile/reset.py:211
msgid "* Your password does not meet the requirements"
msgstr "* パスワードが要件を満たしていない"
-#: users/views/profile/reset.py:215
+#: users/views/profile/reset.py:217
msgid "* The new password cannot be the last {} passwords"
msgstr "* 新しいパスワードを最後の {} パスワードにすることはできません"
-#: users/views/profile/reset.py:232
+#: users/views/profile/reset.py:234
msgid "Reset password success, return to login page"
msgstr "パスワードの成功をリセットし、ログインページに戻る"
@@ -8506,11 +8672,11 @@ msgid ""
"The current task is not synchronized with unmatched policy assets, skipping"
msgstr ""
-#: xpack/plugins/cloud/api.py:56
+#: xpack/plugins/cloud/api.py:60
msgid "Test connection successful"
msgstr "テスト接続成功"
-#: xpack/plugins/cloud/api.py:58
+#: xpack/plugins/cloud/api.py:62
msgid "Test connection failed: {}"
msgstr "テスト接続に失敗しました: {}"
@@ -8562,91 +8728,96 @@ msgstr "谷歌雲"
msgid "UCloud"
msgstr "ucloud"
-#: xpack/plugins/cloud/const.py:22
+#: xpack/plugins/cloud/const.py:21
+msgid "Volcengine"
+msgstr "Volcengine"
+
+#: xpack/plugins/cloud/const.py:23
msgid "VMware"
msgstr "VMware"
-#: xpack/plugins/cloud/const.py:23 xpack/plugins/cloud/providers/nutanix.py:15
+#: xpack/plugins/cloud/const.py:24 xpack/plugins/cloud/providers/nutanix.py:15
msgid "Nutanix"
msgstr "Nutanix"
-#: xpack/plugins/cloud/const.py:24
+#: xpack/plugins/cloud/const.py:25
msgid "Huawei Private Cloud"
msgstr "華為私有雲"
-#: xpack/plugins/cloud/const.py:25
+#: xpack/plugins/cloud/const.py:26
msgid "Qingyun Private Cloud"
msgstr "青雲私有雲"
-#: xpack/plugins/cloud/const.py:26
+#: xpack/plugins/cloud/const.py:27
msgid "CTYun Private Cloud"
msgstr "スカイウィング私有雲"
-#: xpack/plugins/cloud/const.py:27
+#: xpack/plugins/cloud/const.py:28
msgid "OpenStack"
msgstr "OpenStack"
-#: xpack/plugins/cloud/const.py:28 xpack/plugins/cloud/providers/zstack.py:21
+#: xpack/plugins/cloud/const.py:29 xpack/plugins/cloud/providers/zstack.py:21
msgid "ZStack"
msgstr "ZStack"
-#: xpack/plugins/cloud/const.py:29
+#: xpack/plugins/cloud/const.py:30
msgid "Fusion Compute"
msgstr "融合計算"
-#: xpack/plugins/cloud/const.py:30
+#: xpack/plugins/cloud/const.py:31
msgid "SCP"
msgstr "SCP"
-#: xpack/plugins/cloud/const.py:31
+#: xpack/plugins/cloud/const.py:32
msgid "Apsara Stack"
msgstr "Apsara Stack"
-#: xpack/plugins/cloud/const.py:36
+#: xpack/plugins/cloud/const.py:37
msgid "Private IP"
msgstr "プライベートIP"
-#: xpack/plugins/cloud/const.py:37
+#: xpack/plugins/cloud/const.py:38
msgid "Public IP"
msgstr "パブリックIP"
-#: xpack/plugins/cloud/const.py:41 xpack/plugins/cloud/models.py:303
+#: xpack/plugins/cloud/const.py:42 xpack/plugins/cloud/models.py:299
msgid "Instance name"
msgstr "インスタンス名"
-#: xpack/plugins/cloud/const.py:42
+#: xpack/plugins/cloud/const.py:43
msgid "Instance name and Partial IP"
msgstr "インスタンス名と部分IP"
-#: xpack/plugins/cloud/const.py:47
+#: xpack/plugins/cloud/const.py:48
msgid "Succeed"
msgstr "成功"
-#: xpack/plugins/cloud/const.py:51
+#: xpack/plugins/cloud/const.py:52
msgid "Unsync"
msgstr "同期していません"
-#: xpack/plugins/cloud/const.py:52
+#: xpack/plugins/cloud/const.py:53
msgid "New Sync"
msgstr "新しい同期"
-#: xpack/plugins/cloud/const.py:53
+#: xpack/plugins/cloud/const.py:54
msgid "Synced"
msgstr "同期済み"
-#: xpack/plugins/cloud/const.py:54
+#: xpack/plugins/cloud/const.py:55
msgid "Released"
msgstr "リリース済み"
-#: xpack/plugins/cloud/const.py:58
+#: xpack/plugins/cloud/const.py:59
msgid "And"
msgstr "そして"
-#: xpack/plugins/cloud/const.py:59
+#: xpack/plugins/cloud/const.py:60
msgid "Or"
msgstr "または"
-#: xpack/plugins/cloud/manager.py:57
+#: xpack/plugins/cloud/manager.py:55 xpack/plugins/cloud/providers/gcp.py:64
+#: xpack/plugins/cloud/providers/huaweicloud.py:34
msgid "Account unavailable"
msgstr "利用できないアカウント"
@@ -8670,143 +8841,141 @@ msgstr "クラウドアカウント"
msgid "Test cloud account"
msgstr "クラウドアカウントのテスト"
-#: xpack/plugins/cloud/models.py:92 xpack/plugins/cloud/serializers/task.py:159
+#: xpack/plugins/cloud/models.py:88 xpack/plugins/cloud/serializers/task.py:159
msgid "Regions"
msgstr "リージョン"
-#: xpack/plugins/cloud/models.py:95
+#: xpack/plugins/cloud/models.py:91
msgid "Hostname strategy"
msgstr "ホスト名戦略"
-#: xpack/plugins/cloud/models.py:100
-#: xpack/plugins/cloud/serializers/task.py:162
+#: xpack/plugins/cloud/models.py:96 xpack/plugins/cloud/serializers/task.py:162
msgid "IP network segment group"
msgstr "IPネットワークセグメントグループ"
-#: xpack/plugins/cloud/models.py:103
-#: xpack/plugins/cloud/serializers/task.py:167
+#: xpack/plugins/cloud/models.py:99 xpack/plugins/cloud/serializers/task.py:167
msgid "Sync IP type"
msgstr "同期IPタイプ"
-#: xpack/plugins/cloud/models.py:106
+#: xpack/plugins/cloud/models.py:102
#: xpack/plugins/cloud/serializers/task.py:185
msgid "Always update"
msgstr "常に更新"
-#: xpack/plugins/cloud/models.py:108
+#: xpack/plugins/cloud/models.py:104
msgid "Fully synchronous"
msgstr "完全同期"
-#: xpack/plugins/cloud/models.py:113
+#: xpack/plugins/cloud/models.py:109
msgid "Date last sync"
msgstr "最終同期日"
-#: xpack/plugins/cloud/models.py:116 xpack/plugins/cloud/models.py:321
-#: xpack/plugins/cloud/models.py:345
+#: xpack/plugins/cloud/models.py:112 xpack/plugins/cloud/models.py:317
+#: xpack/plugins/cloud/models.py:341
msgid "Strategy"
msgstr "戦略"
-#: xpack/plugins/cloud/models.py:121 xpack/plugins/cloud/models.py:200
+#: xpack/plugins/cloud/models.py:117 xpack/plugins/cloud/models.py:196
msgid "Sync instance task"
msgstr "インスタンスの同期タスク"
-#: xpack/plugins/cloud/models.py:211 xpack/plugins/cloud/models.py:263
+#: xpack/plugins/cloud/models.py:207 xpack/plugins/cloud/models.py:259
msgid "Date sync"
msgstr "日付の同期"
-#: xpack/plugins/cloud/models.py:215
+#: xpack/plugins/cloud/models.py:211
msgid "Sync instance snapshot"
msgstr "インスタンススナップショットの同期"
-#: xpack/plugins/cloud/models.py:219
+#: xpack/plugins/cloud/models.py:215
msgid "Sync instance task execution"
msgstr "インスタンスタスクの同期実行"
-#: xpack/plugins/cloud/models.py:243
+#: xpack/plugins/cloud/models.py:239
msgid "Sync task"
msgstr "同期タスク"
-#: xpack/plugins/cloud/models.py:247
+#: xpack/plugins/cloud/models.py:243
msgid "Sync instance task history"
msgstr "インスタンスタスク履歴の同期"
-#: xpack/plugins/cloud/models.py:250
+#: xpack/plugins/cloud/models.py:246
msgid "Instance"
msgstr "インスタンス"
-#: xpack/plugins/cloud/models.py:267
+#: xpack/plugins/cloud/models.py:263
msgid "Sync instance detail"
msgstr "同期インスタンスの詳細"
-#: xpack/plugins/cloud/models.py:279 xpack/plugins/cloud/serializers/task.py:72
+#: xpack/plugins/cloud/models.py:275 xpack/plugins/cloud/serializers/task.py:72
msgid "Rule relation"
msgstr "条件関係"
-#: xpack/plugins/cloud/models.py:288
+#: xpack/plugins/cloud/models.py:284
msgid "Task strategy"
msgstr "ミッション戦略です"
-#: xpack/plugins/cloud/models.py:292
+#: xpack/plugins/cloud/models.py:288
msgid "Equal"
msgstr "等しい"
-#: xpack/plugins/cloud/models.py:293
+#: xpack/plugins/cloud/models.py:289
msgid "Not Equal"
msgstr "不等于"
-#: xpack/plugins/cloud/models.py:294
+#: xpack/plugins/cloud/models.py:290
msgid "In"
msgstr "で..."
-#: xpack/plugins/cloud/models.py:295
+#: xpack/plugins/cloud/models.py:291
msgid "Contains"
msgstr "含む"
-#: xpack/plugins/cloud/models.py:296
+#: xpack/plugins/cloud/models.py:292
msgid "Exclude"
msgstr "除外"
-#: xpack/plugins/cloud/models.py:297
+#: xpack/plugins/cloud/models.py:293
msgid "Startswith"
msgstr "始まる..."
-#: xpack/plugins/cloud/models.py:298
+#: xpack/plugins/cloud/models.py:294
msgid "Endswith"
msgstr "終わる..."
-#: xpack/plugins/cloud/models.py:304
+#: xpack/plugins/cloud/models.py:300
msgid "Instance platform"
msgstr "インスタンス名"
-#: xpack/plugins/cloud/models.py:305
+#: xpack/plugins/cloud/models.py:301
msgid "Instance address"
msgstr "インスタンスアドレス"
-#: xpack/plugins/cloud/models.py:312
+#: xpack/plugins/cloud/models.py:308
msgid "Rule attr"
msgstr "ルール属性"
-#: xpack/plugins/cloud/models.py:316
+#: xpack/plugins/cloud/models.py:312
msgid "Rule match"
msgstr "ルール一致"
-#: xpack/plugins/cloud/models.py:318
+#: xpack/plugins/cloud/models.py:314
msgid "Rule value"
msgstr "ルール値"
-#: xpack/plugins/cloud/models.py:325 xpack/plugins/cloud/serializers/task.py:75
+#: xpack/plugins/cloud/models.py:321 xpack/plugins/cloud/serializers/task.py:75
msgid "Strategy rule"
msgstr "戦略ルール"
-#: xpack/plugins/cloud/models.py:340
+#: xpack/plugins/cloud/models.py:336
msgid "Action attr"
msgstr "アクション属性"
-#: xpack/plugins/cloud/models.py:342
+#: xpack/plugins/cloud/models.py:338
msgid "Action value"
msgstr "アクション値"
-#: xpack/plugins/cloud/models.py:349 xpack/plugins/cloud/serializers/task.py:78
+#: xpack/plugins/cloud/models.py:345 xpack/plugins/cloud/serializers/task.py:78
msgid "Strategy action"
msgstr "戦略アクション"
@@ -8902,97 +9071,97 @@ msgstr "中东 (バーレーン)"
msgid "South America (São Paulo)"
msgstr "南米 (サンパウロ)"
-#: xpack/plugins/cloud/providers/baiducloud.py:54
+#: xpack/plugins/cloud/providers/baiducloud.py:56
#: xpack/plugins/cloud/providers/jdcloud.py:125
msgid "CN North-Beijing"
msgstr "華北-北京"
-#: xpack/plugins/cloud/providers/baiducloud.py:55
-#: xpack/plugins/cloud/providers/huaweicloud.py:42
+#: xpack/plugins/cloud/providers/baiducloud.py:57
+#: xpack/plugins/cloud/providers/huaweicloud.py:47
#: xpack/plugins/cloud/providers/jdcloud.py:128
msgid "CN South-Guangzhou"
msgstr "華南-広州"
-#: xpack/plugins/cloud/providers/baiducloud.py:56
+#: xpack/plugins/cloud/providers/baiducloud.py:58
msgid "CN East-Suzhou"
msgstr "華東-蘇州"
-#: xpack/plugins/cloud/providers/baiducloud.py:57
-#: xpack/plugins/cloud/providers/huaweicloud.py:49
+#: xpack/plugins/cloud/providers/baiducloud.py:59
+#: xpack/plugins/cloud/providers/huaweicloud.py:54
msgid "CN-Hong Kong"
msgstr "中国-香港"
-#: xpack/plugins/cloud/providers/baiducloud.py:58
+#: xpack/plugins/cloud/providers/baiducloud.py:60
msgid "CN Center-Wuhan"
msgstr "華中-武漢"
-#: xpack/plugins/cloud/providers/baiducloud.py:59
+#: xpack/plugins/cloud/providers/baiducloud.py:61
msgid "CN North-Baoding"
msgstr "華北-保定"
-#: xpack/plugins/cloud/providers/baiducloud.py:60
+#: xpack/plugins/cloud/providers/baiducloud.py:62
#: xpack/plugins/cloud/providers/jdcloud.py:127
msgid "CN East-Shanghai"
msgstr "華東-上海"
-#: xpack/plugins/cloud/providers/baiducloud.py:61
-#: xpack/plugins/cloud/providers/huaweicloud.py:51
+#: xpack/plugins/cloud/providers/baiducloud.py:63
+#: xpack/plugins/cloud/providers/huaweicloud.py:56
msgid "AP-Singapore"
msgstr "アジア太平洋-シンガポール"
-#: xpack/plugins/cloud/providers/huaweicloud.py:39
+#: xpack/plugins/cloud/providers/huaweicloud.py:44
msgid "CN North-Beijing1"
msgstr "華北-北京1"
-#: xpack/plugins/cloud/providers/huaweicloud.py:40
+#: xpack/plugins/cloud/providers/huaweicloud.py:45
msgid "CN North-Beijing4"
msgstr "華北-北京4"
-#: xpack/plugins/cloud/providers/huaweicloud.py:41
+#: xpack/plugins/cloud/providers/huaweicloud.py:46
msgid "CN North-Ulanqab1"
msgstr "華北-ウランチャブ一"
-#: xpack/plugins/cloud/providers/huaweicloud.py:43
+#: xpack/plugins/cloud/providers/huaweicloud.py:48
msgid "CN South-Shenzhen"
msgstr "華南-広州"
-#: xpack/plugins/cloud/providers/huaweicloud.py:44
+#: xpack/plugins/cloud/providers/huaweicloud.py:49
msgid "CN South-Guangzhou-InvitationOnly"
msgstr "華南-広州-友好ユーザー環境"
-#: xpack/plugins/cloud/providers/huaweicloud.py:45
+#: xpack/plugins/cloud/providers/huaweicloud.py:50
msgid "CN East-Shanghai2"
msgstr "華東-上海2"
-#: xpack/plugins/cloud/providers/huaweicloud.py:46
+#: xpack/plugins/cloud/providers/huaweicloud.py:51
msgid "CN East-Shanghai1"
msgstr "華東-上海1"
-#: xpack/plugins/cloud/providers/huaweicloud.py:48
+#: xpack/plugins/cloud/providers/huaweicloud.py:53
msgid "CN Southwest-Guiyang1"
msgstr "南西-貴陽1"
-#: xpack/plugins/cloud/providers/huaweicloud.py:50
+#: xpack/plugins/cloud/providers/huaweicloud.py:55
msgid "AP-Bangkok"
msgstr "アジア太平洋-バンコク"
-#: xpack/plugins/cloud/providers/huaweicloud.py:53
+#: xpack/plugins/cloud/providers/huaweicloud.py:58
msgid "AF-Johannesburg"
msgstr "アフリカ-ヨハネスブルク"
-#: xpack/plugins/cloud/providers/huaweicloud.py:54
+#: xpack/plugins/cloud/providers/huaweicloud.py:59
msgid "LA-Mexico City1"
msgstr "LA-メキシコCity1"
-#: xpack/plugins/cloud/providers/huaweicloud.py:55
+#: xpack/plugins/cloud/providers/huaweicloud.py:60
msgid "LA-Santiago"
msgstr "ラテンアメリカ-サンディエゴ"
-#: xpack/plugins/cloud/providers/huaweicloud.py:56
+#: xpack/plugins/cloud/providers/huaweicloud.py:61
msgid "LA-Sao Paulo1"
msgstr "ラミー・サンパウロ1"
-#: xpack/plugins/cloud/providers/huaweicloud.py:58
+#: xpack/plugins/cloud/providers/huaweicloud.py:63
msgid "TR-Istanbul"
msgstr "TR-Istanbul"
@@ -9000,11 +9169,11 @@ msgstr "TR-Istanbul"
msgid "CN East-Suqian"
msgstr "華東-宿遷"
-#: xpack/plugins/cloud/serializers/account.py:68
+#: xpack/plugins/cloud/serializers/account.py:69
msgid "Validity display"
msgstr "有効表示"
-#: xpack/plugins/cloud/serializers/account.py:69
+#: xpack/plugins/cloud/serializers/account.py:70
msgid "Provider display"
msgstr "プロバイダ表示"
@@ -9157,14 +9326,10 @@ msgid "Theme"
msgstr "テーマ"
#: xpack/plugins/interface/models.py:42
-msgid "Beian link"
-msgstr "公安オンライン申告ジャンプリンク"
+msgid "Footer content"
+msgstr "フッターの内容"
-#: xpack/plugins/interface/models.py:43
-msgid "Beian text"
-msgstr "公安網登録番号"
-
-#: xpack/plugins/interface/models.py:46 xpack/plugins/interface/models.py:87
+#: xpack/plugins/interface/models.py:45 xpack/plugins/interface/models.py:86
msgid "Interface setting"
msgstr "インターフェイスの設定"
@@ -9195,27 +9360,3 @@ msgstr "エンタープライズプロフェッショナル版"
#: xpack/plugins/license/models.py:86
msgid "Ultimate edition"
msgstr "エンタープライズ・フラッグシップ・エディション"
-
-#~ msgid "SMTP port"
-#~ msgstr "SMTPポート"
-
-#~ msgid "SMTP account"
-#~ msgstr "SMTPアカウント"
-
-#~ msgid "SMTP password"
-#~ msgstr "SMTPパスワード"
-
-#~ msgid "Password can not contains `{{` or `}}`"
-#~ msgstr "パスワードには `{` または `}` 文字を含めることはできません"
-
-#~ msgid "Password can not contains `{%` or `%}`"
-#~ msgstr "パスワードには `{%` または `%}` 文字を含めることはできません"
-
-#~ msgid "FeiShu query user failed"
-#~ msgstr "FeiShuクエリユーザーが失敗しました"
-
-#~ msgid "The FeiShu is already bound to another user"
-#~ msgstr "FeiShuはすでに別のユーザーにバインドされています"
-
-#~ msgid "Binding FeiShu successfully"
-#~ msgstr "本を飛ばすのバインドに成功"
diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo
index 3905e0b73..6b7911b0b 100644
--- a/apps/locale/zh/LC_MESSAGES/django.mo
+++ b/apps/locale/zh/LC_MESSAGES/django.mo
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e66a6fa05d25f1c502f95001b5ff0d0a310affd32eac939fd7b840845028074f
-size 142298
+oid sha256:4bfbb31baed0a6afd7717ded8ff640e43579bc01991714904cbf81f6dfa280e5
+size 145173
diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po
index 9bf7548c0..d07dc611c 100644
--- a/apps/locale/zh/LC_MESSAGES/django.po
+++ b/apps/locale/zh/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: JumpServer 0.3.3\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-02-27 16:09+0800\n"
+"POT-Creation-Date: 2024-04-15 17:54+0800\n"
"PO-Revision-Date: 2021-05-20 10:54+0800\n"
"Last-Translator: ibuler \n"
"Language-Team: JumpServer team\n"
@@ -21,21 +21,21 @@ msgstr ""
msgid "The parameter 'action' must be [{}]"
msgstr "参数 'action' 必须是 [{}]"
-#: accounts/automations/change_secret/manager.py:201
+#: accounts/automations/change_secret/manager.py:225
#, python-format
msgid "Success: %s, Failed: %s, Total: %s"
msgstr "成功: %s, 失败: %s, 总数: %s"
#: accounts/const/account.py:6
-#: accounts/serializers/automations/change_secret.py:32
+#: accounts/serializers/automations/change_secret.py:34
#: assets/models/_user.py:24 audits/signal_handlers/login_log.py:34
#: authentication/confirm/password.py:9 authentication/confirm/password.py:24
-#: authentication/confirm/password.py:26 authentication/forms.py:32
+#: authentication/confirm/password.py:26 authentication/forms.py:28
#: authentication/templates/authentication/login.html:330
#: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47
#: settings/serializers/msg.py:35 terminal/serializers/storage.py:123
-#: terminal/serializers/storage.py:142 users/forms/profile.py:22
-#: users/serializers/user.py:106
+#: terminal/serializers/storage.py:142 users/forms/profile.py:21
+#: users/serializers/user.py:109
#: users/templates/users/_msg_user_created.html:13
#: users/templates/users/user_password_verify.html:18
#: xpack/plugins/cloud/serializers/account_attrs.py:28
@@ -43,7 +43,7 @@ msgid "Password"
msgstr "密码"
#: accounts/const/account.py:7
-#: accounts/serializers/automations/change_secret.py:33
+#: accounts/serializers/automations/change_secret.py:35
#: terminal/serializers/storage.py:124
msgid "SSH key"
msgstr "SSH 密钥"
@@ -79,32 +79,36 @@ msgstr "同名账号"
msgid "Anonymous account"
msgstr "匿名账号"
-#: accounts/const/account.py:25 users/models/user.py:742
+#: accounts/const/account.py:18
+msgid "Specified account"
+msgstr "指定账号"
+
+#: accounts/const/account.py:26 users/models/user.py:752
msgid "Local"
msgstr "数据库"
-#: accounts/const/account.py:26
+#: accounts/const/account.py:27
msgid "Collected"
msgstr "收集"
-#: accounts/const/account.py:27 accounts/serializers/account/account.py:28
+#: accounts/const/account.py:28 accounts/serializers/account/account.py:28
#: settings/serializers/auth/sms.py:79
msgid "Template"
msgstr "模板"
-#: accounts/const/account.py:31 ops/const.py:46
+#: accounts/const/account.py:32 ops/const.py:46
msgid "Skip"
msgstr "跳过"
-#: accounts/const/account.py:32 audits/const.py:24 rbac/tree.py:239
+#: accounts/const/account.py:33 audits/const.py:24 rbac/tree.py:239
#: templates/_csv_import_export.html:18 templates/_csv_update_modal.html:6
msgid "Update"
msgstr "更新"
-#: accounts/const/account.py:33
-#: accounts/serializers/automations/change_secret.py:150 audits/const.py:62
+#: accounts/const/account.py:34 accounts/const/automation.py:109
+#: accounts/serializers/automations/change_secret.py:164 audits/const.py:62
#: audits/signal_handlers/activity_log.py:33 common/const/choices.py:19
-#: ops/const.py:75 terminal/const.py:79 xpack/plugins/cloud/const.py:46
+#: ops/const.py:76 terminal/const.py:79 xpack/plugins/cloud/const.py:47
msgid "Failed"
msgstr "失败"
@@ -205,9 +209,9 @@ msgstr "仅创建"
#: authentication/serializers/password_mfa.py:16
#: authentication/serializers/password_mfa.py:24
#: notifications/backends/__init__.py:10 settings/serializers/msg.py:22
-#: settings/serializers/msg.py:64 users/forms/profile.py:102
-#: users/forms/profile.py:109 users/models/user.py:802
-#: users/templates/users/forgot_password.html:160
+#: settings/serializers/msg.py:64 users/forms/profile.py:100
+#: users/forms/profile.py:108 users/models/user.py:816
+#: users/templates/users/forgot_password.html:162
#: users/views/profile/reset.py:94
msgid "Email"
msgstr "邮箱"
@@ -216,6 +220,20 @@ msgstr "邮箱"
msgid "SFTP"
msgstr "SFTP"
+#: accounts/const/automation.py:110
+#: accounts/serializers/automations/change_secret.py:163 audits/const.py:61
+#: audits/models.py:64 audits/signal_handlers/activity_log.py:33
+#: common/const/choices.py:18 ops/const.py:74 ops/serializers/celery.py:46
+#: terminal/const.py:78 terminal/models/session/sharing.py:121
+#: tickets/views/approve.py:128
+msgid "Success"
+msgstr "成功"
+
+#: accounts/const/automation.py:111 common/const/choices.py:16
+#: terminal/const.py:77 tickets/const.py:29 tickets/const.py:38
+msgid "Pending"
+msgstr "待定的"
+
#: accounts/const/vault.py:8 assets/const/category.py:12
#: assets/models/asset/database.py:9 assets/models/asset/database.py:24
msgid "Database"
@@ -248,19 +266,20 @@ msgstr "用户 %s 查看/导出 了密码"
#: accounts/serializers/account/account.py:213
#: accounts/serializers/account/account.py:258
#: accounts/serializers/account/gathered_account.py:10
-#: accounts/serializers/automations/change_secret.py:106
-#: accounts/serializers/automations/change_secret.py:126
+#: accounts/serializers/automations/change_secret.py:108
+#: accounts/serializers/automations/change_secret.py:140
#: accounts/templates/accounts/asset_account_change_info.html:7
+#: accounts/templates/accounts/change_secret_failed_info.html:11
#: acls/serializers/base.py:123 assets/models/asset/common.py:95
#: assets/models/asset/common.py:350 assets/models/cmd_filter.py:36
#: audits/models.py:58 authentication/models/connection_token.py:36
#: perms/models/asset_permission.py:69 perms/serializers/permission.py:36
-#: terminal/backends/command/models.py:17 terminal/models/session/session.py:31
+#: terminal/backends/command/models.py:17 terminal/models/session/session.py:32
#: terminal/notifications.py:155 terminal/serializers/command.py:17
#: terminal/serializers/session.py:28
#: terminal/templates/terminal/_msg_command_warning.html:4
#: terminal/templates/terminal/_msg_session_sharing.html:4
-#: tickets/models/ticket/apply_asset.py:16 xpack/plugins/cloud/models.py:256
+#: tickets/models/ticket/apply_asset.py:16 xpack/plugins/cloud/models.py:252
msgid "Asset"
msgstr "资产"
@@ -272,15 +291,14 @@ msgstr "资产"
msgid "Su from"
msgstr "切换自"
-#: accounts/models/account.py:55 assets/const/protocol.py:169
-#: settings/serializers/auth/cas.py:20 settings/serializers/auth/feishu.py:20
-#: terminal/models/applet/applet.py:35
+#: accounts/models/account.py:55 assets/const/protocol.py:177
+#: settings/serializers/auth/cas.py:20 terminal/models/applet/applet.py:35
#: terminal/models/virtualapp/virtualapp.py:21
msgid "Version"
msgstr "版本"
#: accounts/models/account.py:57 accounts/serializers/account/account.py:215
-#: users/models/user.py:845
+#: users/models/user.py:859
msgid "Source"
msgstr "来源"
@@ -289,17 +307,18 @@ msgid "Source ID"
msgstr "来源 ID"
#: accounts/models/account.py:61
-#: accounts/serializers/automations/change_secret.py:107
-#: accounts/serializers/automations/change_secret.py:127
+#: accounts/serializers/automations/change_secret.py:110
+#: accounts/serializers/automations/change_secret.py:141
+#: accounts/templates/accounts/change_secret_failed_info.html:12
#: acls/serializers/base.py:124 acls/templates/acls/asset_login_reminder.html:7
#: assets/serializers/asset/common.py:128 assets/serializers/gateway.py:28
-#: audits/models.py:59 authentication/api/connection_token.py:405
+#: audits/models.py:59 authentication/api/connection_token.py:411
#: ops/models/base.py:18 perms/models/asset_permission.py:75
#: perms/serializers/permission.py:41 settings/serializers/msg.py:33
-#: terminal/backends/command/models.py:18 terminal/models/session/session.py:33
+#: terminal/backends/command/models.py:18 terminal/models/session/session.py:34
#: terminal/templates/terminal/_msg_command_warning.html:8
#: terminal/templates/terminal/_msg_session_sharing.html:8
-#: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:89
+#: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:85
msgid "Account"
msgstr "账号"
@@ -364,11 +383,11 @@ msgstr "账号备份计划"
#: accounts/models/automations/backup_account.py:119
#: assets/models/automations/base.py:115 audits/models.py:65
-#: ops/models/base.py:55 ops/models/celery.py:86 ops/models/job.py:236
+#: ops/models/base.py:55 ops/models/celery.py:88 ops/models/job.py:237
#: ops/templates/ops/celery_task_log.html:75
#: perms/models/asset_permission.py:78
#: settings/templates/ldap/_msg_import_ldap_user.html:5
-#: terminal/models/applet/host.py:141 terminal/models/session/session.py:44
+#: terminal/models/applet/host.py:141 terminal/models/session/session.py:45
#: tickets/models/ticket/apply_application.py:30
#: tickets/models/ticket/apply_asset.py:19
msgid "Date start"
@@ -387,21 +406,21 @@ msgstr "账号备份快照"
#: accounts/models/automations/backup_account.py:130
#: accounts/serializers/account/backup.py:49
-#: accounts/serializers/automations/base.py:55
+#: accounts/serializers/automations/base.py:56
#: assets/models/automations/base.py:122
#: assets/serializers/automations/base.py:40
msgid "Trigger mode"
msgstr "触发模式"
#: accounts/models/automations/backup_account.py:133 audits/models.py:203
-#: terminal/models/session/sharing.py:125 xpack/plugins/cloud/models.py:208
+#: terminal/models/session/sharing.py:125 xpack/plugins/cloud/models.py:204
msgid "Reason"
msgstr "原因"
#: accounts/models/automations/backup_account.py:135
-#: accounts/serializers/automations/change_secret.py:105
-#: accounts/serializers/automations/change_secret.py:128
-#: ops/serializers/job.py:71 terminal/serializers/session.py:51
+#: accounts/serializers/automations/change_secret.py:107
+#: accounts/serializers/automations/change_secret.py:142
+#: ops/serializers/job.py:71 terminal/serializers/session.py:52
msgid "Is success"
msgstr "是否成功"
@@ -452,8 +471,8 @@ msgstr "SSH 密钥推送方式"
#: accounts/models/automations/change_secret.py:15
#: accounts/models/automations/gather_account.py:58
#: accounts/serializers/account/backup.py:41
-#: accounts/serializers/automations/change_secret.py:56
-#: settings/serializers/auth/ldap.py:81
+#: accounts/serializers/automations/change_secret.py:58
+#: settings/serializers/auth/ldap.py:90
msgid "Recipient"
msgstr "收件人"
@@ -475,27 +494,29 @@ msgstr "开始日期"
#: accounts/models/automations/change_secret.py:42
#: assets/models/automations/base.py:116 ops/models/base.py:56
-#: ops/models/celery.py:87 ops/models/job.py:237
+#: ops/models/celery.py:89 ops/models/job.py:238
#: terminal/models/applet/host.py:142
msgid "Date finished"
msgstr "结束日期"
-#: accounts/models/automations/change_secret.py:43
+#: accounts/models/automations/change_secret.py:44
#: assets/models/automations/base.py:113 audits/models.py:208
-#: audits/serializers.py:54 ops/models/base.py:49 ops/models/job.py:228
+#: audits/serializers.py:54 ops/models/base.py:49 ops/models/job.py:229
#: terminal/models/applet/applet.py:320 terminal/models/applet/host.py:140
#: terminal/models/component/status.py:30
#: terminal/models/virtualapp/virtualapp.py:99
#: terminal/serializers/applet.py:18 terminal/serializers/applet_host.py:136
-#: terminal/serializers/virtualapp.py:35 tickets/models/ticket/general.py:283
+#: terminal/serializers/virtualapp.py:35 tickets/models/ticket/general.py:281
#: tickets/serializers/super_ticket.py:13
-#: tickets/serializers/ticket/ticket.py:20 xpack/plugins/cloud/models.py:204
-#: xpack/plugins/cloud/models.py:260
+#: tickets/serializers/ticket/ticket.py:20 xpack/plugins/cloud/models.py:200
+#: xpack/plugins/cloud/models.py:256
msgid "Status"
msgstr "状态"
-#: accounts/models/automations/change_secret.py:44
-#: accounts/serializers/account/account.py:260 assets/const/automation.py:8
+#: accounts/models/automations/change_secret.py:47
+#: accounts/serializers/account/account.py:260
+#: accounts/templates/accounts/change_secret_failed_info.html:13
+#: assets/const/automation.py:8
#: authentication/templates/authentication/passkey.html:173
#: authentication/views/base.py:42 authentication/views/base.py:43
#: authentication/views/base.py:44 common/const/choices.py:20
@@ -503,7 +524,7 @@ msgstr "状态"
msgid "Error"
msgstr "错误"
-#: accounts/models/automations/change_secret.py:48
+#: accounts/models/automations/change_secret.py:51
msgid "Change secret record"
msgstr "改密记录"
@@ -519,12 +540,12 @@ msgstr "最后登录日期"
#: accounts/models/automations/push_account.py:15 accounts/models/base.py:65
#: accounts/serializers/account/virtual.py:21 acls/serializers/base.py:19
#: acls/serializers/base.py:50 assets/models/_user.py:23 audits/models.py:188
-#: authentication/forms.py:25 authentication/forms.py:27
+#: authentication/forms.py:21 authentication/forms.py:23
#: authentication/models/temp_token.py:9
#: authentication/templates/authentication/_msg_different_city.html:9
#: authentication/templates/authentication/_msg_oauth_bind.html:9
-#: terminal/serializers/storage.py:136 users/forms/profile.py:32
-#: users/forms/profile.py:115 users/models/user.py:798
+#: terminal/serializers/storage.py:136 users/forms/profile.py:31
+#: users/forms/profile.py:114 users/models/user.py:812
#: users/templates/users/_msg_user_created.html:12
#: xpack/plugins/cloud/serializers/account_attrs.py:26
msgid "Username"
@@ -556,6 +577,7 @@ msgstr "触发方式"
#: audits/models.py:92 audits/serializers.py:84
#: authentication/serializers/connect_token_secret.py:119
#: authentication/templates/authentication/_access_key_modal.html:34
+#: tickets/serializers/ticket/ticket.py:21
msgid "Action"
msgstr "动作"
@@ -570,7 +592,7 @@ msgstr "账号验证"
#: accounts/models/base.py:37 accounts/models/base.py:67
#: accounts/serializers/account/account.py:440
#: accounts/serializers/account/base.py:17
-#: accounts/serializers/automations/change_secret.py:45
+#: accounts/serializers/automations/change_secret.py:47
#: authentication/serializers/connect_token_secret.py:42
#: authentication/serializers/connect_token_secret.py:51
#: terminal/serializers/storage.py:140
@@ -586,12 +608,12 @@ msgid "Secret"
msgstr "密钥"
#: accounts/models/base.py:42
-#: accounts/serializers/automations/change_secret.py:39
+#: accounts/serializers/automations/change_secret.py:41
msgid "Secret strategy"
msgstr "密文策略"
#: accounts/models/base.py:44 accounts/serializers/account/template.py:24
-#: accounts/serializers/automations/change_secret.py:44
+#: accounts/serializers/automations/change_secret.py:46
msgid "Password rules"
msgstr "密码规则"
@@ -608,7 +630,7 @@ msgstr "密码规则"
#: authentication/serializers/connect_token_secret.py:113
#: authentication/serializers/connect_token_secret.py:168 labels/models.py:11
#: ops/mixin.py:21 ops/models/adhoc.py:20 ops/models/celery.py:15
-#: ops/models/celery.py:80 ops/models/job.py:137 ops/models/playbook.py:28
+#: ops/models/celery.py:80 ops/models/job.py:138 ops/models/playbook.py:28
#: ops/serializers/job.py:18 orgs/models.py:82
#: perms/models/asset_permission.py:61 rbac/models/role.py:29
#: settings/models.py:33 settings/models.py:181 settings/serializers/msg.py:89
@@ -618,9 +640,9 @@ msgstr "密码规则"
#: terminal/models/component/terminal.py:85
#: terminal/models/virtualapp/provider.py:10
#: terminal/models/virtualapp/virtualapp.py:19 tickets/api/ticket.py:87
-#: users/forms/profile.py:33 users/models/group.py:13
-#: users/models/preference.py:11 users/models/user.py:800
-#: xpack/plugins/cloud/models.py:32 xpack/plugins/cloud/models.py:276
+#: users/forms/profile.py:32 users/models/group.py:13
+#: users/models/preference.py:11 users/models/user.py:814
+#: xpack/plugins/cloud/models.py:32 xpack/plugins/cloud/models.py:272
#: xpack/plugins/cloud/serializers/task.py:70
msgid "Name"
msgstr "名称"
@@ -635,7 +657,7 @@ msgstr "特权账号"
#: authentication/serializers/connect_token_secret.py:117
#: terminal/models/applet/applet.py:40
#: terminal/models/component/endpoint.py:120
-#: terminal/models/virtualapp/virtualapp.py:23 users/serializers/user.py:169
+#: terminal/models/virtualapp/virtualapp.py:23 users/serializers/user.py:173
msgid "Is active"
msgstr "激活"
@@ -651,22 +673,22 @@ msgstr "系统平台"
msgid "Push params"
msgstr "账号推送参数"
-#: accounts/models/template.py:26 xpack/plugins/cloud/models.py:333
+#: accounts/models/template.py:26 xpack/plugins/cloud/models.py:329
msgid "Account template"
-msgstr "账号模版"
+msgstr "账号模板"
#: accounts/models/template.py:31
msgid "Can view asset account template secret"
-msgstr "可以查看资产账号模版密码"
+msgstr "可以查看资产账号模板密码"
#: accounts/models/template.py:32
msgid "Can change asset account template secret"
-msgstr "可以更改资产账号模版密码"
+msgstr "可以更改资产账号模板密码"
# msgid "Can view asset account template secret"
-# msgstr "可以查看资产账号模版密码"
+# msgstr "可以查看资产账号模板密码"
# msgid "Can change asset account template secret"
-# msgstr "可以更改资产账号模版密码"
+# msgstr "可以更改资产账号模板密码"
#: accounts/models/virtual.py:13
msgid "Alias"
msgstr "别名"
@@ -694,17 +716,17 @@ msgid ""
msgstr ""
"连接资产时不使用用户名和密码的账号,仅支持 web类型 和 自定义类型 的资产"
-#: accounts/notifications.py:11 accounts/notifications.py:36
+#: accounts/notifications.py:12 accounts/notifications.py:37
msgid "Notification of account backup route task results"
msgstr "账号备份任务结果通知"
-#: accounts/notifications.py:21 accounts/notifications.py:45
+#: accounts/notifications.py:22 accounts/notifications.py:46
msgid ""
"{} - The account backup passage task has been completed. See the attachment "
"for details"
msgstr "{} - 账号备份任务已完成, 详情见附件"
-#: accounts/notifications.py:24
+#: accounts/notifications.py:25
msgid ""
"{} - The account backup passage task has been completed: the encryption "
"password has not been set - please go to personal information -> Basic file "
@@ -713,17 +735,17 @@ msgstr ""
"{} - 账号备份任务已完成: 未设置加密密码 - 请前往个人信息 -> 偏好设置的基本中"
"设置文件加密密码"
-#: accounts/notifications.py:55
+#: accounts/notifications.py:56
msgid "Notification of implementation result of encryption change plan"
msgstr "改密计划任务结果通知"
-#: accounts/notifications.py:66
+#: accounts/notifications.py:67
msgid ""
"{} - The encryption change task has been completed. See the attachment for "
"details"
msgstr "{} - 改密任务已完成, 详情见附件"
-#: accounts/notifications.py:70
+#: accounts/notifications.py:71
msgid ""
"{} - The encryption change task has been completed: the encryption password "
"has not been set - please go to personal information -> set encryption "
@@ -732,10 +754,15 @@ msgstr ""
"{} - 改密任务已完成: 未设置加密密码 - 请前往个人信息 -> 偏好设置中设置加密密"
"码"
-#: accounts/notifications.py:82
+#: accounts/notifications.py:83
+#: accounts/templates/accounts/asset_account_change_info.html:3
msgid "Gather account change information"
msgstr "账号变更信息"
+#: accounts/notifications.py:105
+msgid "Change secret or push account failed information"
+msgstr "改密或推送账号失败信息"
+
#: accounts/serializers/account/account.py:31
msgid "Push now"
msgstr "立即推送"
@@ -754,21 +781,21 @@ msgid "Category"
msgstr "类别"
#: accounts/serializers/account/account.py:194
-#: accounts/serializers/automations/base.py:54 acls/models/command_acl.py:24
+#: accounts/serializers/automations/base.py:55 acls/models/command_acl.py:24
#: acls/serializers/command_acl.py:19 applications/models.py:14
#: assets/models/_user.py:50 assets/models/automations/base.py:20
#: assets/models/cmd_filter.py:74 assets/models/platform.py:97
#: assets/serializers/asset/common.py:126 assets/serializers/platform.py:120
#: assets/serializers/platform.py:139 audits/serializers.py:53
#: audits/serializers.py:170
-#: authentication/serializers/connect_token_secret.py:126 ops/models/job.py:145
+#: authentication/serializers/connect_token_secret.py:126 ops/models/job.py:146
#: perms/serializers/user_permission.py:27 terminal/models/applet/applet.py:39
#: terminal/models/component/storage.py:57
#: terminal/models/component/storage.py:146 terminal/serializers/applet.py:29
#: terminal/serializers/session.py:23 terminal/serializers/storage.py:264
#: terminal/serializers/storage.py:276 tickets/models/comment.py:26
#: tickets/models/flow.py:56 tickets/models/ticket/apply_application.py:16
-#: tickets/models/ticket/general.py:275 tickets/serializers/flow.py:53
+#: tickets/models/ticket/general.py:273 tickets/serializers/flow.py:53
#: tickets/serializers/ticket/ticket.py:19
msgid "Type"
msgstr "类型"
@@ -782,9 +809,8 @@ msgid "Has secret"
msgstr "已托管密码"
#: accounts/serializers/account/account.py:259 ops/models/celery.py:83
-#: tickets/models/comment.py:13 tickets/models/ticket/general.py:45
-#: tickets/models/ticket/general.py:279 tickets/serializers/super_ticket.py:14
-#: tickets/serializers/ticket/ticket.py:21
+#: tickets/models/comment.py:13 tickets/models/ticket/general.py:46
+#: tickets/models/ticket/general.py:277 tickets/serializers/super_ticket.py:14
msgid "State"
msgstr "状态"
@@ -797,8 +823,8 @@ msgstr "已修改"
#: acls/templates/acls/asset_login_reminder.html:6
#: assets/models/automations/base.py:19
#: assets/serializers/automations/base.py:20
-#: authentication/api/connection_token.py:404 ops/models/base.py:17
-#: ops/models/job.py:147 ops/serializers/job.py:19
+#: authentication/api/connection_token.py:410 ops/models/base.py:17
+#: ops/models/job.py:148 ops/serializers/job.py:19
#: terminal/templates/terminal/_msg_command_execute_alert.html:16
msgid "Assets"
msgstr "资产"
@@ -834,13 +860,13 @@ msgstr "ID"
#: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:63
#: perms/serializers/permission.py:32 rbac/builtin.py:124
#: rbac/models/rolebinding.py:49 rbac/serializers/rolebinding.py:17
-#: terminal/backends/command/models.py:16 terminal/models/session/session.py:29
+#: terminal/backends/command/models.py:16 terminal/models/session/session.py:30
#: terminal/models/session/sharing.py:34 terminal/notifications.py:156
#: terminal/notifications.py:205 terminal/serializers/command.py:16
#: terminal/templates/terminal/_msg_command_warning.html:6
#: terminal/templates/terminal/_msg_session_sharing.html:6
-#: tickets/models/comment.py:21 users/const.py:14 users/models/user.py:1004
-#: users/models/user.py:1041 users/serializers/group.py:21
+#: tickets/models/comment.py:21 users/const.py:14 users/models/user.py:1019
+#: users/models/user.py:1057 users/serializers/group.py:21
msgid "User"
msgstr "用户"
@@ -851,19 +877,20 @@ msgid "Date"
msgstr "日期"
#: accounts/serializers/account/backup.py:38
-#: accounts/serializers/automations/base.py:36
+#: accounts/serializers/automations/base.py:24
+#: accounts/serializers/automations/base.py:37
#: assets/serializers/automations/base.py:34 ops/mixin.py:23 ops/mixin.py:104
#: settings/serializers/auth/ldap.py:66
msgid "Periodic perform"
msgstr "定时执行"
#: accounts/serializers/account/backup.py:39
-#: accounts/serializers/automations/base.py:37
+#: accounts/serializers/automations/base.py:38
msgid "Executed amount"
msgstr "执行次数"
#: accounts/serializers/account/backup.py:42
-#: accounts/serializers/automations/change_secret.py:57
+#: accounts/serializers/automations/change_secret.py:59
msgid "Currently only mail sending is supported"
msgstr "当前只支持邮件发送"
@@ -929,15 +956,15 @@ msgstr "关联平台,可配置推送参数,如果不关联,将使用默认
#: accounts/serializers/account/virtual.py:19 assets/models/_user.py:27
#: assets/models/cmd_filter.py:40 assets/models/cmd_filter.py:88
#: assets/models/group.py:20 common/db/models.py:36 ops/models/adhoc.py:26
-#: ops/models/job.py:153 ops/models/playbook.py:31 rbac/models/role.py:37
+#: ops/models/job.py:154 ops/models/playbook.py:31 rbac/models/role.py:37
#: settings/models.py:38 terminal/models/applet/applet.py:45
#: terminal/models/applet/applet.py:321 terminal/models/applet/host.py:143
#: terminal/models/component/endpoint.py:25
#: terminal/models/component/endpoint.py:119
-#: terminal/models/session/session.py:46
+#: terminal/models/session/session.py:47
#: terminal/models/virtualapp/virtualapp.py:28 tickets/models/comment.py:32
-#: tickets/models/ticket/general.py:297 users/models/user.py:836
-#: xpack/plugins/cloud/models.py:39 xpack/plugins/cloud/models.py:110
+#: tickets/models/ticket/general.py:295 users/models/user.py:850
+#: xpack/plugins/cloud/models.py:39 xpack/plugins/cloud/models.py:106
msgid "Comment"
msgstr "备注"
@@ -956,41 +983,33 @@ msgstr ""
msgid "Nodes"
msgstr "节点"
-#: accounts/serializers/automations/base.py:44
+#: accounts/serializers/automations/base.py:45
msgid "Name already exists"
msgstr "名称已存在"
-#: accounts/serializers/automations/base.py:53
+#: accounts/serializers/automations/base.py:54
#: assets/models/automations/base.py:118
#: assets/serializers/automations/base.py:39
msgid "Automation snapshot"
msgstr "自动化快照"
-#: accounts/serializers/automations/change_secret.py:42
+#: accounts/serializers/automations/change_secret.py:44
msgid "SSH Key strategy"
msgstr "SSH 密钥更改方式"
-#: accounts/serializers/automations/change_secret.py:79
+#: accounts/serializers/automations/change_secret.py:81
msgid "* Please enter the correct password length"
msgstr "* 请输入正确的密码长度"
-#: accounts/serializers/automations/change_secret.py:83
+#: accounts/serializers/automations/change_secret.py:85
msgid "* Password length range 6-30 bits"
msgstr "* 密码长度范围 6-30 位"
-#: accounts/serializers/automations/change_secret.py:109
+#: accounts/serializers/automations/change_secret.py:114
#: assets/models/automations/base.py:127
msgid "Automation task execution"
msgstr "自动化任务执行历史"
-#: accounts/serializers/automations/change_secret.py:149 audits/const.py:61
-#: audits/models.py:64 audits/signal_handlers/activity_log.py:33
-#: common/const/choices.py:18 ops/const.py:73 ops/serializers/celery.py:46
-#: terminal/const.py:78 terminal/models/session/sharing.py:121
-#: tickets/views/approve.py:128
-msgid "Success"
-msgstr "成功"
-
#: accounts/signal_handlers.py:47
#, python-format
msgid "Push related accounts to assets: %s, by system"
@@ -1010,7 +1029,7 @@ msgstr "删除账号: %s"
msgid "Account execute automation"
msgstr "账号执行自动化"
-#: accounts/tasks/automation.py:51 accounts/tasks/automation.py:62
+#: accounts/tasks/automation.py:51 accounts/tasks/automation.py:56
msgid "Execute automation record"
msgstr "自动化执行记录"
@@ -1058,6 +1077,27 @@ msgstr "新增账号"
msgid "Deleted account"
msgstr "删除账号"
+#: accounts/templates/accounts/change_secret_failed_info.html:3
+#: ops/templates/ops/celery_task_log.html:71 terminal/serializers/task.py:10
+msgid "Task name"
+msgstr "任务名称"
+
+#: accounts/templates/accounts/change_secret_failed_info.html:4
+msgid "Task execution id"
+msgstr "任务执行 ID"
+
+#: accounts/templates/accounts/change_secret_failed_info.html:5
+#: acls/templates/acls/asset_login_reminder.html:3
+#: acls/templates/acls/user_login_reminder.html:3
+msgid "Respectful"
+msgstr "尊敬的"
+
+#: accounts/templates/accounts/change_secret_failed_info.html:6
+msgid ""
+"Hello! The following is the failure of changing the password of your assets "
+"or pushing the account. Please check and handle it in time."
+msgstr "你好! 以下是资产改密或推送账户失败的情况。 请及时检查并处理。"
+
#: accounts/utils.py:52
msgid ""
"If the password starts with {{` and ends with }} `, then the password is not "
@@ -1072,7 +1112,7 @@ msgstr "密钥不合法或密钥密码错误"
msgid "Acls"
msgstr "访问控制"
-#: acls/const.py:6 audits/const.py:36 terminal/const.py:11 tickets/const.py:46
+#: acls/const.py:6 audits/const.py:36 terminal/const.py:11 tickets/const.py:44
#: tickets/templates/tickets/approve_check_password.html:47
msgid "Reject"
msgstr "拒绝"
@@ -1095,13 +1135,13 @@ msgstr "通知"
#: acls/models/base.py:37 assets/models/_user.py:51
#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:112
-#: xpack/plugins/cloud/models.py:282
+#: xpack/plugins/cloud/models.py:278
msgid "Priority"
msgstr "优先级"
#: acls/models/base.py:38 assets/models/_user.py:51
#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:113
-#: xpack/plugins/cloud/models.py:283
+#: xpack/plugins/cloud/models.py:279
msgid "1-100, the lower the value will be match first"
msgstr "优先级可选范围为 1-100 (数值越小越优先)"
@@ -1114,7 +1154,7 @@ msgstr "审批人"
#: authentication/models/connection_token.py:53
#: authentication/templates/authentication/_access_key_modal.html:32
#: perms/models/asset_permission.py:82 terminal/models/session/sharing.py:29
-#: tickets/const.py:38
+#: tickets/const.py:36
msgid "Active"
msgstr "激活中"
@@ -1130,7 +1170,7 @@ msgstr "账号管理"
#: acls/models/command_acl.py:16 assets/models/cmd_filter.py:60
#: ops/serializers/job.py:70 terminal/const.py:86
-#: terminal/models/session/session.py:42 terminal/serializers/command.py:18
+#: terminal/models/session/session.py:43 terminal/serializers/command.py:18
#: terminal/templates/terminal/_msg_command_alert.html:12
#: terminal/templates/terminal/_msg_command_execute_alert.html:10
#: terminal/templates/terminal/_msg_command_warning.html:23
@@ -1138,7 +1178,7 @@ msgid "Command"
msgstr "命令"
#: acls/models/command_acl.py:17 assets/models/cmd_filter.py:59
-#: xpack/plugins/cloud/models.py:299
+#: xpack/plugins/cloud/models.py:295
msgid "Regex"
msgstr "正则表达式"
@@ -1267,11 +1307,6 @@ msgstr "IP"
msgid "Time Period"
msgstr "时段"
-#: acls/templates/acls/asset_login_reminder.html:3
-#: acls/templates/acls/user_login_reminder.html:3
-msgid "Respectful"
-msgstr "尊敬的"
-
#: acls/templates/acls/asset_login_reminder.html:10
msgid ""
"The user has just logged in to the asset. Please ensure that this is an "
@@ -1312,7 +1347,7 @@ msgid "Applications"
msgstr "应用管理"
#: applications/models.py:16 xpack/plugins/cloud/models.py:37
-#: xpack/plugins/cloud/serializers/account.py:67
+#: xpack/plugins/cloud/serializers/account.py:68
msgid "Attrs"
msgstr "属性"
@@ -1328,7 +1363,7 @@ msgstr "匹配应用"
msgid "Cannot create asset directly, you should create a host or other"
msgstr "不能直接创建资产, 你应该创建主机或其他资产"
-#: assets/api/domain.py:64
+#: assets/api/domain.py:67
msgid "Number required"
msgstr "需要为数字"
@@ -1361,7 +1396,7 @@ msgid " - Platform {} ansible disabled"
msgstr " - 平台 {} Ansible 已禁用, 无法执行任务"
#: assets/automations/ping_gateway/manager.py:33
-#: authentication/models/connection_token.py:128
+#: authentication/models/connection_token.py:131
msgid "No account"
msgstr "没有账号"
@@ -1375,7 +1410,8 @@ msgid "Unable to connect to port {port} on {address}"
msgstr "无法连接到 {port} 上的端口 {address}"
#: assets/automations/ping_gateway/manager.py:58
-#: authentication/middleware.py:93 xpack/plugins/cloud/providers/fc.py:47
+#: authentication/backends/oauth2/views.py:60 authentication/middleware.py:93
+#: xpack/plugins/cloud/providers/fc.py:47
msgid "Authentication failed"
msgstr "认证失败"
@@ -1414,11 +1450,11 @@ msgstr "禁用"
#: assets/const/base.py:33 settings/serializers/basic.py:8
#: users/serializers/preference/koko.py:19
#: users/serializers/preference/lina.py:39
-#: users/serializers/preference/luna.py:73
+#: users/serializers/preference/luna.py:77
msgid "Basic"
msgstr "基本"
-#: assets/const/base.py:34 assets/const/protocol.py:252
+#: assets/const/base.py:34 assets/const/protocol.py:268
#: assets/models/asset/web.py:13
msgid "Script"
msgstr "脚本"
@@ -1441,7 +1477,7 @@ msgstr "云服务"
#: assets/const/category.py:14 assets/models/asset/gpt.py:11
#: assets/models/asset/web.py:16 audits/const.py:42
-#: terminal/models/applet/applet.py:27 users/const.py:59
+#: terminal/models/applet/applet.py:27 users/const.py:64
msgid "Web"
msgstr "Web"
@@ -1486,11 +1522,19 @@ msgstr "ChatGPT"
msgid "Other"
msgstr "其它"
-#: assets/const/protocol.py:49
+#: assets/const/protocol.py:45
+msgid "Old SSH version"
+msgstr "Old SSH version"
+
+#: assets/const/protocol.py:46
+msgid "Old SSH version like openssh 5.x or 6.x"
+msgstr "旧的 SSH 版本,例如 openssh 5.x 或 6.x"
+
+#: assets/const/protocol.py:57
msgid "SFTP root"
msgstr "SFTP 根路径"
-#: assets/const/protocol.py:51
+#: assets/const/protocol.py:59
#, python-brace-format
msgid ""
"SFTP root directory, Support variable:
- ${ACCOUNT} The connected "
@@ -1500,105 +1544,113 @@ msgstr ""
"SFTP根目录,支持变量:
-${ACCOUNT}已连接帐户用户名
-${HOME}连接帐户的主"
"目录
-${USER}用户的用户名"
-#: assets/const/protocol.py:66
+#: assets/const/protocol.py:74
msgid "Console"
msgstr "控制台"
-#: assets/const/protocol.py:67
+#: assets/const/protocol.py:75
msgid "Connect to console session"
msgstr "连接到控制台会话"
-#: assets/const/protocol.py:71
+#: assets/const/protocol.py:79
msgid "Any"
msgstr "任意"
-#: assets/const/protocol.py:73 settings/serializers/security.py:228
+#: assets/const/protocol.py:81 settings/serializers/security.py:232
msgid "Security"
msgstr "安全"
-#: assets/const/protocol.py:74
+#: assets/const/protocol.py:82
msgid "Security layer to use for the connection"
msgstr "连接 RDP 使用的安全层"
-#: assets/const/protocol.py:80
+#: assets/const/protocol.py:88
msgid "AD domain"
msgstr "AD 网域"
-#: assets/const/protocol.py:95
+#: assets/const/protocol.py:103
msgid "Username prompt"
msgstr "用户名提示"
-#: assets/const/protocol.py:96
+#: assets/const/protocol.py:104
msgid "We will send username when we see this prompt"
msgstr "当我们看到这个提示时,我们将发送用户名"
-#: assets/const/protocol.py:101
+#: assets/const/protocol.py:109
msgid "Password prompt"
msgstr "密码提示"
-#: assets/const/protocol.py:102
+#: assets/const/protocol.py:110
msgid "We will send password when we see this prompt"
msgstr "当我们看到这个提示时,我们将发送密码"
-#: assets/const/protocol.py:107
+#: assets/const/protocol.py:115
msgid "Success prompt"
msgstr "成功提示"
-#: assets/const/protocol.py:108
+#: assets/const/protocol.py:116
msgid "We will consider login success when we see this prompt"
msgstr "当我们看到这个提示时,我们将认为登录成功"
-#: assets/const/protocol.py:119 assets/models/asset/database.py:10
+#: assets/const/protocol.py:127 assets/models/asset/database.py:10
#: settings/serializers/msg.py:47
msgid "Use SSL"
msgstr "使用 SSL"
-#: assets/const/protocol.py:154
+#: assets/const/protocol.py:162
msgid "SYSDBA"
msgstr "SYSDBA"
-#: assets/const/protocol.py:155
+#: assets/const/protocol.py:163
msgid "Connect as SYSDBA"
msgstr "以 SYSDBA 角色连接"
-#: assets/const/protocol.py:170
+#: assets/const/protocol.py:178
msgid ""
"SQL Server version, Different versions have different connection drivers"
msgstr "SQL Server 版本,不同版本有不同的连接驱动"
-#: assets/const/protocol.py:199
+#: assets/const/protocol.py:202
+msgid "Auth source"
+msgstr "认证数据库"
+
+#: assets/const/protocol.py:203
+msgid "The database to authenticate against"
+msgstr "要进行身份验证的数据库"
+
+#: assets/const/protocol.py:215
msgid "Auth username"
msgstr "使用用户名认证"
-#: assets/const/protocol.py:222
+#: assets/const/protocol.py:238
msgid "Safe mode"
msgstr "安全模式"
-#: assets/const/protocol.py:224
+#: assets/const/protocol.py:240
msgid ""
"When safe mode is enabled, some operations will be disabled, such as: New "
"tab, right click, visit other website, etc."
msgstr ""
"当安全模式启用时,一些操作将被禁用,例如:新建标签页、右键、访问其它网站 等"
-#: assets/const/protocol.py:229 assets/models/asset/web.py:9
+#: assets/const/protocol.py:245 assets/models/asset/web.py:9
#: assets/serializers/asset/info/spec.py:16
msgid "Autofill"
msgstr "自动代填"
-#: assets/const/protocol.py:237 assets/models/asset/web.py:10
+#: assets/const/protocol.py:253 assets/models/asset/web.py:10
msgid "Username selector"
msgstr "用户名选择器"
-#: assets/const/protocol.py:242 assets/models/asset/web.py:11
+#: assets/const/protocol.py:258 assets/models/asset/web.py:11
msgid "Password selector"
msgstr "密码选择器"
-#: assets/const/protocol.py:247 assets/models/asset/web.py:12
+#: assets/const/protocol.py:263 assets/models/asset/web.py:12
msgid "Submit selector"
msgstr "确认按钮选择器"
-#: assets/const/protocol.py:270
+#: assets/const/protocol.py:286
msgid "API mode"
msgstr "API 模式"
@@ -1627,18 +1679,18 @@ msgstr "SSH公钥"
#: assets/models/_user.py:28 assets/models/automations/base.py:114
#: assets/models/cmd_filter.py:41 assets/models/group.py:19
#: audits/models.py:267 common/db/models.py:34 ops/models/base.py:54
-#: ops/models/job.py:235 users/models/user.py:1042
+#: ops/models/job.py:236 users/models/user.py:1058
msgid "Date created"
msgstr "创建日期"
#: assets/models/_user.py:29 assets/models/cmd_filter.py:42
-#: common/db/models.py:35 users/models/user.py:854
+#: common/db/models.py:35 users/models/user.py:868
msgid "Date updated"
msgstr "更新日期"
#: assets/models/_user.py:30 assets/models/cmd_filter.py:44
#: assets/models/cmd_filter.py:91 assets/models/group.py:18
-#: common/db/models.py:32 users/models/user.py:843
+#: common/db/models.py:32 users/models/user.py:857
#: users/serializers/group.py:32
msgid "Created by"
msgstr "创建者"
@@ -1667,7 +1719,7 @@ msgstr "用户名与用户相同"
#: authentication/serializers/connect_token_secret.py:114
#: settings/serializers/msg.py:29 terminal/models/applet/applet.py:42
#: terminal/models/virtualapp/virtualapp.py:24
-#: terminal/serializers/session.py:21 terminal/serializers/session.py:47
+#: terminal/serializers/session.py:21 terminal/serializers/session.py:48
#: terminal/serializers/storage.py:71
msgid "Protocol"
msgstr "协议"
@@ -1676,7 +1728,7 @@ msgstr "协议"
msgid "Sudo"
msgstr "Sudo"
-#: assets/models/_user.py:55 ops/const.py:50 ops/const.py:60
+#: assets/models/_user.py:55 ops/const.py:50 ops/const.py:61
msgid "Shell"
msgstr "Shell"
@@ -1730,20 +1782,20 @@ msgstr "地址"
#: assets/models/asset/common.py:161 assets/models/platform.py:126
#: authentication/backends/passkey/models.py:12
#: authentication/serializers/connect_token_secret.py:118
-#: perms/serializers/user_permission.py:25 xpack/plugins/cloud/models.py:329
+#: perms/serializers/user_permission.py:25 xpack/plugins/cloud/models.py:325
msgid "Platform"
msgstr "系统平台"
#: assets/models/asset/common.py:163 assets/models/domain.py:22
#: authentication/serializers/connect_token_secret.py:136
-#: perms/serializers/user_permission.py:28 xpack/plugins/cloud/models.py:331
+#: perms/serializers/user_permission.py:28 xpack/plugins/cloud/models.py:327
msgid "Domain"
msgstr "网域"
#: assets/models/asset/common.py:165 assets/models/automations/base.py:18
-#: assets/models/cmd_filter.py:32 assets/models/node.py:549
+#: assets/models/cmd_filter.py:32 assets/models/node.py:553
#: perms/models/asset_permission.py:72 perms/serializers/permission.py:37
-#: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:330
+#: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:326
msgid "Node"
msgstr "节点"
@@ -1796,7 +1848,7 @@ msgstr "忽略证书校验"
msgid "Proxy"
msgstr "代理"
-#: assets/models/automations/base.py:22 ops/models/job.py:231
+#: assets/models/automations/base.py:22 ops/models/job.py:232
#: settings/serializers/auth/sms.py:103
msgid "Parameters"
msgstr "参数"
@@ -1827,7 +1879,7 @@ msgstr "校验日期"
#: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:66
#: perms/serializers/permission.py:34 users/models/group.py:25
-#: users/models/user.py:806
+#: users/models/user.py:820
msgid "User group"
msgstr "用户组"
@@ -1859,7 +1911,7 @@ msgstr "命令过滤规则"
msgid "Favorite asset"
msgstr "收藏的资产"
-#: assets/models/gateway.py:34 assets/serializers/domain.py:18
+#: assets/models/gateway.py:34 assets/serializers/domain.py:19
msgid "Gateway"
msgstr "网关"
@@ -1877,11 +1929,11 @@ msgstr "默认"
msgid "Default asset group"
msgstr "默认资产组"
-#: assets/models/label.py:15 rbac/const.py:6 users/models/user.py:1027
+#: assets/models/label.py:15 rbac/const.py:6 users/models/user.py:1043
msgid "System"
msgstr "系统"
-#: assets/models/label.py:19 assets/models/node.py:535
+#: assets/models/label.py:19 assets/models/node.py:539
#: assets/serializers/cagegory.py:11 assets/serializers/cagegory.py:18
#: assets/serializers/cagegory.py:24
#: authentication/models/connection_token.py:29
@@ -1900,27 +1952,27 @@ msgstr "值"
msgid "Label"
msgstr "标签"
-#: assets/models/node.py:165
+#: assets/models/node.py:169
msgid "New node"
msgstr "新节点"
-#: assets/models/node.py:463 audits/backends/db.py:65 audits/backends/db.py:66
+#: assets/models/node.py:467 audits/backends/db.py:65 audits/backends/db.py:66
msgid "empty"
msgstr "空"
-#: assets/models/node.py:534 perms/models/perm_node.py:28
+#: assets/models/node.py:538 perms/models/perm_node.py:28
msgid "Key"
msgstr "键"
-#: assets/models/node.py:536 assets/serializers/node.py:20
+#: assets/models/node.py:540 assets/serializers/node.py:20
msgid "Full value"
msgstr "全称"
-#: assets/models/node.py:540 perms/models/perm_node.py:30
+#: assets/models/node.py:544 perms/models/perm_node.py:30
msgid "Parent key"
msgstr "ssh私钥"
-#: assets/models/node.py:552
+#: assets/models/node.py:556
msgid "Can match node"
msgstr "可以匹配节点"
@@ -1937,7 +1989,7 @@ msgid "Public"
msgstr "开放的"
#: assets/models/platform.py:22 assets/serializers/platform.py:49
-#: settings/serializers/settings.py:66
+#: settings/serializers/settings.py:95
#: users/templates/users/reset_password.html:29
msgid "Setting"
msgstr "设置"
@@ -2026,7 +2078,7 @@ msgstr "账号移除方式"
msgid "Remove account params"
msgstr "账号移除参数"
-#: assets/models/platform.py:98 tickets/models/ticket/general.py:300
+#: assets/models/platform.py:98 tickets/models/ticket/general.py:298
msgid "Meta"
msgstr "元数据"
@@ -2069,7 +2121,7 @@ msgstr "资产中批量更新平台,不符合平台类型跳过的资产"
#: authentication/serializers/connect_token_secret.py:30
#: authentication/serializers/connect_token_secret.py:75
#: perms/models/asset_permission.py:76 perms/serializers/permission.py:42
-#: perms/serializers/user_permission.py:74 xpack/plugins/cloud/models.py:332
+#: perms/serializers/user_permission.py:74 xpack/plugins/cloud/models.py:328
#: xpack/plugins/cloud/serializers/task.py:33
msgid "Protocols"
msgstr "协议组"
@@ -2129,7 +2181,7 @@ msgid "Model"
msgstr "型号"
#: assets/serializers/asset/info/gathered.py:8
-#: tickets/models/ticket/general.py:299
+#: tickets/models/ticket/general.py:297
msgid "Serial number"
msgstr "序列号"
@@ -2178,7 +2230,7 @@ msgstr "约束"
msgid "Types"
msgstr "类型"
-#: assets/serializers/domain.py:53 perms/serializers/permission.py:188
+#: assets/serializers/domain.py:62 perms/serializers/permission.py:188
msgid "Assets amount"
msgstr "资产数量"
@@ -2380,7 +2432,7 @@ msgstr "连接"
#: audits/const.py:30 authentication/templates/authentication/login.html:296
#: authentication/templates/authentication/login.html:369
-#: templates/_header_bar.html:95
+#: templates/_header_bar.html:101
msgid "Login"
msgstr "登录"
@@ -2388,21 +2440,21 @@ msgstr "登录"
msgid "Change password"
msgstr "改密"
-#: audits/const.py:37 tickets/const.py:47
+#: audits/const.py:37 tickets/const.py:45
msgid "Approve"
msgstr "同意"
#: audits/const.py:38
#: authentication/templates/authentication/_access_key_modal.html:155
#: authentication/templates/authentication/_mfa_confirm_modal.html:53
-#: templates/_modal.html:22 tickets/const.py:45
+#: templates/_modal.html:22 tickets/const.py:43
msgid "Close"
msgstr "关闭"
#: audits/const.py:43 settings/serializers/terminal.py:6
#: terminal/models/applet/host.py:26 terminal/models/component/terminal.py:175
-#: terminal/models/virtualapp/provider.py:14 terminal/serializers/session.py:54
-#: terminal/serializers/session.py:68
+#: terminal/models/virtualapp/provider.py:14 terminal/serializers/session.py:55
+#: terminal/serializers/session.py:69
msgid "Terminal"
msgstr "终端"
@@ -2427,11 +2479,11 @@ msgstr "任务"
msgid "-"
msgstr "-"
-#: audits/handler.py:117
+#: audits/handler.py:116
msgid "Yes"
msgstr "是"
-#: audits/handler.py:117
+#: audits/handler.py:116
msgid "No"
msgstr "否"
@@ -2440,7 +2492,7 @@ msgid "Job audit log"
msgstr "作业审计日志"
#: audits/models.py:56 audits/models.py:100 audits/models.py:175
-#: terminal/models/session/session.py:38 terminal/models/session/sharing.py:113
+#: terminal/models/session/session.py:39 terminal/models/session/sharing.py:113
msgid "Remote addr"
msgstr "远端地址"
@@ -2518,7 +2570,7 @@ msgstr "登录 IP"
#: audits/models.py:200 audits/serializers.py:52
#: authentication/templates/authentication/_mfa_confirm_modal.html:14
-#: users/forms/profile.py:65 users/models/user.py:823
+#: users/forms/profile.py:63 users/models/user.py:837
#: users/serializers/profile.py:102
msgid "MFA"
msgstr "MFA"
@@ -2540,17 +2592,18 @@ msgstr "用户登录日志"
msgid "Session key"
msgstr "会话标识"
-#: audits/models.py:305
+#: audits/models.py:298
msgid "User session"
msgstr "用户会话"
-#: audits/models.py:307
+#: audits/models.py:300
msgid "Offline user session"
msgstr "下线用户会话"
#: audits/serializers.py:33 ops/models/adhoc.py:25 ops/models/base.py:16
-#: ops/models/base.py:53 ops/models/job.py:146 ops/models/job.py:234
-#: ops/models/playbook.py:30 terminal/models/session/sharing.py:25
+#: ops/models/base.py:53 ops/models/celery.py:86 ops/models/job.py:147
+#: ops/models/job.py:235 ops/models/playbook.py:30
+#: terminal/models/session/sharing.py:25
msgid "Creator"
msgstr "创建者"
@@ -2566,7 +2619,7 @@ msgstr "用户 %s %s 了当前资源"
#: audits/serializers.py:172 authentication/models/connection_token.py:47
#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:80
#: tickets/models/ticket/apply_application.py:31
-#: tickets/models/ticket/apply_asset.py:20 users/models/user.py:841
+#: tickets/models/ticket/apply_asset.py:20 users/models/user.py:855
msgid "Date expired"
msgstr "失效日期"
@@ -2599,48 +2652,47 @@ msgstr "认证令牌"
#: audits/signal_handlers/login_log.py:37 authentication/notifications.py:73
#: authentication/views/login.py:77 notifications/backends/__init__.py:11
-#: settings/serializers/auth/wecom.py:10 users/models/user.py:749
-#: users/models/user.py:855
+#: settings/serializers/auth/wecom.py:10 users/models/user.py:759
+#: users/models/user.py:869
msgid "WeCom"
msgstr "企业微信"
-#: audits/signal_handlers/login_log.py:38 authentication/views/feishu.py:87
+#: audits/signal_handlers/login_log.py:38 authentication/views/feishu.py:105
#: authentication/views/login.py:89 notifications/backends/__init__.py:14
-#: settings/serializers/auth/feishu.py:10
-#: settings/serializers/auth/feishu.py:13 users/models/user.py:751
-#: users/models/user.py:857
+#: settings/serializers/auth/feishu.py:10 users/models/user.py:761
+#: users/models/user.py:871
msgid "FeiShu"
msgstr "飞书"
-#: audits/signal_handlers/login_log.py:39 authentication/views/login.py:95
-#: authentication/views/slack.py:87 notifications/backends/__init__.py:15
-#: settings/serializers/auth/slack.py:10 users/models/user.py:752
-#: users/models/user.py:858
+#: audits/signal_handlers/login_log.py:40 authentication/views/login.py:101
+#: authentication/views/slack.py:87 notifications/backends/__init__.py:16
+#: settings/serializers/auth/slack.py:10 users/models/user.py:763
+#: users/models/user.py:873
msgid "Slack"
msgstr ""
-#: audits/signal_handlers/login_log.py:40 authentication/views/dingtalk.py:161
+#: audits/signal_handlers/login_log.py:41 authentication/views/dingtalk.py:161
#: authentication/views/login.py:83 notifications/backends/__init__.py:12
-#: settings/serializers/auth/dingtalk.py:10 users/models/user.py:750
-#: users/models/user.py:856
+#: settings/serializers/auth/dingtalk.py:10 users/models/user.py:760
+#: users/models/user.py:870
msgid "DingTalk"
msgstr "钉钉"
-#: audits/signal_handlers/login_log.py:41
+#: audits/signal_handlers/login_log.py:42
#: authentication/models/temp_token.py:16
msgid "Temporary token"
msgstr "临时密码"
-#: audits/signal_handlers/login_log.py:42 authentication/views/login.py:101
+#: audits/signal_handlers/login_log.py:43 authentication/views/login.py:107
#: settings/serializers/auth/passkey.py:8
msgid "Passkey"
msgstr "Passkey"
-#: audits/tasks.py:109
+#: audits/tasks.py:117
msgid "Clean audits session task log"
msgstr "清理资产审计会话任务日志"
-#: audits/tasks.py:123
+#: audits/tasks.py:130
msgid "Upload FTP file to external storage"
msgstr "上传 FTP 文件到外部存储"
@@ -2657,27 +2709,27 @@ msgstr "参数中的值必须包含 %s"
msgid "This action require verify your MFA"
msgstr "该操作需要验证您的 MFA, 请先开启并配置"
-#: authentication/api/connection_token.py:260
+#: authentication/api/connection_token.py:265
msgid "Reusable connection token is not allowed, global setting not enabled"
msgstr "不允许使用可重复使用的连接令牌,未启用全局设置"
-#: authentication/api/connection_token.py:374
+#: authentication/api/connection_token.py:379
msgid "Anonymous account is not supported for this asset"
msgstr "匿名账号不支持当前资产"
-#: authentication/api/connection_token.py:393
+#: authentication/api/connection_token.py:399
msgid "Account not found"
msgstr "账号未找到"
-#: authentication/api/connection_token.py:396
+#: authentication/api/connection_token.py:402
msgid "Permission expired"
msgstr "授权已过期"
-#: authentication/api/connection_token.py:426
+#: authentication/api/connection_token.py:435
msgid "ACL action is reject: {}({})"
msgstr "ACL 动作是拒绝: {}({})"
-#: authentication/api/connection_token.py:430
+#: authentication/api/connection_token.py:439
msgid "ACL action is review"
msgstr "ACL 动作是复核"
@@ -2690,7 +2742,7 @@ msgstr "当前用户不支持 MFA 类型: {}"
msgid "User does not exist: {}"
msgstr "用户不存在: {}"
-#: authentication/api/password.py:33 users/views/profile/reset.py:164
+#: authentication/api/password.py:33 users/views/profile/reset.py:166
msgid "No user matched"
msgstr "没有匹配到用户"
@@ -2871,19 +2923,19 @@ msgstr "等待登录复核处理"
msgid "Login confirm ticket was {}"
msgstr "登录复核: {}"
-#: authentication/errors/failed.py:145
+#: authentication/errors/failed.py:149
msgid "Current IP and Time period is not allowed"
msgstr "当前 IP 和时间段不被允许登录"
-#: authentication/errors/failed.py:150
+#: authentication/errors/failed.py:154
msgid "Please enter MFA code"
msgstr "请输入 MFA 验证码"
-#: authentication/errors/failed.py:155
+#: authentication/errors/failed.py:159
msgid "Please enter SMS code"
msgstr "请输入短信验证码"
-#: authentication/errors/failed.py:160 users/exceptions.py:15
+#: authentication/errors/failed.py:164 users/exceptions.py:15
msgid "Phone not set"
msgstr "手机号没有设置"
@@ -2905,19 +2957,23 @@ msgstr "没有绑定企业微信"
msgid "DingTalk is not bound"
msgstr "钉钉没有绑定"
-#: authentication/errors/mfa.py:33 authentication/views/feishu.py:128
+#: authentication/errors/mfa.py:33 authentication/views/feishu.py:138
msgid "FeiShu is not bound"
msgstr "没有绑定飞书"
-#: authentication/errors/mfa.py:38 authentication/views/slack.py:127
-msgid "Slack is not bound"
-msgstr "Slack没有绑定"
+#: authentication/errors/mfa.py:38 authentication/views/lark.py:48
+msgid "Lark is not bound"
+msgstr "Lark 没有绑定"
-#: authentication/errors/mfa.py:43
+#: authentication/errors/mfa.py:43 authentication/views/slack.py:127
+msgid "Slack is not bound"
+msgstr "Slack 没有绑定"
+
+#: authentication/errors/mfa.py:48
msgid "Your password is invalid"
msgstr "您的密码无效"
-#: authentication/errors/mfa.py:48
+#: authentication/errors/mfa.py:53
#, python-format
msgid "Please wait for %s seconds before retry"
msgstr "请在 %s 秒后重试"
@@ -2934,28 +2990,28 @@ msgstr "登录完成前,请先修改密码"
msgid "Your password has expired, please reset before logging in"
msgstr "您的密码已过期,先修改再登录"
-#: authentication/forms.py:45
-msgid "{} days auto login"
-msgstr "{} 天内自动登录"
+#: authentication/forms.py:39
+msgid "Auto login"
+msgstr "自动登录"
-#: authentication/forms.py:56
+#: authentication/forms.py:52
msgid "MFA Code"
msgstr "MFA 验证码"
-#: authentication/forms.py:57
+#: authentication/forms.py:53
msgid "MFA type"
msgstr "MFA 类型"
-#: authentication/forms.py:65
+#: authentication/forms.py:61
#: authentication/templates/authentication/_captcha_field.html:15
msgid "Captcha"
msgstr "验证码"
-#: authentication/forms.py:70 users/forms/profile.py:28
+#: authentication/forms.py:66 users/forms/profile.py:27
msgid "MFA code"
msgstr "MFA 验证码"
-#: authentication/forms.py:72
+#: authentication/forms.py:68
msgid "Dynamic code"
msgstr "动态码"
@@ -3009,8 +3065,8 @@ msgstr "短信验证码校验失败"
#: authentication/mfa/sms.py:12 authentication/serializers/password_mfa.py:16
#: authentication/serializers/password_mfa.py:24
-#: settings/serializers/auth/sms.py:32 users/forms/profile.py:104
-#: users/forms/profile.py:109 users/templates/users/forgot_password.html:155
+#: settings/serializers/auth/sms.py:32 users/forms/profile.py:103
+#: users/forms/profile.py:108 users/templates/users/forgot_password.html:157
#: users/views/profile/reset.py:100
msgid "SMS"
msgstr "短信"
@@ -3105,27 +3161,27 @@ msgstr "可以复用连接令牌"
msgid "Connection token"
msgstr "连接令牌"
-#: authentication/models/connection_token.py:115
+#: authentication/models/connection_token.py:118
msgid "Connection token inactive"
msgstr "连接令牌未激活"
-#: authentication/models/connection_token.py:119
+#: authentication/models/connection_token.py:122
msgid "Connection token expired at: {}"
msgstr "连接令牌过期: {}"
-#: authentication/models/connection_token.py:122
+#: authentication/models/connection_token.py:125
msgid "No user or invalid user"
msgstr "没有用户或用户失效"
-#: authentication/models/connection_token.py:125
+#: authentication/models/connection_token.py:128
msgid "No asset or inactive asset"
msgstr "没有资产或资产未激活"
-#: authentication/models/connection_token.py:269
+#: authentication/models/connection_token.py:272
msgid "Can view super connection token secret"
msgstr "可以查看超级连接令牌密文"
-#: authentication/models/connection_token.py:271
+#: authentication/models/connection_token.py:274
msgid "Super connection token"
msgstr "超级连接令牌"
@@ -3202,12 +3258,12 @@ msgstr "动作"
#: authentication/serializers/connection_token.py:42
#: perms/serializers/permission.py:40 perms/serializers/permission.py:60
-#: users/serializers/user.py:97 users/serializers/user.py:173
+#: users/serializers/user.py:100 users/serializers/user.py:177
msgid "Is expired"
msgstr "已过期"
#: authentication/serializers/password_mfa.py:29
-#: users/templates/users/forgot_password.html:151
+#: users/templates/users/forgot_password.html:153
msgid "The {} cannot be empty"
msgstr "{} 不能为空"
@@ -3216,8 +3272,8 @@ msgid "Access IP"
msgstr "IP 白名单"
#: authentication/serializers/token.py:92 perms/serializers/permission.py:39
-#: perms/serializers/permission.py:61 users/serializers/user.py:98
-#: users/serializers/user.py:170
+#: perms/serializers/permission.py:61 users/serializers/user.py:101
+#: users/serializers/user.py:174
msgid "Is valid"
msgstr "是否有效"
@@ -3242,13 +3298,13 @@ msgid "Show"
msgstr "显示"
#: authentication/templates/authentication/_access_key_modal.html:66
-#: users/const.py:37 users/models/user.py:644 users/serializers/profile.py:92
+#: users/const.py:42 users/models/user.py:654 users/serializers/profile.py:92
#: users/templates/users/user_verify_mfa.html:36
msgid "Disable"
msgstr "禁用"
#: authentication/templates/authentication/_access_key_modal.html:67
-#: users/const.py:38 users/models/user.py:645 users/serializers/profile.py:93
+#: users/const.py:43 users/models/user.py:655 users/serializers/profile.py:93
#: users/templates/users/mfa_setting.html:26
#: users/templates/users/mfa_setting.html:68
msgid "Enable"
@@ -3287,7 +3343,7 @@ msgstr "代码错误"
#: authentication/templates/authentication/_msg_reset_password_code.html:9
#: authentication/templates/authentication/_msg_rest_password_success.html:2
#: authentication/templates/authentication/_msg_rest_public_key_success.html:2
-#: jumpserver/conf.py:460
+#: jumpserver/conf.py:465
#: perms/templates/perms/_msg_item_permissions_expire.html:3
#: perms/templates/perms/_msg_permed_items_expire.html:3
#: tickets/templates/tickets/approve_check_password.html:32
@@ -3343,7 +3399,7 @@ msgstr "重新申请"
#: authentication/templates/authentication/_msg_reset_password_code.html:12
#: terminal/models/session/sharing.py:27 terminal/models/session/sharing.py:97
#: terminal/templates/terminal/_msg_session_sharing.html:12
-#: users/forms/profile.py:107 users/templates/users/forgot_password.html:97
+#: users/forms/profile.py:106 users/templates/users/forgot_password.html:98
msgid "Verify code"
msgstr "验证码"
@@ -3448,7 +3504,7 @@ msgid "Do you want to retry ?"
msgstr "是否重试 ?"
#: authentication/utils.py:24 common/utils/ip/geoip/utils.py:24
-#: xpack/plugins/cloud/const.py:32
+#: xpack/plugins/cloud/const.py:33
msgid "LAN"
msgstr "局域网"
@@ -3457,17 +3513,17 @@ msgstr "局域网"
msgid "If you have any question, please contact the administrator"
msgstr "如果有疑问或需求,请联系系统管理员"
-#: authentication/views/base.py:138
+#: authentication/views/base.py:146
#, python-format
msgid "%s query user failed"
msgstr "%s 查询用户失败"
-#: authentication/views/base.py:147
+#: authentication/views/base.py:155
#, python-format
msgid "The %s is already bound to another user"
msgstr "%s 已绑定到另一个用户"
-#: authentication/views/base.py:154
+#: authentication/views/base.py:162
#, python-format
msgid "Binding %s successfully"
msgstr "绑定 %s 成功"
@@ -3480,7 +3536,7 @@ msgstr "钉钉错误,请联系系统管理员"
msgid "DingTalk Error"
msgstr "钉钉错误"
-#: authentication/views/dingtalk.py:57 authentication/views/feishu.py:47
+#: authentication/views/dingtalk.py:57 authentication/views/feishu.py:68
#: authentication/views/slack.py:47 authentication/views/wecom.py:55
msgid ""
"The system configuration is incorrect. Please contact your administrator"
@@ -3514,35 +3570,47 @@ msgstr "从钉钉获取用户失败"
msgid "Please login with a password and then bind the DingTalk"
msgstr "请使用密码登录,然后绑定钉钉"
-#: authentication/views/feishu.py:35 authentication/views/feishu.py:127
+#: authentication/views/feishu.py:43 authentication/views/feishu.py:137
msgid "FeiShu Error"
msgstr "飞书错误"
-#: authentication/views/feishu.py:63
+#: authentication/views/feishu.py:44
msgid "FeiShu is already bound"
msgstr "飞书已经绑定"
-#: authentication/views/feishu.py:129
+#: authentication/views/feishu.py:139
msgid "Failed to get user from FeiShu"
msgstr "从飞书获取用户失败"
-#: authentication/views/login.py:217
+#: authentication/views/lark.py:19 authentication/views/lark.py:47
+msgid "Lark Error"
+msgstr "Lark 错误"
+
+#: authentication/views/lark.py:20
+msgid "Lark is already bound"
+msgstr "Lark 已经绑定"
+
+#: authentication/views/lark.py:49
+msgid "Failed to get user from Lark"
+msgstr "从 Lark 获取用户失败"
+
+#: authentication/views/login.py:227
msgid "Redirecting"
msgstr "跳转中"
-#: authentication/views/login.py:218
+#: authentication/views/login.py:228
msgid "Redirecting to {} authentication"
msgstr "正在跳转到 {} 认证"
-#: authentication/views/login.py:241
+#: authentication/views/login.py:251
msgid "Login timeout, please try again."
msgstr "登录超时,请重新登录"
-#: authentication/views/login.py:284
+#: authentication/views/login.py:294
msgid "User email already exists ({})"
msgstr "用户邮箱已存在 ({})"
-#: authentication/views/login.py:362
+#: authentication/views/login.py:372
msgid ""
"Wait for {} confirm, You also can copy link to her/him
\n"
" Don't close this page"
@@ -3550,15 +3618,15 @@ msgstr ""
"等待 {} 确认, 你也可以复制链接发给他/她
\n"
" 不要关闭本页面"
-#: authentication/views/login.py:367
+#: authentication/views/login.py:377
msgid "No ticket found"
msgstr "没有发现工单"
-#: authentication/views/login.py:403
+#: authentication/views/login.py:413
msgid "Logout success"
msgstr "退出登录成功"
-#: authentication/views/login.py:404
+#: authentication/views/login.py:414
msgid "Logout success, return login page"
msgstr "退出登录成功,返回到登录页面"
@@ -3610,12 +3678,7 @@ msgstr "定时触发"
msgid "Ready"
msgstr "准备"
-#: common/const/choices.py:16 terminal/const.py:77 tickets/const.py:30
-#: tickets/const.py:40
-msgid "Pending"
-msgstr "待定的"
-
-#: common/const/choices.py:17 ops/const.py:72
+#: common/const/choices.py:17 ops/const.py:73
msgid "Running"
msgstr "运行中"
@@ -3665,7 +3728,7 @@ msgstr "编码数据为 text"
msgid "Encrypt field using Secret Key"
msgstr "加密的字段"
-#: common/db/fields.py:580
+#: common/db/fields.py:582
msgid ""
"Invalid JSON data for JSONManyToManyField, should be like {'type': 'all'} or "
"{'type': 'ids', 'ids': []} or {'type': 'attrs', 'attrs': [{'name': 'ip', "
@@ -3675,15 +3738,15 @@ msgstr ""
"{'type': 'attrs', 'attrs': [{'name': 'ip', 'match': 'exact', 'value': "
"'1.1.1.1'}}"
-#: common/db/fields.py:587
+#: common/db/fields.py:589
msgid "Invalid type, should be \"all\", \"ids\" or \"attrs\""
msgstr "无效类型,应为 all、ids 或 attrs"
-#: common/db/fields.py:590
+#: common/db/fields.py:592
msgid "Invalid ids for ids, should be a list"
msgstr "无效的ID,应为列表"
-#: common/db/fields.py:592 common/db/fields.py:597
+#: common/db/fields.py:594 common/db/fields.py:599
#: common/serializers/fields.py:133 tickets/serializers/ticket/common.py:58
#: xpack/plugins/cloud/serializers/account_attrs.py:56
#: xpack/plugins/cloud/serializers/account_attrs.py:79
@@ -3691,11 +3754,11 @@ msgstr "无效的ID,应为列表"
msgid "This field is required."
msgstr "该字段是必填项。"
-#: common/db/fields.py:595 common/db/fields.py:600
+#: common/db/fields.py:597 common/db/fields.py:602
msgid "Invalid attrs, should be a list of dict"
msgstr "无效的属性,应为dict列表"
-#: common/db/fields.py:602
+#: common/db/fields.py:604
msgid "Invalid attrs, should be has name and value"
msgstr "无效属性,应具有名称和值"
@@ -3707,7 +3770,7 @@ msgstr "忽略的"
msgid "discard time"
msgstr "忽略时间"
-#: common/db/models.py:33 users/models/user.py:844
+#: common/db/models.py:33 users/models/user.py:858
msgid "Updated by"
msgstr "最后更新者"
@@ -3735,7 +3798,7 @@ msgstr "解析文件错误: {}"
msgid "Invalid excel file"
msgstr "无效的 excel 文件"
-#: common/drf/renders/base.py:207
+#: common/drf/renders/base.py:208
msgid ""
"{} - The encryption password has not been set - please go to personal "
"information -> file encryption password to set the encryption password"
@@ -3837,7 +3900,7 @@ msgstr "短信服务商不支持:{}"
#: common/sdk/sms/endpoint.py:54
msgid "SMS verification code signature or template invalid"
-msgstr "短信验证码签名或模版无效"
+msgstr "短信验证码签名或模板无效"
#: common/sdk/sms/exceptions.py:8
msgid "The verification code has expired. Please resend it"
@@ -3887,7 +3950,7 @@ msgstr "发件邮件"
msgid "Send email attachment"
msgstr "发送邮件附件"
-#: common/tasks.py:80 terminal/tasks.py:62
+#: common/tasks.py:80 terminal/tasks.py:58
msgid "Upload session replay to external storage"
msgstr "上传会话录像到外部存储"
@@ -3916,20 +3979,20 @@ msgstr "不能包含特殊字符"
msgid "The mobile phone number format is incorrect"
msgstr "手机号格式不正确"
-#: jumpserver/conf.py:454
+#: jumpserver/conf.py:459
#, python-brace-format
msgid "The verification code is: {code}"
msgstr "验证码为: {code}"
-#: jumpserver/conf.py:459
+#: jumpserver/conf.py:464
msgid "Create account successfully"
msgstr "创建账号成功"
-#: jumpserver/conf.py:461
+#: jumpserver/conf.py:466
msgid "Your account has been created successfully"
msgstr "你的账号已创建成功"
-#: jumpserver/context_processor.py:12
+#: jumpserver/context_processor.py:14
msgid "JumpServer Open Source Bastion Host"
msgstr "JumpServer 开源堡垒机"
@@ -4004,15 +4067,15 @@ msgstr "系统信息"
msgid "Publish the station message"
msgstr "发布站内消息"
-#: ops/ansible/inventory.py:97 ops/models/job.py:62
+#: ops/ansible/inventory.py:106 ops/models/job.py:63
msgid "No account available"
msgstr "无可用账号"
-#: ops/ansible/inventory.py:264
+#: ops/ansible/inventory.py:280
msgid "Ansible disabled"
msgstr "Ansible 已禁用"
-#: ops/ansible/inventory.py:280
+#: ops/ansible/inventory.py:296
msgid "Skip hosts below:"
msgstr "跳过以下主机: "
@@ -4020,25 +4083,42 @@ msgstr "跳过以下主机: "
msgid "Waiting task start"
msgstr "等待任务开始"
-#: ops/api/celery.py:246
+#: ops/api/celery.py:262
msgid "Task {} not found"
msgstr "任务 {} 不存在"
-#: ops/api/celery.py:251
+#: ops/api/celery.py:267
msgid "Task {} args or kwargs error"
msgstr "任务 {} 执行参数错误"
-#: ops/api/job.py:135
+#: ops/api/job.py:81
+#, python-brace-format
+msgid ""
+"Asset ({asset}) must have at least one of the following protocols added: "
+"SSH, SFTP, or WinRM"
+msgstr "资产({asset})至少要添加ssh,sftp,winrm其中一种协议"
+
+#: ops/api/job.py:82
+#, python-brace-format
+msgid "Asset ({asset}) authorization is missing SSH, SFTP, or WinRM protocol"
+msgstr "资产({asset})授权缺少ssh,sftp或winrm协议"
+
+#: ops/api/job.py:83
+#, python-brace-format
+msgid "Asset ({asset}) authorization lacks upload permissions"
+msgstr "资产({asset})授权缺少上传权限"
+
+#: ops/api/job.py:168
msgid "Duplicate file exists"
msgstr "存在同名文件"
-#: ops/api/job.py:140
+#: ops/api/job.py:173
#, python-brace-format
msgid ""
"File size exceeds maximum limit. Please select a file smaller than {limit}MB"
msgstr "文件大小超过最大限制。请选择小于 {limit}MB 的文件。"
-#: ops/api/job.py:204
+#: ops/api/job.py:237
msgid ""
"The task is being created and cannot be interrupted. Please try again later."
msgstr "正在创建任务,无法中断,请稍后重试。"
@@ -4047,31 +4127,31 @@ msgstr "正在创建任务,无法中断,请稍后重试。"
msgid "Currently playbook is being used in a job"
msgstr "当前 playbook 正在作业中使用"
-#: ops/api/playbook.py:93
+#: ops/api/playbook.py:96
msgid "Unsupported file content"
msgstr "不支持的文件内容"
-#: ops/api/playbook.py:95 ops/api/playbook.py:141 ops/api/playbook.py:189
+#: ops/api/playbook.py:98 ops/api/playbook.py:144 ops/api/playbook.py:192
msgid "Invalid file path"
msgstr "无效的文件路径"
-#: ops/api/playbook.py:167
+#: ops/api/playbook.py:170
msgid "This file can not be rename"
msgstr "该文件不能重命名"
-#: ops/api/playbook.py:186
+#: ops/api/playbook.py:189
msgid "File already exists"
msgstr "文件已存在"
-#: ops/api/playbook.py:204
+#: ops/api/playbook.py:207
msgid "File key is required"
msgstr "文件密钥该字段是必填项。"
-#: ops/api/playbook.py:207
+#: ops/api/playbook.py:210
msgid "This file can not be delete"
msgstr "无法删除此文件"
-#: ops/apps.py:9 ops/notifications.py:17 rbac/tree.py:57
+#: ops/apps.py:9 ops/notifications.py:18 rbac/tree.py:57
msgid "App ops"
msgstr "作业中心"
@@ -4111,7 +4191,7 @@ msgstr "VCS"
msgid "Adhoc"
msgstr "命令"
-#: ops/const.py:39 ops/models/job.py:144
+#: ops/const.py:39 ops/models/job.py:145
msgid "Playbook"
msgstr "Playbook"
@@ -4127,39 +4207,43 @@ msgstr "仅限特权账号"
msgid "Privileged First"
msgstr "特权账号优先"
-#: ops/const.py:51 ops/const.py:61
+#: ops/const.py:51 ops/const.py:62
msgid "Powershell"
msgstr "PowerShell"
-#: ops/const.py:52 ops/const.py:62
+#: ops/const.py:52 ops/const.py:63
msgid "Python"
msgstr "Python"
-#: ops/const.py:53 ops/const.py:63
+#: ops/const.py:53 ops/const.py:64
msgid "MySQL"
msgstr "MySQL"
-#: ops/const.py:54 ops/const.py:65
+#: ops/const.py:54 ops/const.py:66
msgid "PostgreSQL"
msgstr "PostgreSQL"
-#: ops/const.py:55 ops/const.py:66
+#: ops/const.py:55 ops/const.py:67
msgid "SQLServer"
msgstr "SQLServer"
-#: ops/const.py:56 ops/const.py:68
+#: ops/const.py:56 ops/const.py:69
msgid "Raw"
msgstr "Raw"
-#: ops/const.py:64
+#: ops/const.py:57
+msgid "HUAWEI"
+msgstr ""
+
+#: ops/const.py:65
msgid "MariaDB"
msgstr "MariaDB"
-#: ops/const.py:67
+#: ops/const.py:68
msgid "Oracle"
msgstr "Oracle"
-#: ops/const.py:74
+#: ops/const.py:75
msgid "Timeout"
msgstr "超时"
@@ -4196,11 +4280,11 @@ msgstr "需要周期或定期设置"
msgid "Pattern"
msgstr "模式"
-#: ops/models/adhoc.py:23 ops/models/job.py:141
+#: ops/models/adhoc.py:23 ops/models/job.py:142
msgid "Module"
msgstr "模块"
-#: ops/models/adhoc.py:24 ops/models/celery.py:81 ops/models/job.py:139
+#: ops/models/adhoc.py:24 ops/models/celery.py:81 ops/models/job.py:140
#: terminal/models/component/task.py:14
msgid "Args"
msgstr "参数"
@@ -4219,12 +4303,12 @@ msgstr "最后执行"
msgid "Date last run"
msgstr "最后运行日期"
-#: ops/models/base.py:51 ops/models/job.py:232
-#: xpack/plugins/cloud/models.py:202
+#: ops/models/base.py:51 ops/models/job.py:233
+#: xpack/plugins/cloud/models.py:198
msgid "Result"
msgstr "结果"
-#: ops/models/base.py:52 ops/models/job.py:233
+#: ops/models/base.py:52 ops/models/job.py:234
msgid "Summary"
msgstr "汇总"
@@ -4245,55 +4329,55 @@ msgid "Kwargs"
msgstr "其它参数"
#: ops/models/celery.py:84 terminal/models/session/sharing.py:128
-#: tickets/const.py:26
+#: tickets/const.py:25
msgid "Finished"
msgstr "结束"
-#: ops/models/celery.py:85
+#: ops/models/celery.py:87
msgid "Date published"
msgstr "发布日期"
-#: ops/models/celery.py:110
+#: ops/models/celery.py:112
msgid "Celery Task Execution"
msgstr "Celery 任务执行"
-#: ops/models/job.py:142
+#: ops/models/job.py:143
msgid "Chdir"
msgstr "运行目录"
-#: ops/models/job.py:143
+#: ops/models/job.py:144
msgid "Timeout (Seconds)"
msgstr "超时时间 (秒)"
-#: ops/models/job.py:148
+#: ops/models/job.py:149
msgid "Use Parameter Define"
msgstr "使用参数定义"
-#: ops/models/job.py:149
+#: ops/models/job.py:150
msgid "Parameters define"
msgstr "参数定义"
-#: ops/models/job.py:150
+#: ops/models/job.py:151
msgid "Runas"
msgstr "运行用户"
-#: ops/models/job.py:152
+#: ops/models/job.py:153
msgid "Runas policy"
msgstr "用户策略"
-#: ops/models/job.py:216
+#: ops/models/job.py:217
msgid "Job"
msgstr "作业"
-#: ops/models/job.py:239
+#: ops/models/job.py:240
msgid "Material"
msgstr "Material"
-#: ops/models/job.py:241
+#: ops/models/job.py:242
msgid "Material Type"
msgstr "Material 类型"
-#: ops/models/job.py:567
+#: ops/models/job.py:539
msgid "Job Execution"
msgstr "作业执行"
@@ -4305,30 +4389,30 @@ msgstr "创建方式"
msgid "VCS URL"
msgstr "VCS URL"
-#: ops/notifications.py:18
+#: ops/notifications.py:19
msgid "Server performance"
msgstr "监控告警"
-#: ops/notifications.py:24
+#: ops/notifications.py:25
msgid "Terminal health check warning"
msgstr "终端健康状况检查警告"
-#: ops/notifications.py:69
+#: ops/notifications.py:70
#, python-brace-format
msgid "The terminal is offline: {name}"
msgstr "终端已离线: {name}"
-#: ops/notifications.py:74
+#: ops/notifications.py:75
#, python-brace-format
msgid "Disk used more than {max_threshold}%: => {value}"
msgstr "硬盘使用率超过 {max_threshold}%: => {value}"
-#: ops/notifications.py:79
+#: ops/notifications.py:80
#, python-brace-format
msgid "Memory used more than {max_threshold}%: => {value}"
msgstr "内存使用率超过 {max_threshold}%: => {value}"
-#: ops/notifications.py:84
+#: ops/notifications.py:85
#, python-brace-format
msgid "CPU load more than {max_threshold}: => {value}"
msgstr "CPU 使用率超过 {max_threshold}: => {value}"
@@ -4341,7 +4425,7 @@ msgstr "保存后执行"
msgid "Job type"
msgstr "任务类型"
-#: ops/serializers/job.py:72 terminal/serializers/session.py:55
+#: ops/serializers/job.py:72 terminal/serializers/session.py:56
msgid "Is finished"
msgstr "是否完成"
@@ -4350,31 +4434,35 @@ msgstr "是否完成"
msgid "Time cost"
msgstr "花费时间"
-#: ops/tasks.py:37
+#: ops/serializers/job.py:87
+msgid "You do not have permission for the current job."
+msgstr "你没有当前作业的权限。"
+
+#: ops/tasks.py:38
msgid "Run ansible task"
msgstr "运行 Ansible 任务"
-#: ops/tasks.py:71
+#: ops/tasks.py:72
msgid "Run ansible task execution"
msgstr "开始执行 Ansible 任务"
-#: ops/tasks.py:93
+#: ops/tasks.py:94
msgid "Clear celery periodic tasks"
msgstr "清理周期任务"
-#: ops/tasks.py:114
+#: ops/tasks.py:115
msgid "Create or update periodic tasks"
msgstr "创建或更新周期任务"
-#: ops/tasks.py:122
+#: ops/tasks.py:123
msgid "Periodic check service performance"
msgstr "周期检测服务性能"
-#: ops/tasks.py:128
+#: ops/tasks.py:129
msgid "Clean up unexpected jobs"
msgstr "清理异常作业"
-#: ops/tasks.py:135
+#: ops/tasks.py:136
msgid "Clean job_execution db record"
msgstr "清理作业中心执行历史"
@@ -4382,10 +4470,6 @@ msgstr "清理作业中心执行历史"
msgid "Task log"
msgstr "任务列表"
-#: ops/templates/ops/celery_task_log.html:71 terminal/serializers/task.py:10
-msgid "Task name"
-msgstr "任务名称"
-
#: ops/variables.py:24
msgid "The current user`s username of JumpServer"
msgstr "JumpServer 当前用户的用户名"
@@ -4422,17 +4506,17 @@ msgstr "Job ID"
msgid "Name of the job"
msgstr "Job 名称"
-#: orgs/api.py:62
+#: orgs/api.py:61
msgid "The current organization ({}) cannot be deleted"
msgstr "当前组织 ({}) 不能被删除"
-#: orgs/api.py:67
+#: orgs/api.py:66
msgid ""
"LDAP synchronization is set to the current organization. Please switch to "
"another organization before deleting"
msgstr "LDAP 同步设置组织为当前组织,请切换其他组织后再进行删除操作"
-#: orgs/api.py:77
+#: orgs/api.py:76
msgid "The organization have resource ({}) cannot be deleted"
msgstr "组织存在资源 ({}) 不能被删除"
@@ -4445,7 +4529,7 @@ msgstr "组织管理"
#: rbac/serializers/rolebinding.py:44 settings/serializers/auth/ldap.py:63
#: terminal/templates/terminal/_msg_command_warning.html:21
#: terminal/templates/terminal/_msg_session_sharing.html:14
-#: tickets/models/ticket/general.py:302 tickets/serializers/ticket/ticket.py:60
+#: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:60
msgid "Organization"
msgstr "组织"
@@ -4624,7 +4708,7 @@ msgstr "内部角色,不能删除"
msgid "The role has been bound to users, can't be destroy"
msgstr "角色已绑定用户,不能删除"
-#: rbac/api/role.py:102
+#: rbac/api/role.py:105
msgid "Internal role, can't be update"
msgstr "内部角色,不能更新"
@@ -4698,7 +4782,7 @@ msgid "Scope"
msgstr "范围"
#: rbac/models/role.py:46 rbac/models/rolebinding.py:52
-#: users/models/user.py:810
+#: users/models/user.py:824
msgid "Role"
msgstr "角色"
@@ -4710,7 +4794,7 @@ msgstr "系统角色"
msgid "Organization role"
msgstr "组织角色"
-#: rbac/models/rolebinding.py:61
+#: rbac/models/rolebinding.py:62
msgid "Role binding"
msgstr "角色绑定"
@@ -4718,17 +4802,17 @@ msgstr "角色绑定"
msgid "All organizations"
msgstr "所有组织"
-#: rbac/models/rolebinding.py:190
+#: rbac/models/rolebinding.py:193
msgid ""
"User last role in org, can not be delete, you can remove user from org "
"instead"
msgstr "用户最后一个角色,不能删除,你可以将用户从组织移除"
-#: rbac/models/rolebinding.py:197
+#: rbac/models/rolebinding.py:200
msgid "Organization role binding"
msgstr "组织角色绑定"
-#: rbac/models/rolebinding.py:212
+#: rbac/models/rolebinding.py:215
msgid "System role binding"
msgstr "系统角色绑定"
@@ -4808,7 +4892,7 @@ msgid "Ticket comment"
msgstr "工单评论"
#: rbac/tree.py:130 settings/serializers/feature.py:109
-#: tickets/models/ticket/general.py:307
+#: tickets/models/ticket/general.py:305
msgid "Ticket"
msgstr "工单管理"
@@ -4960,26 +5044,30 @@ msgid "FeiShu Auth"
msgstr "飞书 认证"
#: settings/serializers/auth/base.py:20
+msgid "Lark Auth"
+msgstr "Lark 认证"
+
+#: settings/serializers/auth/base.py:21
msgid "Slack Auth"
msgstr "Slack 认证"
-#: settings/serializers/auth/base.py:21
+#: settings/serializers/auth/base.py:22
msgid "WeCom Auth"
msgstr "企业微信 认证"
-#: settings/serializers/auth/base.py:22
+#: settings/serializers/auth/base.py:23
msgid "SSO Auth"
msgstr "SSO 令牌认证"
-#: settings/serializers/auth/base.py:23
+#: settings/serializers/auth/base.py:24
msgid "Passkey Auth"
msgstr "Passkey 认证"
-#: settings/serializers/auth/base.py:26
+#: settings/serializers/auth/base.py:27
msgid "Forgot password url"
msgstr "忘记密码 URL"
-#: settings/serializers/auth/base.py:29
+#: settings/serializers/auth/base.py:30
msgid "Enable login redirect msg"
msgstr "启用登录跳转提示"
@@ -5024,10 +5112,14 @@ msgstr "创建用户(如果不存在)"
msgid "Enable DingTalk Auth"
msgstr "启用钉钉认证"
-#: settings/serializers/auth/feishu.py:16
+#: settings/serializers/auth/feishu.py:12
msgid "Enable FeiShu Auth"
msgstr "启用飞书认证"
+#: settings/serializers/auth/lark.py:12
+msgid "Enable Lark Auth"
+msgstr "启用 Lark 认证"
+
#: settings/serializers/auth/ldap.py:39
msgid "LDAP"
msgstr "LDAP"
@@ -5078,11 +5170,21 @@ msgstr ""
msgid "Connect timeout (s)"
msgstr "连接超时时间 (秒)"
-#: settings/serializers/auth/ldap.py:79
+#: settings/serializers/auth/ldap.py:82
+msgid "User DN cache timeout (s)"
+msgstr "User DN 缓存超时时间 (秒)"
+
+#: settings/serializers/auth/ldap.py:84
+msgid ""
+"Caching the User DN obtained during user login authentication can "
+"effectivelyimprove the speed of user authentication., 0 means no cache"
+msgstr "对用户登录认证时查询出的 User DN 进行缓存,可以有效提高用户认证的速度"
+
+#: settings/serializers/auth/ldap.py:88
msgid "Search paged size (piece)"
msgstr "搜索分页数量 (条)"
-#: settings/serializers/auth/ldap.py:84
+#: settings/serializers/auth/ldap.py:93
msgid "Enable LDAP auth"
msgstr "启用 LDAP 认证"
@@ -5528,7 +5630,7 @@ msgstr "启动聊天 AI"
msgid "Base Url"
msgstr "基本地址"
-#: settings/serializers/feature.py:81 templates/_header_bar.html:90
+#: settings/serializers/feature.py:81 templates/_header_bar.html:96
msgid "API Key"
msgstr "API Key"
@@ -5921,40 +6023,48 @@ msgstr "连接最大空闲时间 (分)"
msgid "If idle time more than it, disconnect connection."
msgstr "提示:如果超过该配置没有操作,连接会被断开"
+#: settings/serializers/security.py:200
+msgid "Session expire at browser closed"
+msgstr "会话在浏览器关闭时过期"
+
#: settings/serializers/security.py:201
+msgid "Whether to expire the session when the user closes their browser."
+msgstr "当用户关闭浏览器时是否使会话过期。"
+
+#: settings/serializers/security.py:205
msgid "Session max connection time (hour)"
msgstr "会话连接最大时间 (时)"
-#: settings/serializers/security.py:202
+#: settings/serializers/security.py:206
msgid "If session connection time more than it, disconnect connection."
msgstr "提示:如果会话连接超过该配置,连接会被断开"
-#: settings/serializers/security.py:205
+#: settings/serializers/security.py:209
msgid "Remember manual auth"
msgstr "保存手动输入密码"
-#: settings/serializers/security.py:208
+#: settings/serializers/security.py:212
#: terminal/templates/terminal/_msg_session_sharing.html:10
msgid "Session share"
msgstr "会话分享"
-#: settings/serializers/security.py:209
+#: settings/serializers/security.py:213
msgid "Enabled, Allows user active session to be shared with other users"
msgstr "开启后允许用户分享已连接的资产会话给他人,协同工作"
-#: settings/serializers/security.py:215
+#: settings/serializers/security.py:219
msgid "Insecure command alert"
msgstr "危险命令告警"
-#: settings/serializers/security.py:218
+#: settings/serializers/security.py:222
msgid "Email recipient"
msgstr "邮件收件人"
-#: settings/serializers/security.py:219
+#: settings/serializers/security.py:223
msgid "Multiple user using , split"
msgstr "多个用户,使用 , 分割"
-#: settings/serializers/settings.py:70
+#: settings/serializers/settings.py:62
#, python-format
msgid "[%s] %s"
msgstr "[%s] %s"
@@ -6026,7 +6136,7 @@ msgid "Sync task Finish"
msgstr "同步任务完成"
#: settings/templates/ldap/_msg_import_ldap_user.html:6
-#: terminal/models/session/session.py:45
+#: terminal/models/session/session.py:46
msgid "Date end"
msgstr "结束日期"
@@ -6143,11 +6253,11 @@ msgstr "认证失败: (未知): {}"
msgid "Authentication success: {}"
msgstr "认证成功: {}"
-#: settings/ws.py:236
+#: settings/ws.py:195
msgid "Get ldap users is None"
msgstr "获取 LDAP 用户为 None"
-#: settings/ws.py:246
+#: settings/ws.py:205
msgid "Imported {} users successfully (Organization: {})"
msgstr "成功导入 {} 个用户 ( 组织: {} )"
@@ -6165,7 +6275,7 @@ msgstr "下载导入的模板或使用导出的csv格式"
#: templates/_csv_import_modal.html:13
msgid "Download the import template"
-msgstr "下载导入模版"
+msgstr "下载导入模板"
#: templates/_csv_import_modal.html:17 templates/_csv_update_modal.html:17
msgid "Select the CSV file to import"
@@ -6181,7 +6291,7 @@ msgstr "下载更新的模板或使用导出的csv格式"
#: templates/_csv_update_modal.html:13
msgid "Download the update template"
-msgstr "下载更新模版"
+msgstr "下载更新模板"
#: templates/_header_bar.html:12
msgid "Help"
@@ -6195,19 +6305,19 @@ msgstr "文档"
msgid "Commercial support"
msgstr "商业支持"
-#: templates/_header_bar.html:79 users/forms/profile.py:44
+#: templates/_header_bar.html:85 users/forms/profile.py:43
msgid "Profile"
msgstr "个人信息"
-#: templates/_header_bar.html:83
+#: templates/_header_bar.html:89
msgid "Admin page"
msgstr "管理页面"
-#: templates/_header_bar.html:86
+#: templates/_header_bar.html:92
msgid "User page"
msgstr "用户页面"
-#: templates/_header_bar.html:91
+#: templates/_header_bar.html:97
msgid "Logout"
msgstr "注销登录"
@@ -6288,13 +6398,13 @@ msgstr ""
msgid "Send verification code"
msgstr "发送验证码"
-#: templates/_mfa_login_field.html:106
-#: users/templates/users/forgot_password.html:174
+#: templates/_mfa_login_field.html:107
+#: users/templates/users/forgot_password.html:176
msgid "Wait: "
msgstr "等待:"
-#: templates/_mfa_login_field.html:116
-#: users/templates/users/forgot_password.html:190
+#: templates/_mfa_login_field.html:117
+#: users/templates/users/forgot_password.html:192
msgid "The verification code has been sent"
msgstr "验证码已发送"
@@ -6303,7 +6413,7 @@ msgid "Home page"
msgstr "首页"
#: templates/resource_download.html:18 templates/resource_download.html:33
-#: users/const.py:60
+#: users/const.py:65
msgid "Client"
msgstr "客户端"
@@ -6357,31 +6467,31 @@ msgstr "企业版远程应用,在社区版中不能使用"
msgid "Not found protocol query params"
msgstr "未发现 protocol 查询参数"
-#: terminal/api/component/storage.py:30
+#: terminal/api/component/storage.py:31
msgid "Deleting the default storage is not allowed"
msgstr "不允许删除默认存储配置"
-#: terminal/api/component/storage.py:33
+#: terminal/api/component/storage.py:34
msgid "Cannot delete storage that is being used"
msgstr "不允许删除正在使用的存储配置"
-#: terminal/api/component/storage.py:74 terminal/api/component/storage.py:75
+#: terminal/api/component/storage.py:75 terminal/api/component/storage.py:76
msgid "Command storages"
msgstr "命令存储"
-#: terminal/api/component/storage.py:81
+#: terminal/api/component/storage.py:82
msgid "Invalid"
msgstr "无效"
-#: terminal/api/component/storage.py:129 terminal/tasks.py:142
+#: terminal/api/component/storage.py:130 terminal/tasks.py:149
msgid "Test failure: {}"
msgstr "测试失败: {}"
-#: terminal/api/component/storage.py:132
+#: terminal/api/component/storage.py:133
msgid "Test successful"
msgstr "测试成功"
-#: terminal/api/component/storage.py:134
+#: terminal/api/component/storage.py:135
msgid "Test failure: Please check configuration"
msgstr "测试失败:请检查配置"
@@ -6451,6 +6561,10 @@ msgstr "数据库客户端"
msgid "Remote Desktop"
msgstr "远程桌面客户端"
+#: terminal/connect_methods.py:37
+msgid "RDP Guide"
+msgstr "RDP 连接向导"
+
#: terminal/const.py:12
msgid "Review & Reject"
msgstr "审批 & 拒绝"
@@ -6558,7 +6672,8 @@ msgstr "可以并发"
msgid "Tags"
msgstr "标签"
-#: terminal/models/applet/applet.py:48 terminal/serializers/storage.py:197
+#: terminal/models/applet/applet.py:48 terminal/serializers/applet_host.py:167
+#: terminal/serializers/storage.py:197
msgid "Hosts"
msgstr "主机"
@@ -6722,7 +6837,7 @@ msgstr "应用用户"
msgid "Can view terminal config"
msgstr "可以查看终端配置"
-#: terminal/models/session/command.py:66
+#: terminal/models/session/command.py:76
msgid "Command record"
msgstr "命令记录"
@@ -6738,43 +6853,43 @@ msgstr "可以上传会话录像"
msgid "Can download session replay"
msgstr "可以下载会话录像"
-#: terminal/models/session/session.py:34
+#: terminal/models/session/session.py:35
msgid "Account id"
msgstr "账号 ID"
-#: terminal/models/session/session.py:36 terminal/models/session/sharing.py:118
+#: terminal/models/session/session.py:37 terminal/models/session/sharing.py:118
msgid "Login from"
msgstr "登录来源"
-#: terminal/models/session/session.py:41
+#: terminal/models/session/session.py:42
msgid "Replay"
msgstr "回放"
-#: terminal/models/session/session.py:47 terminal/serializers/session.py:67
+#: terminal/models/session/session.py:48 terminal/serializers/session.py:68
msgid "Command amount"
msgstr "命令数量"
-#: terminal/models/session/session.py:48 terminal/serializers/session.py:30
+#: terminal/models/session/session.py:49 terminal/serializers/session.py:30
msgid "Error reason"
msgstr "错误原因"
-#: terminal/models/session/session.py:282
+#: terminal/models/session/session.py:290
msgid "Session record"
msgstr "会话记录"
-#: terminal/models/session/session.py:284
+#: terminal/models/session/session.py:292
msgid "Can monitor session"
msgstr "可以监控会话"
-#: terminal/models/session/session.py:285
+#: terminal/models/session/session.py:293
msgid "Can share session"
msgstr "可以分享会话"
-#: terminal/models/session/session.py:286
+#: terminal/models/session/session.py:294
msgid "Can terminate session"
msgstr "可以终断会话"
-#: terminal/models/session/session.py:287
+#: terminal/models/session/session.py:295
msgid "Can validate session action perm"
msgstr "可以验证会话动作权限"
@@ -6877,7 +6992,7 @@ msgstr "命令及录像存储"
msgid "Connectivity alarm"
msgstr "可连接性告警"
-#: terminal/notifications.py:240 terminal/tasks.py:146
+#: terminal/notifications.py:240 terminal/tasks.py:153
msgid "Test failure: Account invalid"
msgstr "测试失败: 账号无效"
@@ -6994,6 +7109,18 @@ msgstr ""
"优先使用同名账号连接发布机。为了安全,需配置文件中开启配置 "
"CACHE_LOGIN_PASSWORD_ENABLED=true, 修改后重启服务"
+#: terminal/serializers/applet_host.py:137
+msgid "Install applets"
+msgstr "安装应用"
+
+#: terminal/serializers/applet_host.py:167
+msgid "Host ID"
+msgstr "主机 ID"
+
+#: terminal/serializers/applet_host.py:168
+msgid "Applet ID"
+msgstr "远程应用 ID"
+
#: terminal/serializers/command.py:19
msgid "Session ID"
msgstr "会话ID"
@@ -7066,31 +7193,35 @@ msgstr "如果不同端点下的资产 IP 有冲突,使用资产标签实现"
msgid "Asset IP"
msgstr "资产 IP"
-#: terminal/serializers/session.py:25 terminal/serializers/session.py:52
+#: terminal/serializers/session.py:25 terminal/serializers/session.py:53
msgid "Can replay"
msgstr "是否可重放"
-#: terminal/serializers/session.py:26 terminal/serializers/session.py:53
+#: terminal/serializers/session.py:26 terminal/serializers/session.py:54
msgid "Can join"
msgstr "是否可加入"
-#: terminal/serializers/session.py:27 terminal/serializers/session.py:56
+#: terminal/serializers/session.py:27 terminal/serializers/session.py:57
msgid "Can terminate"
msgstr "是否可中断"
-#: terminal/serializers/session.py:48
+#: terminal/serializers/session.py:47
+msgid "Duration"
+msgstr "时长"
+
+#: terminal/serializers/session.py:49
msgid "User ID"
msgstr "用户 ID"
-#: terminal/serializers/session.py:49
+#: terminal/serializers/session.py:50
msgid "Asset ID"
msgstr "资产 ID"
-#: terminal/serializers/session.py:50
+#: terminal/serializers/session.py:51
msgid "Login from display"
msgstr "登录来源名称"
-#: terminal/serializers/session.py:57
+#: terminal/serializers/session.py:58
msgid "Terminal display"
msgstr "终端显示"
@@ -7112,7 +7243,7 @@ msgstr "Access key ID(AK)"
msgid "Access key secret"
msgstr "Access key secret(SK)"
-#: terminal/serializers/storage.py:68 xpack/plugins/cloud/models.py:253
+#: terminal/serializers/storage.py:68 xpack/plugins/cloud/models.py:249
msgid "Region"
msgstr "地域"
@@ -7132,7 +7263,7 @@ msgstr "端点后缀"
msgid "HOST"
msgstr "主机"
-#: terminal/serializers/storage.py:146 users/models/user.py:830
+#: terminal/serializers/storage.py:146 users/models/user.py:844
#: xpack/plugins/cloud/serializers/account_attrs.py:213
msgid "Private key"
msgstr "ssh私钥"
@@ -7290,27 +7421,31 @@ msgstr "授权已过期"
msgid "storage is null"
msgstr "存储为空"
-#: terminal/tasks.py:33
+#: terminal/tasks.py:31
msgid "Periodic delete terminal status"
msgstr "周期清理终端状态"
-#: terminal/tasks.py:42
+#: terminal/tasks.py:39
msgid "Clean orphan session"
msgstr "清除离线会话"
-#: terminal/tasks.py:91
+#: terminal/tasks.py:87
msgid "Run applet host deployment"
msgstr "运行应用机部署"
-#: terminal/tasks.py:101
+#: terminal/tasks.py:97
msgid "Install applet"
msgstr "安装应用"
-#: terminal/tasks.py:112
+#: terminal/tasks.py:108
+msgid "Uninstall applet"
+msgstr "卸载应用"
+
+#: terminal/tasks.py:119
msgid "Generate applet host accounts"
msgstr "收集远程应用上的账号"
-#: terminal/tasks.py:124
+#: terminal/tasks.py:131
msgid "Check command replay storage connectivity"
msgstr "检查命令及录像存储可连接性 "
@@ -7337,7 +7472,7 @@ msgstr "没有端口可以使用,检查并修改配置文件中 Magnus 监听
msgid "All available port count: {}, Already use port count: {}"
msgstr "所有可用端口数量:{},已使用端口数量:{}"
-#: tickets/api/ticket.py:88 tickets/models/ticket/general.py:288
+#: tickets/api/ticket.py:88 tickets/models/ticket/general.py:286
msgid "Applicant"
msgstr "申请人"
@@ -7349,59 +7484,55 @@ msgstr "工单管理"
msgid "Apply for asset"
msgstr "申请资产"
-#: tickets/const.py:17 tickets/const.py:25 tickets/const.py:44
+#: tickets/const.py:17 tickets/const.py:24 tickets/const.py:42
msgid "Open"
msgstr "打开"
-#: tickets/const.py:19 tickets/const.py:32
-msgid "Reopen"
-msgstr "重新打开"
-
-#: tickets/const.py:20 tickets/const.py:33
+#: tickets/const.py:19 tickets/const.py:31
msgid "Approved"
msgstr "已同意"
-#: tickets/const.py:21 tickets/const.py:34
+#: tickets/const.py:20 tickets/const.py:32
msgid "Rejected"
msgstr "已拒绝"
-#: tickets/const.py:31 tickets/const.py:39
+#: tickets/const.py:30 tickets/const.py:37
msgid "Closed"
msgstr "关闭的"
-#: tickets/const.py:51
+#: tickets/const.py:49
msgid "One level"
msgstr "1 级"
-#: tickets/const.py:52
+#: tickets/const.py:50
msgid "Two level"
msgstr "2 级"
-#: tickets/const.py:56
+#: tickets/const.py:54
msgid "Org admin"
msgstr "组织管理员"
-#: tickets/const.py:57
+#: tickets/const.py:55
msgid "Custom user"
msgstr "自定义用户"
-#: tickets/const.py:58
+#: tickets/const.py:56
msgid "Super admin"
msgstr "超级管理员"
-#: tickets/const.py:59
+#: tickets/const.py:57
msgid "Super admin and org admin"
msgstr "组织管理员或超级管理员"
-#: tickets/const.py:63
+#: tickets/const.py:61
msgid "All assets"
msgstr "所有资产"
-#: tickets/const.py:64
+#: tickets/const.py:62
msgid "Permed assets"
msgstr "授权的资产"
-#: tickets/const.py:65
+#: tickets/const.py:63
msgid "Permed valid assets"
msgstr "有效授权的资产"
@@ -7445,7 +7576,7 @@ msgid "Body"
msgstr "内容"
#: tickets/models/flow.py:19 tickets/models/flow.py:61
-#: tickets/models/ticket/general.py:41
+#: tickets/models/ticket/general.py:42
msgid "Approve level"
msgstr "审批级别"
@@ -7515,35 +7646,35 @@ msgstr "命令过滤器"
msgid "Apply Command Ticket"
msgstr "命令复核工单"
-#: tickets/models/ticket/general.py:76
+#: tickets/models/ticket/general.py:77
msgid "Ticket step"
msgstr "工单步骤"
-#: tickets/models/ticket/general.py:94
+#: tickets/models/ticket/general.py:95
msgid "Ticket assignee"
msgstr "工单受理人"
-#: tickets/models/ticket/general.py:272
+#: tickets/models/ticket/general.py:270
msgid "Title"
msgstr "标题"
-#: tickets/models/ticket/general.py:292
+#: tickets/models/ticket/general.py:290
msgid "TicketFlow"
msgstr "工单流程"
-#: tickets/models/ticket/general.py:295
+#: tickets/models/ticket/general.py:293
msgid "Approval step"
msgstr "审批步骤"
-#: tickets/models/ticket/general.py:298
+#: tickets/models/ticket/general.py:296
msgid "Relation snapshot"
msgstr "工单快照"
-#: tickets/models/ticket/general.py:401
+#: tickets/models/ticket/general.py:399
msgid "Please try again"
msgstr "请再次尝试"
-#: tickets/models/ticket/general.py:470
+#: tickets/models/ticket/general.py:475
msgid "Super ticket"
msgstr "超级工单"
@@ -7686,11 +7817,11 @@ msgstr "无效的审批动作"
msgid "This user is not authorized to approve this ticket"
msgstr "此用户无权审批此工单"
-#: users/api/user.py:137
+#: users/api/user.py:155
msgid "Can not invite self"
msgstr "不能邀请自己"
-#: users/api/user.py:190
+#: users/api/user.py:208
msgid "Could not reset self otp, use profile reset instead"
msgstr "不能在该页面重置 MFA 多因子认证, 请去个人信息页面重置"
@@ -7734,11 +7865,27 @@ msgstr "多屏显示"
msgid "Drives redirect"
msgstr "磁盘挂载"
-#: users/const.py:64
+#: users/const.py:37
+msgid "Current window"
+msgstr "当前窗口"
+
+#: users/const.py:38
+msgid "New window"
+msgstr "新窗口"
+
+#: users/const.py:47
+msgid "High(32 bit)"
+msgstr "高(32 bit)"
+
+#: users/const.py:48
+msgid "Medium(16 bit)"
+msgstr "中(16 bit)"
+
+#: users/const.py:69
msgid "Replace"
msgstr "替换"
-#: users/const.py:65
+#: users/const.py:70
msgid "Suffix"
msgstr "加后缀"
@@ -7750,7 +7897,7 @@ msgstr "MFA 多因子认证没有开启"
msgid "Unable to delete all users"
msgstr "无法删除全部用户"
-#: users/forms/profile.py:50
+#: users/forms/profile.py:48
msgid ""
"When enabled, you will enter the MFA binding process the next time you log "
"in. you can also directly bind in \"personal information -> quick "
@@ -7759,11 +7906,11 @@ msgstr ""
"启用之后您将会在下次登录时进入多因子认证绑定流程;您也可以在 (个人信息->快速"
"修改->设置 MFA 多因子认证)中直接绑定!"
-#: users/forms/profile.py:61
+#: users/forms/profile.py:59
msgid "* Enable MFA to make the account more secure."
msgstr "* 启用 MFA 多因子认证,使账号更加安全。"
-#: users/forms/profile.py:70
+#: users/forms/profile.py:68
msgid ""
"In order to protect you and your company, please keep your account, password "
"and key sensitive information properly. (for example: setting complex "
@@ -7772,136 +7919,140 @@ msgstr ""
"为了保护您和公司的安全,请妥善保管您的账号、密码和密钥等重要敏感信息; (如:"
"设置复杂密码,并启用 MFA 多因子认证)"
-#: users/forms/profile.py:77
+#: users/forms/profile.py:75
msgid "Finish"
msgstr "完成"
-#: users/forms/profile.py:84
+#: users/forms/profile.py:82
msgid "New password"
msgstr "新密码"
-#: users/forms/profile.py:89
+#: users/forms/profile.py:87
msgid "Confirm password"
msgstr "确认密码"
-#: users/forms/profile.py:97
+#: users/forms/profile.py:95
msgid "Password does not match"
msgstr "密码不一致"
-#: users/forms/profile.py:105
+#: users/forms/profile.py:104
msgid "The phone number must contain an area code, for example, +86"
msgstr "手机号码必须包含区号,例如 +86"
-#: users/forms/profile.py:121
+#: users/forms/profile.py:120
msgid "Old password"
msgstr "原来密码"
-#: users/forms/profile.py:131
+#: users/forms/profile.py:130
msgid "Old password error"
msgstr "原来密码错误"
-#: users/forms/profile.py:141
+#: users/forms/profile.py:140
msgid "Automatically configure and download the SSH key"
msgstr "自动配置并下载SSH密钥"
-#: users/forms/profile.py:143
+#: users/forms/profile.py:142
msgid "ssh public key"
msgstr "SSH公钥"
-#: users/forms/profile.py:144
+#: users/forms/profile.py:143
msgid "ssh-rsa AAAA..."
msgstr "ssh-rsa AAAA..."
-#: users/forms/profile.py:145
+#: users/forms/profile.py:144
msgid "Paste your id_rsa.pub here."
msgstr "复制你的公钥到这里"
-#: users/forms/profile.py:158
+#: users/forms/profile.py:157
msgid "Public key should not be the same as your old one."
msgstr "不能和原来的密钥相同"
-#: users/forms/profile.py:162 users/serializers/profile.py:76
+#: users/forms/profile.py:161 users/serializers/profile.py:76
#: users/serializers/profile.py:164 users/serializers/profile.py:191
msgid "Not a valid ssh public key"
msgstr "SSH密钥不合法"
-#: users/forms/profile.py:173 users/models/user.py:833
+#: users/forms/profile.py:172 users/models/user.py:847
#: xpack/plugins/cloud/serializers/account_attrs.py:210
msgid "Public key"
msgstr "SSH公钥"
-#: users/models/preference.py:38
+#: users/models/preference.py:38 users/serializers/preference/preference.py:19
msgid "Preference"
msgstr "用户设置"
-#: users/models/user.py:646 users/serializers/profile.py:94
+#: users/models/user.py:656 users/serializers/profile.py:94
msgid "Force enable"
msgstr "强制启用"
-#: users/models/user.py:812 users/serializers/user.py:171
+#: users/models/user.py:762
+msgid "Lark"
+msgstr ""
+
+#: users/models/user.py:826 users/serializers/user.py:175
msgid "Is service account"
msgstr "服务账号"
-#: users/models/user.py:814
+#: users/models/user.py:828
msgid "Avatar"
msgstr "头像"
-#: users/models/user.py:817
+#: users/models/user.py:831
msgid "Wechat"
msgstr "微信"
-#: users/models/user.py:820 users/serializers/user.py:108
+#: users/models/user.py:834 users/serializers/user.py:111
msgid "Phone"
msgstr "手机"
-#: users/models/user.py:826
+#: users/models/user.py:840
msgid "OTP secret key"
msgstr "OTP 密钥"
# msgid "Private key"
# msgstr "ssh私钥"
-#: users/models/user.py:838 users/serializers/profile.py:128
-#: users/serializers/user.py:168
+#: users/models/user.py:852 users/serializers/profile.py:128
+#: users/serializers/user.py:172
msgid "Is first login"
msgstr "首次登录"
-#: users/models/user.py:848
+#: users/models/user.py:862
msgid "Date password last updated"
msgstr "最后更新密码日期"
-#: users/models/user.py:851
+#: users/models/user.py:865
msgid "Need update password"
msgstr "需要更新密码"
-#: users/models/user.py:853
+#: users/models/user.py:867
msgid "Date api key used"
msgstr "Api key 最后使用日期"
-#: users/models/user.py:985
+#: users/models/user.py:1000
msgid "Can not delete admin user"
msgstr "无法删除管理员用户"
-#: users/models/user.py:1012
+#: users/models/user.py:1028
msgid "Can invite user"
msgstr "可以邀请用户"
-#: users/models/user.py:1013
+#: users/models/user.py:1029
msgid "Can remove user"
msgstr "可以移除用户"
-#: users/models/user.py:1014
+#: users/models/user.py:1030
msgid "Can match user"
msgstr "可以匹配用户"
-#: users/models/user.py:1023
+#: users/models/user.py:1039
msgid "Administrator"
msgstr "管理员"
-#: users/models/user.py:1026
+#: users/models/user.py:1042
msgid "Administrator is the super user of system"
msgstr "Administrator是初始的超级管理员"
-#: users/models/user.py:1051
+#: users/models/user.py:1067
msgid "User password history"
msgstr "用户密码历史"
@@ -7912,7 +8063,7 @@ msgstr "用户密码历史"
msgid "Reset password"
msgstr "重置密码"
-#: users/notifications.py:85 users/views/profile/reset.py:231
+#: users/notifications.py:85 users/views/profile/reset.py:233
msgid "Reset password success"
msgstr "重置密码成功"
@@ -7960,29 +8111,33 @@ msgstr "两次密码不一致"
msgid "Async loading of asset tree"
msgstr "异步加载资产树"
-#: users/serializers/preference/luna.py:33
+#: users/serializers/preference/luna.py:30
+msgid "Connect default open method"
+msgstr "连接默认打开方式"
+
+#: users/serializers/preference/luna.py:37
msgid "RDP resolution"
msgstr "RDP 分辨率"
-#: users/serializers/preference/luna.py:37
+#: users/serializers/preference/luna.py:41
msgid "Keyboard layout"
msgstr "键盘布局"
-#: users/serializers/preference/luna.py:41
+#: users/serializers/preference/luna.py:45
msgid "RDP client option"
msgstr "RDP 客户端选项"
-#: users/serializers/preference/luna.py:45
-msgid "RDP color quality"
-msgstr ""
-
#: users/serializers/preference/luna.py:49
-msgid "Rdp smart size"
-msgstr ""
+msgid "RDP color quality"
+msgstr "RDP 颜色质量"
+
+#: users/serializers/preference/luna.py:53
+msgid "RDP smart size"
+msgstr "RDP 智能尺寸"
# msgid "Rdp smart size"
# msgstr "RDP 智能大小"
-#: users/serializers/preference/luna.py:50
+#: users/serializers/preference/luna.py:54
msgid ""
"Determines whether the client computer should scale the content on the "
"remote computer to fit the window size of the client computer when the "
@@ -7991,27 +8146,27 @@ msgstr ""
"确定调整窗口大小时客户端计算机是否应缩放远程计算机上的内容以适应客户端计算机"
"的窗口大小"
-#: users/serializers/preference/luna.py:55
+#: users/serializers/preference/luna.py:59
msgid "Remote application connection method"
msgstr "远程应用连接方式"
-#: users/serializers/preference/luna.py:62
+#: users/serializers/preference/luna.py:66
msgid "Character terminal font size"
msgstr "字符终端字体大小"
-#: users/serializers/preference/luna.py:65
+#: users/serializers/preference/luna.py:69
msgid "Backspace as Ctrl+H"
msgstr "字符终端Backspace As Ctrl+H"
-#: users/serializers/preference/luna.py:68
+#: users/serializers/preference/luna.py:72
msgid "Right click quickly paste"
msgstr "右键快速粘贴"
-#: users/serializers/preference/luna.py:74
+#: users/serializers/preference/luna.py:78
msgid "Graphics"
msgstr "图形化"
-#: users/serializers/preference/luna.py:75
+#: users/serializers/preference/luna.py:79
msgid "Command line"
msgstr "命令行"
@@ -8027,71 +8182,75 @@ msgstr "密码不满足安全规则"
msgid "The new password cannot be the last {} passwords"
msgstr "新密码不能是最近 {} 次的密码"
-#: users/serializers/user.py:42
+#: users/serializers/user.py:44
msgid "System roles"
msgstr "系统角色"
-#: users/serializers/user.py:46
+#: users/serializers/user.py:48
msgid "Org roles"
msgstr "组织角色"
-#: users/serializers/user.py:90
+#: users/serializers/user.py:51
+msgid "Organizations and roles"
+msgstr "组织和角色"
+
+#: users/serializers/user.py:93
msgid "Password strategy"
msgstr "密码策略"
-#: users/serializers/user.py:92
+#: users/serializers/user.py:95
msgid "MFA enabled"
msgstr "MFA 已启用"
-#: users/serializers/user.py:94
+#: users/serializers/user.py:97
msgid "MFA force enabled"
msgstr "强制 MFA"
-#: users/serializers/user.py:96
+#: users/serializers/user.py:99
msgid "Login blocked"
msgstr "登录被锁定"
-#: users/serializers/user.py:99 users/serializers/user.py:177
+#: users/serializers/user.py:102 users/serializers/user.py:181
msgid "Is OTP bound"
msgstr "是否绑定了虚拟 MFA"
-#: users/serializers/user.py:100
+#: users/serializers/user.py:103
msgid "Super Administrator"
msgstr "超级管理员"
-#: users/serializers/user.py:101
+#: users/serializers/user.py:104
msgid "Organization Administrator"
msgstr "组织管理员"
-#: users/serializers/user.py:103
+#: users/serializers/user.py:106
msgid "Can public key authentication"
msgstr "可以使用公钥认证"
-#: users/serializers/user.py:172
+#: users/serializers/user.py:176
msgid "Is org admin"
msgstr "组织管理员"
-#: users/serializers/user.py:174
+#: users/serializers/user.py:178
msgid "Avatar url"
msgstr "头像路径"
-#: users/serializers/user.py:178
+#: users/serializers/user.py:182
msgid "MFA level"
msgstr "MFA 级别"
-#: users/serializers/user.py:289
+#: users/serializers/user.py:304
msgid "Select users"
msgstr "选择用户"
-#: users/serializers/user.py:290
+#: users/serializers/user.py:305
msgid "For security, only list several users"
msgstr "为了安全,仅列出几个用户"
-#: users/serializers/user.py:323
+#: users/serializers/user.py:338
msgid "name not unique"
msgstr "名称重复"
-#: users/signal_handlers.py:32
+#: users/signal_handlers.py:33
msgid ""
"The administrator has enabled \"Only allow existing users to log in\", \n"
" and the current user is not in the user list. Please contact the "
@@ -8099,7 +8258,7 @@ msgid ""
msgstr ""
"管理员已开启'仅允许已存在用户登录',当前用户不在用户列表中,请联系管理员。"
-#: users/signal_handlers.py:166
+#: users/signal_handlers.py:167
msgid "Clean up expired user sessions"
msgstr "清除过期的用户会话"
@@ -8192,15 +8351,15 @@ msgstr "输入您的手机号码,验证码将发送到您的手机"
msgid "Email account"
msgstr "邮箱账号"
-#: users/templates/users/forgot_password.html:92
+#: users/templates/users/forgot_password.html:93
msgid "Mobile number"
msgstr "手机号码"
-#: users/templates/users/forgot_password.html:100
+#: users/templates/users/forgot_password.html:101
msgid "Send"
msgstr "发送"
-#: users/templates/users/forgot_password.html:104
+#: users/templates/users/forgot_password.html:105
#: users/templates/users/forgot_password_previewing.html:30
msgid "Submit"
msgstr "提交"
@@ -8353,23 +8512,23 @@ msgid ""
"their passwords: {}"
msgstr "非本地用户仅允许从第三方平台登录,不支持修改密码: {}"
-#: users/views/profile/reset.py:186 users/views/profile/reset.py:197
+#: users/views/profile/reset.py:188 users/views/profile/reset.py:199
msgid "Token invalid or expired"
msgstr "令牌错误或失效"
-#: users/views/profile/reset.py:202
+#: users/views/profile/reset.py:204
msgid "User auth from {}, go there change password"
msgstr "用户认证源来自 {}, 请去相应系统修改密码"
-#: users/views/profile/reset.py:209
+#: users/views/profile/reset.py:211
msgid "* Your password does not meet the requirements"
msgstr "* 您的密码不符合要求"
-#: users/views/profile/reset.py:215
+#: users/views/profile/reset.py:217
msgid "* The new password cannot be the last {} passwords"
msgstr "* 新密码不能是最近 {} 次的密码"
-#: users/views/profile/reset.py:232
+#: users/views/profile/reset.py:234
msgid "Reset password success, return to login page"
msgstr "重置密码成功,返回到登录页面"
@@ -8382,11 +8541,11 @@ msgid ""
"The current task is not synchronized with unmatched policy assets, skipping"
msgstr ""
-#: xpack/plugins/cloud/api.py:56
+#: xpack/plugins/cloud/api.py:60
msgid "Test connection successful"
msgstr "测试成功"
-#: xpack/plugins/cloud/api.py:58
+#: xpack/plugins/cloud/api.py:62
msgid "Test connection failed: {}"
msgstr "测试连接失败:{}"
@@ -8438,91 +8597,96 @@ msgstr "谷歌云"
msgid "UCloud"
msgstr "ucloud"
-#: xpack/plugins/cloud/const.py:22
+#: xpack/plugins/cloud/const.py:21
+msgid "Volcengine"
+msgstr "火山引擎"
+
+#: xpack/plugins/cloud/const.py:23
msgid "VMware"
msgstr "VMware"
-#: xpack/plugins/cloud/const.py:23 xpack/plugins/cloud/providers/nutanix.py:15
+#: xpack/plugins/cloud/const.py:24 xpack/plugins/cloud/providers/nutanix.py:15
msgid "Nutanix"
msgstr "Nutanix"
-#: xpack/plugins/cloud/const.py:24
+#: xpack/plugins/cloud/const.py:25
msgid "Huawei Private Cloud"
msgstr "华为私有云"
-#: xpack/plugins/cloud/const.py:25
+#: xpack/plugins/cloud/const.py:26
msgid "Qingyun Private Cloud"
msgstr "青云私有云"
-#: xpack/plugins/cloud/const.py:26
+#: xpack/plugins/cloud/const.py:27
msgid "CTYun Private Cloud"
msgstr "天翼私有云"
-#: xpack/plugins/cloud/const.py:27
+#: xpack/plugins/cloud/const.py:28
msgid "OpenStack"
msgstr "OpenStack"
-#: xpack/plugins/cloud/const.py:28 xpack/plugins/cloud/providers/zstack.py:21
+#: xpack/plugins/cloud/const.py:29 xpack/plugins/cloud/providers/zstack.py:21
msgid "ZStack"
msgstr "ZStack"
-#: xpack/plugins/cloud/const.py:29
+#: xpack/plugins/cloud/const.py:30
msgid "Fusion Compute"
msgstr "融合计算"
-#: xpack/plugins/cloud/const.py:30
+#: xpack/plugins/cloud/const.py:31
msgid "SCP"
msgstr "深信服SCP"
-#: xpack/plugins/cloud/const.py:31
+#: xpack/plugins/cloud/const.py:32
msgid "Apsara Stack"
msgstr "阿里云专有云"
-#: xpack/plugins/cloud/const.py:36
+#: xpack/plugins/cloud/const.py:37
msgid "Private IP"
msgstr "私有IP"
-#: xpack/plugins/cloud/const.py:37
+#: xpack/plugins/cloud/const.py:38
msgid "Public IP"
msgstr "公网IP"
-#: xpack/plugins/cloud/const.py:41 xpack/plugins/cloud/models.py:303
+#: xpack/plugins/cloud/const.py:42 xpack/plugins/cloud/models.py:299
msgid "Instance name"
msgstr "实例名称"
-#: xpack/plugins/cloud/const.py:42
+#: xpack/plugins/cloud/const.py:43
msgid "Instance name and Partial IP"
msgstr "实例名称和部分IP"
-#: xpack/plugins/cloud/const.py:47
+#: xpack/plugins/cloud/const.py:48
msgid "Succeed"
msgstr "成功"
-#: xpack/plugins/cloud/const.py:51
+#: xpack/plugins/cloud/const.py:52
msgid "Unsync"
msgstr "未同步"
-#: xpack/plugins/cloud/const.py:52
+#: xpack/plugins/cloud/const.py:53
msgid "New Sync"
msgstr "新同步"
-#: xpack/plugins/cloud/const.py:53
+#: xpack/plugins/cloud/const.py:54
msgid "Synced"
msgstr "已同步"
-#: xpack/plugins/cloud/const.py:54
+#: xpack/plugins/cloud/const.py:55
msgid "Released"
msgstr "已释放"
-#: xpack/plugins/cloud/const.py:58
+#: xpack/plugins/cloud/const.py:59
msgid "And"
msgstr "与"
-#: xpack/plugins/cloud/const.py:59
+#: xpack/plugins/cloud/const.py:60
msgid "Or"
msgstr "或"
-#: xpack/plugins/cloud/manager.py:57
+#: xpack/plugins/cloud/manager.py:55 xpack/plugins/cloud/providers/gcp.py:64
+#: xpack/plugins/cloud/providers/huaweicloud.py:34
msgid "Account unavailable"
msgstr "账号无效"
@@ -8546,143 +8710,141 @@ msgstr "云账号"
msgid "Test cloud account"
msgstr "测试云账号"
-#: xpack/plugins/cloud/models.py:92 xpack/plugins/cloud/serializers/task.py:159
+#: xpack/plugins/cloud/models.py:88 xpack/plugins/cloud/serializers/task.py:159
msgid "Regions"
msgstr "地域"
-#: xpack/plugins/cloud/models.py:95
+#: xpack/plugins/cloud/models.py:91
msgid "Hostname strategy"
msgstr "主机名策略"
-#: xpack/plugins/cloud/models.py:100
-#: xpack/plugins/cloud/serializers/task.py:162
+#: xpack/plugins/cloud/models.py:96 xpack/plugins/cloud/serializers/task.py:162
msgid "IP network segment group"
msgstr "IP网段组"
-#: xpack/plugins/cloud/models.py:103
-#: xpack/plugins/cloud/serializers/task.py:167
+#: xpack/plugins/cloud/models.py:99 xpack/plugins/cloud/serializers/task.py:167
msgid "Sync IP type"
msgstr "同步IP类型"
-#: xpack/plugins/cloud/models.py:106
+#: xpack/plugins/cloud/models.py:102
#: xpack/plugins/cloud/serializers/task.py:185
msgid "Always update"
msgstr "总是更新"
-#: xpack/plugins/cloud/models.py:108
+#: xpack/plugins/cloud/models.py:104
msgid "Fully synchronous"
msgstr "完全同步"
-#: xpack/plugins/cloud/models.py:113
+#: xpack/plugins/cloud/models.py:109
msgid "Date last sync"
msgstr "最后同步日期"
-#: xpack/plugins/cloud/models.py:116 xpack/plugins/cloud/models.py:321
-#: xpack/plugins/cloud/models.py:345
+#: xpack/plugins/cloud/models.py:112 xpack/plugins/cloud/models.py:317
+#: xpack/plugins/cloud/models.py:341
msgid "Strategy"
msgstr "策略"
-#: xpack/plugins/cloud/models.py:121 xpack/plugins/cloud/models.py:200
+#: xpack/plugins/cloud/models.py:117 xpack/plugins/cloud/models.py:196
msgid "Sync instance task"
msgstr "同步实例任务"
-#: xpack/plugins/cloud/models.py:211 xpack/plugins/cloud/models.py:263
+#: xpack/plugins/cloud/models.py:207 xpack/plugins/cloud/models.py:259
msgid "Date sync"
msgstr "同步日期"
-#: xpack/plugins/cloud/models.py:215
+#: xpack/plugins/cloud/models.py:211
msgid "Sync instance snapshot"
msgstr "同步实例快照"
-#: xpack/plugins/cloud/models.py:219
+#: xpack/plugins/cloud/models.py:215
msgid "Sync instance task execution"
msgstr "同步实例任务执行"
-#: xpack/plugins/cloud/models.py:243
+#: xpack/plugins/cloud/models.py:239
msgid "Sync task"
msgstr "同步任务"
-#: xpack/plugins/cloud/models.py:247
+#: xpack/plugins/cloud/models.py:243
msgid "Sync instance task history"
msgstr "同步实例任务历史"
-#: xpack/plugins/cloud/models.py:250
+#: xpack/plugins/cloud/models.py:246
msgid "Instance"
msgstr "实例"
-#: xpack/plugins/cloud/models.py:267
+#: xpack/plugins/cloud/models.py:263
msgid "Sync instance detail"
msgstr "同步实例详情"
-#: xpack/plugins/cloud/models.py:279 xpack/plugins/cloud/serializers/task.py:72
+#: xpack/plugins/cloud/models.py:275 xpack/plugins/cloud/serializers/task.py:72
msgid "Rule relation"
msgstr "条件关系"
-#: xpack/plugins/cloud/models.py:288
+#: xpack/plugins/cloud/models.py:284
msgid "Task strategy"
msgstr "任务策略"
-#: xpack/plugins/cloud/models.py:292
+#: xpack/plugins/cloud/models.py:288
msgid "Equal"
msgstr "等于"
-#: xpack/plugins/cloud/models.py:293
+#: xpack/plugins/cloud/models.py:289
msgid "Not Equal"
msgstr "不等于"
-#: xpack/plugins/cloud/models.py:294
+#: xpack/plugins/cloud/models.py:290
msgid "In"
msgstr "在...中"
-#: xpack/plugins/cloud/models.py:295
+#: xpack/plugins/cloud/models.py:291
msgid "Contains"
msgstr "包含"
-#: xpack/plugins/cloud/models.py:296
+#: xpack/plugins/cloud/models.py:292
msgid "Exclude"
msgstr "排除"
-#: xpack/plugins/cloud/models.py:297
+#: xpack/plugins/cloud/models.py:293
msgid "Startswith"
msgstr "以...开头"
-#: xpack/plugins/cloud/models.py:298
+#: xpack/plugins/cloud/models.py:294
msgid "Endswith"
msgstr "以...结尾"
-#: xpack/plugins/cloud/models.py:304
+#: xpack/plugins/cloud/models.py:300
msgid "Instance platform"
msgstr "实例平台"
-#: xpack/plugins/cloud/models.py:305
+#: xpack/plugins/cloud/models.py:301
msgid "Instance address"
msgstr "实例地址"
-#: xpack/plugins/cloud/models.py:312
+#: xpack/plugins/cloud/models.py:308
msgid "Rule attr"
msgstr "规则属性"
-#: xpack/plugins/cloud/models.py:316
+#: xpack/plugins/cloud/models.py:312
msgid "Rule match"
msgstr "规则匹配"
-#: xpack/plugins/cloud/models.py:318
+#: xpack/plugins/cloud/models.py:314
msgid "Rule value"
msgstr "规则值"
-#: xpack/plugins/cloud/models.py:325 xpack/plugins/cloud/serializers/task.py:75
+#: xpack/plugins/cloud/models.py:321 xpack/plugins/cloud/serializers/task.py:75
msgid "Strategy rule"
msgstr "条件"
-#: xpack/plugins/cloud/models.py:340
+#: xpack/plugins/cloud/models.py:336
msgid "Action attr"
msgstr "动作属性"
-#: xpack/plugins/cloud/models.py:342
+#: xpack/plugins/cloud/models.py:338
msgid "Action value"
msgstr "动作值"
-#: xpack/plugins/cloud/models.py:349 xpack/plugins/cloud/serializers/task.py:78
+#: xpack/plugins/cloud/models.py:345 xpack/plugins/cloud/serializers/task.py:78
msgid "Strategy action"
msgstr "动作"
@@ -8778,97 +8940,97 @@ msgstr "中东 (巴林)"
msgid "South America (São Paulo)"
msgstr "南美洲 (圣保罗)"
-#: xpack/plugins/cloud/providers/baiducloud.py:54
+#: xpack/plugins/cloud/providers/baiducloud.py:56
#: xpack/plugins/cloud/providers/jdcloud.py:125
msgid "CN North-Beijing"
msgstr "华北-北京"
-#: xpack/plugins/cloud/providers/baiducloud.py:55
-#: xpack/plugins/cloud/providers/huaweicloud.py:42
+#: xpack/plugins/cloud/providers/baiducloud.py:57
+#: xpack/plugins/cloud/providers/huaweicloud.py:47
#: xpack/plugins/cloud/providers/jdcloud.py:128
msgid "CN South-Guangzhou"
msgstr "华南-广州"
-#: xpack/plugins/cloud/providers/baiducloud.py:56
+#: xpack/plugins/cloud/providers/baiducloud.py:58
msgid "CN East-Suzhou"
msgstr "华东-苏州"
-#: xpack/plugins/cloud/providers/baiducloud.py:57
-#: xpack/plugins/cloud/providers/huaweicloud.py:49
+#: xpack/plugins/cloud/providers/baiducloud.py:59
+#: xpack/plugins/cloud/providers/huaweicloud.py:54
msgid "CN-Hong Kong"
msgstr "中国-香港"
-#: xpack/plugins/cloud/providers/baiducloud.py:58
+#: xpack/plugins/cloud/providers/baiducloud.py:60
msgid "CN Center-Wuhan"
msgstr "华中-武汉"
-#: xpack/plugins/cloud/providers/baiducloud.py:59
+#: xpack/plugins/cloud/providers/baiducloud.py:61
msgid "CN North-Baoding"
msgstr "华北-保定"
-#: xpack/plugins/cloud/providers/baiducloud.py:60
+#: xpack/plugins/cloud/providers/baiducloud.py:62
#: xpack/plugins/cloud/providers/jdcloud.py:127
msgid "CN East-Shanghai"
msgstr "华东-上海"
-#: xpack/plugins/cloud/providers/baiducloud.py:61
-#: xpack/plugins/cloud/providers/huaweicloud.py:51
+#: xpack/plugins/cloud/providers/baiducloud.py:63
+#: xpack/plugins/cloud/providers/huaweicloud.py:56
msgid "AP-Singapore"
msgstr "亚太-新加坡"
-#: xpack/plugins/cloud/providers/huaweicloud.py:39
+#: xpack/plugins/cloud/providers/huaweicloud.py:44
msgid "CN North-Beijing1"
msgstr "华北-北京1"
-#: xpack/plugins/cloud/providers/huaweicloud.py:40
+#: xpack/plugins/cloud/providers/huaweicloud.py:45
msgid "CN North-Beijing4"
msgstr "华北-北京4"
-#: xpack/plugins/cloud/providers/huaweicloud.py:41
+#: xpack/plugins/cloud/providers/huaweicloud.py:46
msgid "CN North-Ulanqab1"
msgstr "华北-乌兰察布一"
-#: xpack/plugins/cloud/providers/huaweicloud.py:43
+#: xpack/plugins/cloud/providers/huaweicloud.py:48
msgid "CN South-Shenzhen"
msgstr "华南-广州"
-#: xpack/plugins/cloud/providers/huaweicloud.py:44
+#: xpack/plugins/cloud/providers/huaweicloud.py:49
msgid "CN South-Guangzhou-InvitationOnly"
msgstr "华南-广州-友好用户环境"
-#: xpack/plugins/cloud/providers/huaweicloud.py:45
+#: xpack/plugins/cloud/providers/huaweicloud.py:50
msgid "CN East-Shanghai2"
msgstr "华东-上海2"
-#: xpack/plugins/cloud/providers/huaweicloud.py:46
+#: xpack/plugins/cloud/providers/huaweicloud.py:51
msgid "CN East-Shanghai1"
msgstr "华东-上海1"
-#: xpack/plugins/cloud/providers/huaweicloud.py:48
+#: xpack/plugins/cloud/providers/huaweicloud.py:53
msgid "CN Southwest-Guiyang1"
msgstr "西南-贵阳1"
-#: xpack/plugins/cloud/providers/huaweicloud.py:50
+#: xpack/plugins/cloud/providers/huaweicloud.py:55
msgid "AP-Bangkok"
msgstr "亚太-曼谷"
-#: xpack/plugins/cloud/providers/huaweicloud.py:53
+#: xpack/plugins/cloud/providers/huaweicloud.py:58
msgid "AF-Johannesburg"
msgstr "非洲-约翰内斯堡"
-#: xpack/plugins/cloud/providers/huaweicloud.py:54
+#: xpack/plugins/cloud/providers/huaweicloud.py:59
msgid "LA-Mexico City1"
msgstr "拉美-墨西哥城一"
-#: xpack/plugins/cloud/providers/huaweicloud.py:55
+#: xpack/plugins/cloud/providers/huaweicloud.py:60
msgid "LA-Santiago"
msgstr "拉美-圣地亚哥"
-#: xpack/plugins/cloud/providers/huaweicloud.py:56
+#: xpack/plugins/cloud/providers/huaweicloud.py:61
msgid "LA-Sao Paulo1"
msgstr "拉美-圣保罗一"
-#: xpack/plugins/cloud/providers/huaweicloud.py:58
+#: xpack/plugins/cloud/providers/huaweicloud.py:63
msgid "TR-Istanbul"
msgstr "TR-Istanbul"
@@ -8876,11 +9038,11 @@ msgstr "TR-Istanbul"
msgid "CN East-Suqian"
msgstr "华东-宿迁"
-#: xpack/plugins/cloud/serializers/account.py:68
+#: xpack/plugins/cloud/serializers/account.py:69
msgid "Validity display"
msgstr "有效性显示"
-#: xpack/plugins/cloud/serializers/account.py:69
+#: xpack/plugins/cloud/serializers/account.py:70
msgid "Provider display"
msgstr "服务商显示"
@@ -9030,14 +9192,10 @@ msgid "Theme"
msgstr "主题"
#: xpack/plugins/interface/models.py:42
-msgid "Beian link"
-msgstr "公安联网备案跳转链接"
+msgid "Footer content"
+msgstr "页脚内容"
-#: xpack/plugins/interface/models.py:43
-msgid "Beian text"
-msgstr "公安联网备案号"
-
-#: xpack/plugins/interface/models.py:46 xpack/plugins/interface/models.py:87
+#: xpack/plugins/interface/models.py:45 xpack/plugins/interface/models.py:86
msgid "Interface setting"
msgstr "界面设置"
@@ -9068,27 +9226,3 @@ msgstr "企业专业版"
#: xpack/plugins/license/models.py:86
msgid "Ultimate edition"
msgstr "企业旗舰版"
-
-#~ msgid "SMTP port"
-#~ msgstr "SMTP 端口"
-
-#~ msgid "SMTP account"
-#~ msgstr "SMTP 账号"
-
-#~ msgid "SMTP password"
-#~ msgstr "SMTP 密码"
-
-#~ msgid "Password can not contains `{{` or `}}`"
-#~ msgstr "密码不能包含 `{{` 或 `}}` 字符"
-
-#~ msgid "Password can not contains `{%` or `%}`"
-#~ msgstr "密码不能包含 `{%` 或 `%}` 字符"
-
-#~ msgid "FeiShu query user failed"
-#~ msgstr "飞书查询用户失败"
-
-#~ msgid "The FeiShu is already bound to another user"
-#~ msgstr "该飞书已经绑定其他用户"
-
-#~ msgid "Binding FeiShu successfully"
-#~ msgstr "绑定 飞书 成功"
diff --git a/apps/locale/zh_Hant/LC_MESSAGES/django.mo b/apps/locale/zh_Hant/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..ea77f43fc
--- /dev/null
+++ b/apps/locale/zh_Hant/LC_MESSAGES/django.mo
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c3f8c484470db52242d6d2a2bcf78f59faa6900c19ba4cba03eb30a4d5e310b7
+size 145278
diff --git a/apps/locale/zh_Hant/LC_MESSAGES/django.po b/apps/locale/zh_Hant/LC_MESSAGES/django.po
new file mode 100644
index 000000000..1e8fad72e
--- /dev/null
+++ b/apps/locale/zh_Hant/LC_MESSAGES/django.po
@@ -0,0 +1,9230 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: JumpServer 0.3.3\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-04-15 17:54+0800\n"
+"PO-Revision-Date: 2021-05-20 10:54+0800\n"
+"Last-Translator: ibuler \n"
+"Language-Team: JumpServer team\n"
+"Language: zh_CN\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.4.3\n"
+"X-ZhConverter: 繁化姬 dict-74c8d060-r1048 @ 2024/04/07 18:19:20 | https://"
+"zhconvert.org\n"
+
+#: accounts/api/automations/base.py:79 tickets/api/ticket.py:132
+msgid "The parameter 'action' must be [{}]"
+msgstr "參數 'action' 必須是 [{}]"
+
+#: accounts/automations/change_secret/manager.py:225
+#, python-format
+msgid "Success: %s, Failed: %s, Total: %s"
+msgstr "成功: %s, 失敗: %s, 總數: %s"
+
+#: accounts/const/account.py:6
+#: accounts/serializers/automations/change_secret.py:34
+#: assets/models/_user.py:24 audits/signal_handlers/login_log.py:34
+#: authentication/confirm/password.py:9 authentication/confirm/password.py:24
+#: authentication/confirm/password.py:26 authentication/forms.py:28
+#: authentication/templates/authentication/login.html:330
+#: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47
+#: settings/serializers/msg.py:35 terminal/serializers/storage.py:123
+#: terminal/serializers/storage.py:142 users/forms/profile.py:21
+#: users/serializers/user.py:109
+#: users/templates/users/_msg_user_created.html:13
+#: users/templates/users/user_password_verify.html:18
+#: xpack/plugins/cloud/serializers/account_attrs.py:28
+msgid "Password"
+msgstr "密碼"
+
+#: accounts/const/account.py:7
+#: accounts/serializers/automations/change_secret.py:35
+#: terminal/serializers/storage.py:124
+msgid "SSH key"
+msgstr "SSH 金鑰"
+
+#: accounts/const/account.py:8 authentication/models/access_key.py:42
+msgid "Access key"
+msgstr "Access key"
+
+#: accounts/const/account.py:9 assets/models/_user.py:48
+#: authentication/backends/passkey/models.py:16
+#: authentication/models/sso_token.py:14 settings/serializers/feature.py:52
+msgid "Token"
+msgstr "Token"
+
+#: accounts/const/account.py:10
+msgid "API key"
+msgstr "API key"
+
+#: accounts/const/account.py:14 common/db/fields.py:235
+#: settings/serializers/terminal.py:14
+msgid "All"
+msgstr "全部"
+
+#: accounts/const/account.py:15 accounts/models/virtual.py:26
+msgid "Manual input"
+msgstr "手動輸入"
+
+#: accounts/const/account.py:16
+msgid "Dynamic user"
+msgstr "同名帳號"
+
+#: accounts/const/account.py:17
+msgid "Anonymous account"
+msgstr "匿名帳號"
+
+#: accounts/const/account.py:18
+msgid "Specified account"
+msgstr "指定帳號"
+
+#: accounts/const/account.py:26 users/models/user.py:752
+msgid "Local"
+msgstr "資料庫"
+
+#: accounts/const/account.py:27
+msgid "Collected"
+msgstr "收集"
+
+#: accounts/const/account.py:28 accounts/serializers/account/account.py:28
+#: settings/serializers/auth/sms.py:79
+msgid "Template"
+msgstr "模板"
+
+#: accounts/const/account.py:32 ops/const.py:46
+msgid "Skip"
+msgstr "跳過"
+
+#: accounts/const/account.py:33 audits/const.py:24 rbac/tree.py:239
+#: templates/_csv_import_export.html:18 templates/_csv_update_modal.html:6
+msgid "Update"
+msgstr "更新"
+
+#: accounts/const/account.py:34 accounts/const/automation.py:109
+#: accounts/serializers/automations/change_secret.py:164 audits/const.py:62
+#: audits/signal_handlers/activity_log.py:33 common/const/choices.py:19
+#: ops/const.py:76 terminal/const.py:79 xpack/plugins/cloud/const.py:47
+msgid "Failed"
+msgstr "失敗"
+
+#: accounts/const/automation.py:24 rbac/tree.py:52
+msgid "Push account"
+msgstr "帳號推送"
+
+#: accounts/const/automation.py:25
+msgid "Change secret"
+msgstr "更改密碼"
+
+#: accounts/const/automation.py:26
+msgid "Verify account"
+msgstr "驗證帳號"
+
+#: accounts/const/automation.py:27 accounts/tasks/remove_account.py:24
+#: accounts/tasks/remove_account.py:33
+msgid "Remove account"
+msgstr "移除帳號"
+
+#: accounts/const/automation.py:28
+msgid "Gather accounts"
+msgstr "收集帳號"
+
+#: accounts/const/automation.py:29
+msgid "Verify gateway account"
+msgstr "驗證網關帳號"
+
+#: accounts/const/automation.py:47
+msgid "Specific secret"
+msgstr "指定"
+
+#: accounts/const/automation.py:48
+msgid "Random generate"
+msgstr "隨機生成"
+
+#: accounts/const/automation.py:52 ops/const.py:13
+msgid "Append SSH KEY"
+msgstr "追加"
+
+#: accounts/const/automation.py:53 ops/const.py:14
+msgid "Empty and append SSH KEY"
+msgstr "清空所有並添加"
+
+#: accounts/const/automation.py:54 ops/const.py:15
+msgid "Replace (Replace only keys pushed by JumpServer) "
+msgstr "替換 (只替換由 JumpServer 推送的金鑰)"
+
+#: accounts/const/automation.py:59
+msgid "On asset create"
+msgstr "資產創建時"
+
+#: accounts/const/automation.py:62
+msgid "On perm add user"
+msgstr "授權變更時添加用戶"
+
+#: accounts/const/automation.py:64
+msgid "On perm add user group"
+msgstr "授權變更時添加用戶組"
+
+#: accounts/const/automation.py:66
+msgid "On perm add asset"
+msgstr "授權變更時添加資產"
+
+#: accounts/const/automation.py:68
+msgid "On perm add node"
+msgstr "授權變更時添加節點"
+
+#: accounts/const/automation.py:70
+msgid "On perm add account"
+msgstr "授權變更時添加帳號"
+
+#: accounts/const/automation.py:72
+msgid "On asset join node"
+msgstr "資產變更時添加到節點"
+
+#: accounts/const/automation.py:74
+msgid "On user join group"
+msgstr "用戶變更時添加到用戶組"
+
+#: accounts/const/automation.py:82
+msgid "On perm change"
+msgstr "授權變更時"
+
+#: accounts/const/automation.py:89
+msgid "Inherit from group or node"
+msgstr "繼承自用戶組或資產節點"
+
+#: accounts/const/automation.py:97
+msgid "Create and push"
+msgstr "創建並推送"
+
+#: accounts/const/automation.py:98
+msgid "Only create"
+msgstr "僅創建"
+
+#: accounts/const/automation.py:103
+#: authentication/serializers/password_mfa.py:16
+#: authentication/serializers/password_mfa.py:24
+#: notifications/backends/__init__.py:10 settings/serializers/msg.py:22
+#: settings/serializers/msg.py:64 users/forms/profile.py:100
+#: users/forms/profile.py:108 users/models/user.py:816
+#: users/templates/users/forgot_password.html:162
+#: users/views/profile/reset.py:94
+msgid "Email"
+msgstr "信箱"
+
+#: accounts/const/automation.py:105 terminal/const.py:87
+msgid "SFTP"
+msgstr "SFTP"
+
+#: accounts/const/automation.py:110
+#: accounts/serializers/automations/change_secret.py:163 audits/const.py:61
+#: audits/models.py:64 audits/signal_handlers/activity_log.py:33
+#: common/const/choices.py:18 ops/const.py:74 ops/serializers/celery.py:46
+#: terminal/const.py:78 terminal/models/session/sharing.py:121
+#: tickets/views/approve.py:128
+msgid "Success"
+msgstr "成功"
+
+#: accounts/const/automation.py:111 common/const/choices.py:16
+#: terminal/const.py:77 tickets/const.py:29 tickets/const.py:38
+msgid "Pending"
+msgstr "待定的"
+
+#: accounts/const/vault.py:8 assets/const/category.py:12
+#: assets/models/asset/database.py:9 assets/models/asset/database.py:24
+msgid "Database"
+msgstr "資料庫"
+
+#: accounts/const/vault.py:9 settings/serializers/feature.py:43
+msgid "HCP Vault"
+msgstr "HashiCorp Vault"
+
+#: accounts/mixins.py:35
+msgid "Export all"
+msgstr "匯出所有"
+
+#: accounts/mixins.py:37
+msgid "Export only selected items"
+msgstr "僅匯出選擇項"
+
+#: accounts/mixins.py:42
+#, python-format
+msgid "Export filtered: %s"
+msgstr "匯出搜素: %s"
+
+#: accounts/mixins.py:48
+#, python-format
+msgid "User %s view/export secret"
+msgstr "用戶 %s 查看/匯出 了密碼"
+
+#: accounts/models/account.py:49
+#: accounts/models/automations/gather_account.py:16
+#: accounts/serializers/account/account.py:213
+#: accounts/serializers/account/account.py:258
+#: accounts/serializers/account/gathered_account.py:10
+#: accounts/serializers/automations/change_secret.py:108
+#: accounts/serializers/automations/change_secret.py:140
+#: accounts/templates/accounts/asset_account_change_info.html:7
+#: accounts/templates/accounts/change_secret_failed_info.html:11
+#: acls/serializers/base.py:123 assets/models/asset/common.py:95
+#: assets/models/asset/common.py:350 assets/models/cmd_filter.py:36
+#: audits/models.py:58 authentication/models/connection_token.py:36
+#: perms/models/asset_permission.py:69 perms/serializers/permission.py:36
+#: terminal/backends/command/models.py:17 terminal/models/session/session.py:32
+#: terminal/notifications.py:155 terminal/serializers/command.py:17
+#: terminal/serializers/session.py:28
+#: terminal/templates/terminal/_msg_command_warning.html:4
+#: terminal/templates/terminal/_msg_session_sharing.html:4
+#: tickets/models/ticket/apply_asset.py:16 xpack/plugins/cloud/models.py:252
+msgid "Asset"
+msgstr "資產"
+
+#: accounts/models/account.py:53 accounts/models/template.py:16
+#: accounts/serializers/account/account.py:220
+#: accounts/serializers/account/account.py:268
+#: accounts/serializers/account/template.py:27
+#: authentication/serializers/connect_token_secret.py:50
+msgid "Su from"
+msgstr "切換自"
+
+#: accounts/models/account.py:55 assets/const/protocol.py:177
+#: settings/serializers/auth/cas.py:20 terminal/models/applet/applet.py:35
+#: terminal/models/virtualapp/virtualapp.py:21
+msgid "Version"
+msgstr "版本"
+
+#: accounts/models/account.py:57 accounts/serializers/account/account.py:215
+#: users/models/user.py:859
+msgid "Source"
+msgstr "來源"
+
+#: accounts/models/account.py:58
+msgid "Source ID"
+msgstr "來源 ID"
+
+#: accounts/models/account.py:61
+#: accounts/serializers/automations/change_secret.py:110
+#: accounts/serializers/automations/change_secret.py:141
+#: accounts/templates/accounts/change_secret_failed_info.html:12
+#: acls/serializers/base.py:124 acls/templates/acls/asset_login_reminder.html:7
+#: assets/serializers/asset/common.py:128 assets/serializers/gateway.py:28
+#: audits/models.py:59 authentication/api/connection_token.py:411
+#: ops/models/base.py:18 perms/models/asset_permission.py:75
+#: perms/serializers/permission.py:41 settings/serializers/msg.py:33
+#: terminal/backends/command/models.py:18 terminal/models/session/session.py:34
+#: terminal/templates/terminal/_msg_command_warning.html:8
+#: terminal/templates/terminal/_msg_session_sharing.html:8
+#: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:85
+msgid "Account"
+msgstr "帳號"
+
+#: accounts/models/account.py:67
+msgid "Can view asset account secret"
+msgstr "可以查看資產帳號密碼"
+
+#: accounts/models/account.py:68
+msgid "Can view asset history account"
+msgstr "可以查看資產歷史帳號"
+
+#: accounts/models/account.py:69
+msgid "Can view asset history account secret"
+msgstr "可以查看資產歷史帳號密碼"
+
+#: accounts/models/account.py:70
+msgid "Can verify account"
+msgstr "可以驗證帳號"
+
+#: accounts/models/account.py:71
+msgid "Can push account"
+msgstr "可以推送帳號"
+
+#: accounts/models/account.py:72
+msgid "Can remove account"
+msgstr "可以移除帳號"
+
+#: accounts/models/automations/backup_account.py:27
+msgid "Backup Type"
+msgstr "備份類型"
+
+#: accounts/models/automations/backup_account.py:28
+#: accounts/models/automations/backup_account.py:29
+msgid "Is Password Divided"
+msgstr "金鑰是否拆分成前後兩部分"
+
+#: accounts/models/automations/backup_account.py:32
+msgid "Recipient part one"
+msgstr "收件人部分一"
+
+#: accounts/models/automations/backup_account.py:36
+msgid "Recipient part two"
+msgstr "收件人部分二"
+
+#: accounts/models/automations/backup_account.py:40
+msgid "Object Storage Recipient part one"
+msgstr "接收伺服器一"
+
+#: accounts/models/automations/backup_account.py:44
+msgid "Object Storage Recipient part two"
+msgstr "接收伺服器二"
+
+#: accounts/models/automations/backup_account.py:47
+#: accounts/serializers/account/backup.py:20
+msgid "Zip Encrypt Password"
+msgstr "文件加密密碼"
+
+#: accounts/models/automations/backup_account.py:55
+#: accounts/models/automations/backup_account.py:138
+msgid "Account backup plan"
+msgstr "帳號備份計劃"
+
+#: accounts/models/automations/backup_account.py:119
+#: assets/models/automations/base.py:115 audits/models.py:65
+#: ops/models/base.py:55 ops/models/celery.py:88 ops/models/job.py:237
+#: ops/templates/ops/celery_task_log.html:75
+#: perms/models/asset_permission.py:78
+#: settings/templates/ldap/_msg_import_ldap_user.html:5
+#: terminal/models/applet/host.py:141 terminal/models/session/session.py:45
+#: tickets/models/ticket/apply_application.py:30
+#: tickets/models/ticket/apply_asset.py:19
+msgid "Date start"
+msgstr "開始日期"
+
+#: accounts/models/automations/backup_account.py:122
+#: authentication/templates/authentication/_msg_oauth_bind.html:11
+#: notifications/notifications.py:186
+#: settings/templates/ldap/_msg_import_ldap_user.html:3
+msgid "Time"
+msgstr "時間"
+
+#: accounts/models/automations/backup_account.py:126
+msgid "Account backup snapshot"
+msgstr "帳號備份快照"
+
+#: accounts/models/automations/backup_account.py:130
+#: accounts/serializers/account/backup.py:49
+#: accounts/serializers/automations/base.py:56
+#: assets/models/automations/base.py:122
+#: assets/serializers/automations/base.py:40
+msgid "Trigger mode"
+msgstr "觸發模式"
+
+#: accounts/models/automations/backup_account.py:133 audits/models.py:203
+#: terminal/models/session/sharing.py:125 xpack/plugins/cloud/models.py:204
+msgid "Reason"
+msgstr "原因"
+
+#: accounts/models/automations/backup_account.py:135
+#: accounts/serializers/automations/change_secret.py:107
+#: accounts/serializers/automations/change_secret.py:142
+#: ops/serializers/job.py:71 terminal/serializers/session.py:52
+msgid "Is success"
+msgstr "是否成功"
+
+#: accounts/models/automations/backup_account.py:143
+msgid "Account backup execution"
+msgstr "帳號備份執行"
+
+#: accounts/models/automations/base.py:18
+msgid "Account automation task"
+msgstr "帳號自動化任務"
+
+#: accounts/models/automations/base.py:32
+msgid "Automation execution"
+msgstr "自動化執行"
+
+#: accounts/models/automations/base.py:33
+msgid "Automation executions"
+msgstr "自動化執行"
+
+#: accounts/models/automations/base.py:35
+msgid "Can view change secret execution"
+msgstr "查看改密執行"
+
+#: accounts/models/automations/base.py:36
+msgid "Can add change secret execution"
+msgstr "創建改密執行"
+
+#: accounts/models/automations/base.py:38
+msgid "Can view gather accounts execution"
+msgstr "查看收集帳號執行"
+
+#: accounts/models/automations/base.py:39
+msgid "Can add gather accounts execution"
+msgstr "創建收集帳號執行"
+
+#: accounts/models/automations/base.py:41
+msgid "Can view push account execution"
+msgstr "查看推送帳號執行"
+
+#: accounts/models/automations/base.py:42
+msgid "Can add push account execution"
+msgstr "創建推送帳號執行"
+
+#: accounts/models/automations/base.py:54
+msgid "SSH key change strategy"
+msgstr "SSH 金鑰推送方式"
+
+#: accounts/models/automations/change_secret.py:15
+#: accounts/models/automations/gather_account.py:58
+#: accounts/serializers/account/backup.py:41
+#: accounts/serializers/automations/change_secret.py:58
+#: settings/serializers/auth/ldap.py:90
+msgid "Recipient"
+msgstr "收件人"
+
+#: accounts/models/automations/change_secret.py:22
+msgid "Change secret automation"
+msgstr "自動化改密"
+
+#: accounts/models/automations/change_secret.py:39
+msgid "Old secret"
+msgstr "原金鑰"
+
+#: accounts/models/automations/change_secret.py:40
+msgid "New secret"
+msgstr "新金鑰"
+
+#: accounts/models/automations/change_secret.py:41
+msgid "Date started"
+msgstr "開始日期"
+
+#: accounts/models/automations/change_secret.py:42
+#: assets/models/automations/base.py:116 ops/models/base.py:56
+#: ops/models/celery.py:89 ops/models/job.py:238
+#: terminal/models/applet/host.py:142
+msgid "Date finished"
+msgstr "結束日期"
+
+#: accounts/models/automations/change_secret.py:44
+#: assets/models/automations/base.py:113 audits/models.py:208
+#: audits/serializers.py:54 ops/models/base.py:49 ops/models/job.py:229
+#: terminal/models/applet/applet.py:320 terminal/models/applet/host.py:140
+#: terminal/models/component/status.py:30
+#: terminal/models/virtualapp/virtualapp.py:99
+#: terminal/serializers/applet.py:18 terminal/serializers/applet_host.py:136
+#: terminal/serializers/virtualapp.py:35 tickets/models/ticket/general.py:281
+#: tickets/serializers/super_ticket.py:13
+#: tickets/serializers/ticket/ticket.py:20 xpack/plugins/cloud/models.py:200
+#: xpack/plugins/cloud/models.py:256
+msgid "Status"
+msgstr "狀態"
+
+#: accounts/models/automations/change_secret.py:47
+#: accounts/serializers/account/account.py:260
+#: accounts/templates/accounts/change_secret_failed_info.html:13
+#: assets/const/automation.py:8
+#: authentication/templates/authentication/passkey.html:173
+#: authentication/views/base.py:42 authentication/views/base.py:43
+#: authentication/views/base.py:44 common/const/choices.py:20
+#: settings/templates/ldap/_msg_import_ldap_user.html:26
+msgid "Error"
+msgstr "錯誤"
+
+#: accounts/models/automations/change_secret.py:51
+msgid "Change secret record"
+msgstr "改密記錄"
+
+#: accounts/models/automations/gather_account.py:14
+msgid "Present"
+msgstr "存在"
+
+#: accounts/models/automations/gather_account.py:15
+msgid "Date last login"
+msgstr "最後登錄日期"
+
+#: accounts/models/automations/gather_account.py:17
+#: accounts/models/automations/push_account.py:15 accounts/models/base.py:65
+#: accounts/serializers/account/virtual.py:21 acls/serializers/base.py:19
+#: acls/serializers/base.py:50 assets/models/_user.py:23 audits/models.py:188
+#: authentication/forms.py:21 authentication/forms.py:23
+#: authentication/models/temp_token.py:9
+#: authentication/templates/authentication/_msg_different_city.html:9
+#: authentication/templates/authentication/_msg_oauth_bind.html:9
+#: terminal/serializers/storage.py:136 users/forms/profile.py:31
+#: users/forms/profile.py:114 users/models/user.py:812
+#: users/templates/users/_msg_user_created.html:12
+#: xpack/plugins/cloud/serializers/account_attrs.py:26
+msgid "Username"
+msgstr "使用者名稱"
+
+#: accounts/models/automations/gather_account.py:18
+msgid "Address last login"
+msgstr "最後登錄地址"
+
+#: accounts/models/automations/gather_account.py:44
+msgid "Gather account automation"
+msgstr "自動化收集帳號"
+
+#: accounts/models/automations/gather_account.py:56
+msgid "Is sync account"
+msgstr "是否同步帳號"
+
+#: accounts/models/automations/gather_account.py:75
+#: accounts/tasks/gather_accounts.py:29
+msgid "Gather asset accounts"
+msgstr "收集帳號"
+
+#: accounts/models/automations/push_account.py:14
+msgid "Triggers"
+msgstr "觸發方式"
+
+#: accounts/models/automations/push_account.py:16 acls/models/base.py:41
+#: acls/serializers/base.py:57 assets/models/cmd_filter.py:81
+#: audits/models.py:92 audits/serializers.py:84
+#: authentication/serializers/connect_token_secret.py:119
+#: authentication/templates/authentication/_access_key_modal.html:34
+#: tickets/serializers/ticket/ticket.py:21
+msgid "Action"
+msgstr "動作"
+
+#: accounts/models/automations/push_account.py:57
+msgid "Push asset account"
+msgstr "帳號推送"
+
+#: accounts/models/automations/verify_account.py:15
+msgid "Verify asset account"
+msgstr "帳號驗證"
+
+#: accounts/models/base.py:37 accounts/models/base.py:67
+#: accounts/serializers/account/account.py:440
+#: accounts/serializers/account/base.py:17
+#: accounts/serializers/automations/change_secret.py:47
+#: authentication/serializers/connect_token_secret.py:42
+#: authentication/serializers/connect_token_secret.py:51
+#: terminal/serializers/storage.py:140
+msgid "Secret type"
+msgstr "密文類型"
+
+#: accounts/models/base.py:39 accounts/models/mixins/vault.py:49
+#: accounts/serializers/account/base.py:20
+#: authentication/models/temp_token.py:10
+#: authentication/templates/authentication/_access_key_modal.html:31
+#: settings/serializers/auth/radius.py:19
+msgid "Secret"
+msgstr "金鑰"
+
+#: accounts/models/base.py:42
+#: accounts/serializers/automations/change_secret.py:41
+msgid "Secret strategy"
+msgstr "密文策略"
+
+#: accounts/models/base.py:44 accounts/serializers/account/template.py:24
+#: accounts/serializers/automations/change_secret.py:46
+msgid "Password rules"
+msgstr "密碼規則"
+
+#: accounts/models/base.py:64 accounts/serializers/account/virtual.py:20
+#: acls/models/base.py:35 acls/models/base.py:96 acls/models/command_acl.py:21
+#: acls/serializers/base.py:35 applications/models.py:9
+#: assets/models/_user.py:22 assets/models/asset/common.py:93
+#: assets/models/asset/common.py:159 assets/models/cmd_filter.py:21
+#: assets/models/domain.py:19 assets/models/group.py:17
+#: assets/models/label.py:18 assets/models/platform.py:16
+#: assets/models/platform.py:95 assets/serializers/asset/common.py:149
+#: assets/serializers/platform.py:118 assets/serializers/platform.py:228
+#: authentication/backends/passkey/models.py:10
+#: authentication/serializers/connect_token_secret.py:113
+#: authentication/serializers/connect_token_secret.py:168 labels/models.py:11
+#: ops/mixin.py:21 ops/models/adhoc.py:20 ops/models/celery.py:15
+#: ops/models/celery.py:80 ops/models/job.py:138 ops/models/playbook.py:28
+#: ops/serializers/job.py:18 orgs/models.py:82
+#: perms/models/asset_permission.py:61 rbac/models/role.py:29
+#: settings/models.py:33 settings/models.py:181 settings/serializers/msg.py:89
+#: terminal/models/applet/applet.py:33 terminal/models/component/endpoint.py:12
+#: terminal/models/component/endpoint.py:109
+#: terminal/models/component/storage.py:26 terminal/models/component/task.py:13
+#: terminal/models/component/terminal.py:85
+#: terminal/models/virtualapp/provider.py:10
+#: terminal/models/virtualapp/virtualapp.py:19 tickets/api/ticket.py:87
+#: users/forms/profile.py:32 users/models/group.py:13
+#: users/models/preference.py:11 users/models/user.py:814
+#: xpack/plugins/cloud/models.py:32 xpack/plugins/cloud/models.py:272
+#: xpack/plugins/cloud/serializers/task.py:70
+msgid "Name"
+msgstr "名稱"
+
+#: accounts/models/base.py:69
+msgid "Privileged"
+msgstr "特權帳號"
+
+#: accounts/models/base.py:70 assets/models/asset/common.py:166
+#: assets/models/automations/base.py:21 assets/models/cmd_filter.py:39
+#: assets/models/label.py:22
+#: authentication/serializers/connect_token_secret.py:117
+#: terminal/models/applet/applet.py:40
+#: terminal/models/component/endpoint.py:120
+#: terminal/models/virtualapp/virtualapp.py:23 users/serializers/user.py:173
+msgid "Is active"
+msgstr "啟用"
+
+#: accounts/models/template.py:18 assets/models/_user.py:53
+msgid "Auto push"
+msgstr "自動推送"
+
+#: accounts/models/template.py:21
+msgid "Platforms"
+msgstr "系統平台"
+
+#: accounts/models/template.py:23
+msgid "Push params"
+msgstr "帳號推送參數"
+
+#: accounts/models/template.py:26 xpack/plugins/cloud/models.py:329
+msgid "Account template"
+msgstr "帳號模板"
+
+#: accounts/models/template.py:31
+msgid "Can view asset account template secret"
+msgstr "可以查看資產帳號模板密碼"
+
+#: accounts/models/template.py:32
+msgid "Can change asset account template secret"
+msgstr "可以更改資產帳號模板密碼"
+
+# msgid "Can view asset account template secret"
+# msgstr "可以查看資產帳號模板密碼"
+# msgid "Can change asset account template secret"
+# msgstr "可以更改資產帳號模板密碼"
+#: accounts/models/virtual.py:13
+msgid "Alias"
+msgstr "別名"
+
+#: accounts/models/virtual.py:14
+msgid "Secret from login"
+msgstr "與用戶登錄時相同"
+
+#: accounts/models/virtual.py:27
+msgid "Same with user"
+msgstr "使用者名稱與用戶相同"
+
+#: accounts/models/virtual.py:36
+msgid "Non-asset account, Input username/password on connect"
+msgstr "登錄時手動輸入 使用者名稱/密碼 來連接的帳號"
+
+#: accounts/models/virtual.py:37
+msgid "The account username name same with user on connect"
+msgstr "登錄資產時,帳號使用者名稱與使用者使用者名稱相同的帳號"
+
+#: accounts/models/virtual.py:38
+msgid ""
+"Connect asset without using a username and password, and it only supports "
+"web-based and custom-type assets"
+msgstr ""
+"連接資產時不使用使用者名稱和密碼的帳號,僅支持 web類型 和 自訂類型 的資產"
+
+#: accounts/notifications.py:12 accounts/notifications.py:37
+msgid "Notification of account backup route task results"
+msgstr "帳號備份任務結果通知"
+
+#: accounts/notifications.py:22 accounts/notifications.py:46
+msgid ""
+"{} - The account backup passage task has been completed. See the attachment "
+"for details"
+msgstr "{} - 帳號備份任務已完成, 詳情見附件"
+
+#: accounts/notifications.py:25
+msgid ""
+"{} - The account backup passage task has been completed: the encryption "
+"password has not been set - please go to personal information -> Basic file "
+"encryption password for preference settings"
+msgstr ""
+"{} - 帳號備份任務已完成: 未設置加密密碼 - 請前往個人資訊 -> 偏好設置的基本中"
+"設置文件加密密碼"
+
+#: accounts/notifications.py:56
+msgid "Notification of implementation result of encryption change plan"
+msgstr "改密計劃任務結果通知"
+
+#: accounts/notifications.py:67
+msgid ""
+"{} - The encryption change task has been completed. See the attachment for "
+"details"
+msgstr "{} - 改密任務已完成, 詳情見附件"
+
+#: accounts/notifications.py:71
+msgid ""
+"{} - The encryption change task has been completed: the encryption password "
+"has not been set - please go to personal information -> set encryption "
+"password in preferences"
+msgstr ""
+"{} - 改密任務已完成: 未設置加密密碼 - 請前往個人資訊 -> 偏好設置中設置加密密"
+"碼"
+
+#: accounts/notifications.py:83
+#: accounts/templates/accounts/asset_account_change_info.html:3
+msgid "Gather account change information"
+msgstr "帳號變更資訊"
+
+#: accounts/notifications.py:105
+msgid "Change secret or push account failed information"
+msgstr "改密或推送帳號失敗資訊"
+
+#: accounts/serializers/account/account.py:31
+msgid "Push now"
+msgstr "立即推送"
+
+#: accounts/serializers/account/account.py:38
+msgid "Exist policy"
+msgstr "帳號存在策略"
+
+#: accounts/serializers/account/account.py:193 applications/models.py:11
+#: assets/models/label.py:21 assets/models/platform.py:96
+#: assets/serializers/asset/common.py:125 assets/serializers/cagegory.py:12
+#: assets/serializers/platform.py:140 assets/serializers/platform.py:229
+#: perms/serializers/user_permission.py:26 settings/models.py:35
+#: tickets/models/ticket/apply_application.py:13 users/models/preference.py:12
+msgid "Category"
+msgstr "類別"
+
+#: accounts/serializers/account/account.py:194
+#: accounts/serializers/automations/base.py:55 acls/models/command_acl.py:24
+#: acls/serializers/command_acl.py:19 applications/models.py:14
+#: assets/models/_user.py:50 assets/models/automations/base.py:20
+#: assets/models/cmd_filter.py:74 assets/models/platform.py:97
+#: assets/serializers/asset/common.py:126 assets/serializers/platform.py:120
+#: assets/serializers/platform.py:139 audits/serializers.py:53
+#: audits/serializers.py:170
+#: authentication/serializers/connect_token_secret.py:126 ops/models/job.py:146
+#: perms/serializers/user_permission.py:27 terminal/models/applet/applet.py:39
+#: terminal/models/component/storage.py:57
+#: terminal/models/component/storage.py:146 terminal/serializers/applet.py:29
+#: terminal/serializers/session.py:23 terminal/serializers/storage.py:264
+#: terminal/serializers/storage.py:276 tickets/models/comment.py:26
+#: tickets/models/flow.py:56 tickets/models/ticket/apply_application.py:16
+#: tickets/models/ticket/general.py:273 tickets/serializers/flow.py:53
+#: tickets/serializers/ticket/ticket.py:19
+msgid "Type"
+msgstr "類型"
+
+#: accounts/serializers/account/account.py:209
+msgid "Asset not found"
+msgstr "資產不存在"
+
+#: accounts/serializers/account/account.py:249
+msgid "Has secret"
+msgstr "已託管密碼"
+
+#: accounts/serializers/account/account.py:259 ops/models/celery.py:83
+#: tickets/models/comment.py:13 tickets/models/ticket/general.py:46
+#: tickets/models/ticket/general.py:277 tickets/serializers/super_ticket.py:14
+msgid "State"
+msgstr "狀態"
+
+#: accounts/serializers/account/account.py:261
+msgid "Changed"
+msgstr "已修改"
+
+#: accounts/serializers/account/account.py:271
+#: accounts/serializers/automations/base.py:22 acls/models/base.py:97
+#: acls/templates/acls/asset_login_reminder.html:6
+#: assets/models/automations/base.py:19
+#: assets/serializers/automations/base.py:20
+#: authentication/api/connection_token.py:410 ops/models/base.py:17
+#: ops/models/job.py:148 ops/serializers/job.py:19
+#: terminal/templates/terminal/_msg_command_execute_alert.html:16
+msgid "Assets"
+msgstr "資產"
+
+#: accounts/serializers/account/account.py:326
+msgid "Account already exists"
+msgstr "帳號已存在"
+
+#: accounts/serializers/account/account.py:376
+#, python-format
+msgid "Asset does not support this secret type: %s"
+msgstr "資產不支持帳號類型: %s"
+
+#: accounts/serializers/account/account.py:408
+msgid "Account has exist"
+msgstr "帳號已存在"
+
+#: accounts/serializers/account/account.py:441
+#: authentication/serializers/connect_token_secret.py:159
+#: authentication/templates/authentication/_access_key_modal.html:30
+#: perms/models/perm_node.py:21 users/serializers/group.py:33
+msgid "ID"
+msgstr "ID"
+
+#: accounts/serializers/account/account.py:451 acls/serializers/base.py:116
+#: acls/templates/acls/asset_login_reminder.html:5
+#: acls/templates/acls/user_login_reminder.html:5
+#: assets/models/cmd_filter.py:24 assets/models/label.py:16 audits/models.py:54
+#: audits/models.py:90 audits/models.py:172 audits/models.py:269
+#: audits/serializers.py:171 authentication/models/connection_token.py:32
+#: authentication/models/sso_token.py:16
+#: notifications/models/notification.py:12
+#: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:63
+#: perms/serializers/permission.py:32 rbac/builtin.py:124
+#: rbac/models/rolebinding.py:49 rbac/serializers/rolebinding.py:17
+#: terminal/backends/command/models.py:16 terminal/models/session/session.py:30
+#: terminal/models/session/sharing.py:34 terminal/notifications.py:156
+#: terminal/notifications.py:205 terminal/serializers/command.py:16
+#: terminal/templates/terminal/_msg_command_warning.html:6
+#: terminal/templates/terminal/_msg_session_sharing.html:6
+#: tickets/models/comment.py:21 users/const.py:14 users/models/user.py:1019
+#: users/models/user.py:1057 users/serializers/group.py:21
+msgid "User"
+msgstr "用戶"
+
+#: accounts/serializers/account/account.py:452
+#: authentication/templates/authentication/_access_key_modal.html:33
+#: terminal/notifications.py:158 terminal/notifications.py:207
+msgid "Date"
+msgstr "日期"
+
+#: accounts/serializers/account/backup.py:38
+#: accounts/serializers/automations/base.py:24
+#: accounts/serializers/automations/base.py:37
+#: assets/serializers/automations/base.py:34 ops/mixin.py:23 ops/mixin.py:104
+#: settings/serializers/auth/ldap.py:66
+msgid "Periodic perform"
+msgstr "定時執行"
+
+#: accounts/serializers/account/backup.py:39
+#: accounts/serializers/automations/base.py:38
+msgid "Executed amount"
+msgstr "執行次數"
+
+#: accounts/serializers/account/backup.py:42
+#: accounts/serializers/automations/change_secret.py:59
+msgid "Currently only mail sending is supported"
+msgstr "當前只支持郵件發送"
+
+#: accounts/serializers/account/backup.py:44
+msgid "Asset type"
+msgstr "資產類型"
+
+#: accounts/serializers/account/base.py:25 terminal/serializers/storage.py:149
+msgid "Key password"
+msgstr "金鑰密碼"
+
+#: accounts/serializers/account/base.py:78
+#: assets/serializers/asset/common.py:384
+msgid "Spec info"
+msgstr "特殊資訊"
+
+#: accounts/serializers/account/base.py:80
+msgid ""
+"Tip: If no username is required for authentication, fill in `null`, If AD "
+"account, like `username@domain`"
+msgstr ""
+"提示: 如果認證時不需要使用者名稱,可填寫為 null, 如果是 AD 帳號,格式為 "
+"username@domain"
+
+#: accounts/serializers/account/template.py:13
+msgid "Password length"
+msgstr "密碼長度"
+
+#: accounts/serializers/account/template.py:14
+msgid "Lowercase"
+msgstr "小寫字母"
+
+#: accounts/serializers/account/template.py:15
+msgid "Uppercase"
+msgstr "大寫字母"
+
+#: accounts/serializers/account/template.py:16
+msgid "Digit"
+msgstr "數字"
+
+#: accounts/serializers/account/template.py:17
+msgid "Special symbol"
+msgstr "特殊字元"
+
+#: accounts/serializers/account/template.py:19
+msgid "Exclude symbol"
+msgstr "排除字元"
+
+#: accounts/serializers/account/template.py:38
+msgid "Secret generation strategy for account creation"
+msgstr "密碼生成策略,用於帳號創建時,設置密碼"
+
+#: accounts/serializers/account/template.py:39
+msgid "Whether to automatically push the account to the asset"
+msgstr "是否自動推送帳號到資產"
+
+#: accounts/serializers/account/template.py:42
+msgid ""
+"Associated platform, you can configure push parameters. If not associated, "
+"default parameters will be used"
+msgstr "關聯平台,可配置推送參數,如果不關聯,將使用默認參數"
+
+#: accounts/serializers/account/virtual.py:19 assets/models/_user.py:27
+#: assets/models/cmd_filter.py:40 assets/models/cmd_filter.py:88
+#: assets/models/group.py:20 common/db/models.py:36 ops/models/adhoc.py:26
+#: ops/models/job.py:154 ops/models/playbook.py:31 rbac/models/role.py:37
+#: settings/models.py:38 terminal/models/applet/applet.py:45
+#: terminal/models/applet/applet.py:321 terminal/models/applet/host.py:143
+#: terminal/models/component/endpoint.py:25
+#: terminal/models/component/endpoint.py:119
+#: terminal/models/session/session.py:47
+#: terminal/models/virtualapp/virtualapp.py:28 tickets/models/comment.py:32
+#: tickets/models/ticket/general.py:295 users/models/user.py:850
+#: xpack/plugins/cloud/models.py:39 xpack/plugins/cloud/models.py:106
+msgid "Comment"
+msgstr "備註"
+
+#: accounts/serializers/account/virtual.py:24
+msgid ""
+"Current only support login from AD/LDAP. Secret priority: Same account in "
+"asset secret > Login secret > Manual input.
For security, please set "
+"config CACHE_LOGIN_PASSWORD_ENABLED to true"
+msgstr ""
+"當前僅支持 AD/LDAP 登錄方式用戶。 同名帳號密碼生效順序: 資產上存在的同名帳號"
+"密碼 > 登錄密碼 > 手動輸入
為了安全起見,請設置配置項 "
+"CACHE_LOGIN_PASSWORD_ENABLED=true,重啟服務才能開啟"
+
+#: accounts/serializers/automations/base.py:23
+#: assets/serializers/automations/base.py:21
+msgid "Nodes"
+msgstr "節點"
+
+#: accounts/serializers/automations/base.py:45
+msgid "Name already exists"
+msgstr "名稱已存在"
+
+#: accounts/serializers/automations/base.py:54
+#: assets/models/automations/base.py:118
+#: assets/serializers/automations/base.py:39
+msgid "Automation snapshot"
+msgstr "自動化快照"
+
+#: accounts/serializers/automations/change_secret.py:44
+msgid "SSH Key strategy"
+msgstr "SSH 金鑰更改方式"
+
+#: accounts/serializers/automations/change_secret.py:81
+msgid "* Please enter the correct password length"
+msgstr "* 請輸入正確的密碼長度"
+
+#: accounts/serializers/automations/change_secret.py:85
+msgid "* Password length range 6-30 bits"
+msgstr "* 密碼長度範圍 6-30 位"
+
+#: accounts/serializers/automations/change_secret.py:114
+#: assets/models/automations/base.py:127
+msgid "Automation task execution"
+msgstr "自動化任務執行歷史"
+
+#: accounts/signal_handlers.py:47
+#, python-format
+msgid "Push related accounts to assets: %s, by system"
+msgstr "推送帳號到資產: %s, 由系統執行"
+
+#: accounts/signal_handlers.py:56
+#, python-format
+msgid "Add account: %s"
+msgstr "添加帳號: %s"
+
+#: accounts/signal_handlers.py:58
+#, python-format
+msgid "Delete account: %s"
+msgstr "刪除帳號: %s"
+
+#: accounts/tasks/automation.py:25
+msgid "Account execute automation"
+msgstr "帳號執行自動化"
+
+#: accounts/tasks/automation.py:51 accounts/tasks/automation.py:56
+msgid "Execute automation record"
+msgstr "自動化執行記錄"
+
+#: accounts/tasks/backup_account.py:25
+msgid "Execute account backup plan"
+msgstr "執行帳號備份計劃"
+
+#: accounts/tasks/gather_accounts.py:34
+msgid "Gather assets accounts"
+msgstr "收集資產上的帳號"
+
+#: accounts/tasks/push_account.py:15 accounts/tasks/push_account.py:23
+msgid "Push accounts to assets"
+msgstr "推送帳號到資產"
+
+#: accounts/tasks/remove_account.py:44
+msgid "Clean historical accounts"
+msgstr "清理歷史帳號"
+
+#: accounts/tasks/remove_account.py:76
+msgid "Remove historical accounts that are out of range."
+msgstr "刪除超出範圍的歷史帳戶。"
+
+#: accounts/tasks/template.py:11
+msgid "Template sync info to related accounts"
+msgstr "同步資訊到關聯的帳號"
+
+#: accounts/tasks/vault.py:31
+msgid "Sync secret to vault"
+msgstr "同步密文到 vault"
+
+#: accounts/tasks/verify_account.py:49
+msgid "Verify asset account availability"
+msgstr "驗證資產帳號可用性"
+
+#: accounts/tasks/verify_account.py:55
+msgid "Verify accounts connectivity"
+msgstr "測試帳號可連接性"
+
+#: accounts/templates/accounts/asset_account_change_info.html:8
+msgid "Added account"
+msgstr "新增帳號"
+
+#: accounts/templates/accounts/asset_account_change_info.html:9
+msgid "Deleted account"
+msgstr "刪除帳號"
+
+#: accounts/templates/accounts/change_secret_failed_info.html:3
+#: ops/templates/ops/celery_task_log.html:71 terminal/serializers/task.py:10
+msgid "Task name"
+msgstr "任務名稱"
+
+#: accounts/templates/accounts/change_secret_failed_info.html:4
+msgid "Task execution id"
+msgstr "任務執行 ID"
+
+#: accounts/templates/accounts/change_secret_failed_info.html:5
+#: acls/templates/acls/asset_login_reminder.html:3
+#: acls/templates/acls/user_login_reminder.html:3
+msgid "Respectful"
+msgstr "尊敬的"
+
+#: accounts/templates/accounts/change_secret_failed_info.html:6
+msgid ""
+"Hello! The following is the failure of changing the password of your assets "
+"or pushing the account. Please check and handle it in time."
+msgstr "你好! 以下是資產改密或推送帳戶失敗的情況。 請及時檢查並處理。"
+
+#: accounts/utils.py:52
+msgid ""
+"If the password starts with {{` and ends with }} `, then the password is not "
+"allowed."
+msgstr "如果密碼以 `{{` 開始,並且以 `}}` 結束,則該密碼是不允許的。"
+
+#: accounts/utils.py:59
+msgid "private key invalid or passphrase error"
+msgstr "金鑰不合法或金鑰密碼錯誤"
+
+#: acls/apps.py:7
+msgid "Acls"
+msgstr "訪問控制"
+
+#: acls/const.py:6 audits/const.py:36 terminal/const.py:11 tickets/const.py:44
+#: tickets/templates/tickets/approve_check_password.html:47
+msgid "Reject"
+msgstr "拒絕"
+
+#: acls/const.py:7 audits/const.py:33 terminal/const.py:9
+msgid "Accept"
+msgstr "接受"
+
+#: acls/const.py:8 audits/const.py:34
+msgid "Review"
+msgstr "審批"
+
+#: acls/const.py:9 terminal/const.py:10
+msgid "Warning"
+msgstr "告警"
+
+#: acls/const.py:10 audits/const.py:35 notifications/apps.py:7
+msgid "Notifications"
+msgstr "通知"
+
+#: acls/models/base.py:37 assets/models/_user.py:51
+#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:112
+#: xpack/plugins/cloud/models.py:278
+msgid "Priority"
+msgstr "優先度"
+
+#: acls/models/base.py:38 assets/models/_user.py:51
+#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:113
+#: xpack/plugins/cloud/models.py:279
+msgid "1-100, the lower the value will be match first"
+msgstr "優先度可選範圍為 1-100 (數值越小越優先)"
+
+#: acls/models/base.py:42 assets/models/cmd_filter.py:86
+#: authentication/serializers/connect_token_secret.py:91
+msgid "Reviewers"
+msgstr "審批人"
+
+#: acls/models/base.py:43 authentication/models/access_key.py:25
+#: authentication/models/connection_token.py:53
+#: authentication/templates/authentication/_access_key_modal.html:32
+#: perms/models/asset_permission.py:82 terminal/models/session/sharing.py:29
+#: tickets/const.py:36
+msgid "Active"
+msgstr "啟用中"
+
+#: acls/models/base.py:81 users/apps.py:9 users/models/preference.py:16
+msgid "Users"
+msgstr "用戶管理"
+
+#: acls/models/base.py:98 assets/models/automations/base.py:17
+#: assets/models/cmd_filter.py:38 perms/serializers/user_permission.py:75
+#: rbac/tree.py:35
+msgid "Accounts"
+msgstr "帳號管理"
+
+#: acls/models/command_acl.py:16 assets/models/cmd_filter.py:60
+#: ops/serializers/job.py:70 terminal/const.py:86
+#: terminal/models/session/session.py:43 terminal/serializers/command.py:18
+#: terminal/templates/terminal/_msg_command_alert.html:12
+#: terminal/templates/terminal/_msg_command_execute_alert.html:10
+#: terminal/templates/terminal/_msg_command_warning.html:23
+msgid "Command"
+msgstr "命令"
+
+#: acls/models/command_acl.py:17 assets/models/cmd_filter.py:59
+#: xpack/plugins/cloud/models.py:295
+msgid "Regex"
+msgstr "正則表達式"
+
+#: acls/models/command_acl.py:26 assets/models/cmd_filter.py:79
+#: settings/models.py:182 settings/serializers/feature.py:19
+#: xpack/plugins/license/models.py:30
+msgid "Content"
+msgstr "內容"
+
+#: acls/models/command_acl.py:26 assets/models/cmd_filter.py:79
+msgid "One line one command"
+msgstr "每行一個命令"
+
+#: acls/models/command_acl.py:27 assets/models/cmd_filter.py:80
+msgid "Ignore case"
+msgstr "忽略大小寫"
+
+#: acls/models/command_acl.py:33 acls/models/command_acl.py:97
+#: acls/serializers/command_acl.py:29
+#: authentication/serializers/connect_token_secret.py:88
+#: terminal/templates/terminal/_msg_command_warning.html:14
+msgid "Command group"
+msgstr "命令組"
+
+#: acls/models/command_acl.py:86
+msgid "The generated regular expression is incorrect: {}"
+msgstr "生成的正則表達式有誤"
+
+#: acls/models/command_acl.py:103
+#: terminal/templates/terminal/_msg_command_warning.html:12
+msgid "Command acl"
+msgstr "命令過濾"
+
+#: acls/models/command_acl.py:112 tickets/const.py:12
+msgid "Command confirm"
+msgstr "命令覆核"
+
+#: acls/models/connect_method.py:10
+msgid "Connect methods"
+msgstr "連接方式"
+
+#: acls/models/connect_method.py:13
+msgid "Connect method acl"
+msgstr "連接方式控制"
+
+#: acls/models/login_acl.py:11 acls/models/login_asset_acl.py:9
+#: acls/serializers/login_acl.py:15 acls/serializers/login_asset_acl.py:13
+msgid "Rule"
+msgstr "規則"
+
+#: acls/models/login_acl.py:14
+msgid "Login acl"
+msgstr "登錄訪問控制"
+
+#: acls/models/login_acl.py:27 tickets/const.py:11
+msgid "Login confirm"
+msgstr "登錄覆核"
+
+#: acls/models/login_asset_acl.py:12
+msgid "Login asset acl"
+msgstr "登錄資產訪問控制"
+
+#: acls/models/login_asset_acl.py:22 tickets/const.py:13
+msgid "Login asset confirm"
+msgstr "登錄資產覆核"
+
+#: acls/notifications.py:12
+msgid "User login reminder"
+msgstr "用戶登錄提醒"
+
+#: acls/notifications.py:42
+msgid "Asset login reminder"
+msgstr "資產登錄提醒"
+
+#: acls/serializers/base.py:11 acls/serializers/login_acl.py:11
+msgid "With * indicating a match all. "
+msgstr "* 表示匹配所有. "
+
+#: acls/serializers/base.py:26
+msgid ""
+"With * indicating a match all. Such as: 192.168.10.1, 192.168.1.0/24, "
+"10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 (Domain name "
+"support)"
+msgstr ""
+"* 表示匹配所有。例如: 192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:"
+"db8:2de::e13, 2001:db8:1a:1110::/64 (支持網域)"
+
+#: acls/serializers/base.py:41 assets/serializers/asset/host.py:19
+msgid "IP/Host"
+msgstr "IP/主機"
+
+#: acls/serializers/base.py:91
+msgid "Recipients"
+msgstr "接收人"
+
+#: acls/serializers/base.py:103 tickets/serializers/ticket/ticket.py:77
+msgid "The organization `{}` does not exist"
+msgstr "組織 `{}` 不存在"
+
+#: acls/serializers/base.py:109
+msgid "None of the reviewers belong to Organization `{}`"
+msgstr "所有覆核人都不屬於組織 `{}`"
+
+#: acls/serializers/rules/rules.py:20
+#: xpack/plugins/cloud/serializers/task.py:145
+msgid "IP address invalid: `{}`"
+msgstr "IP 地址無效: `{}`"
+
+#: acls/serializers/rules/rules.py:25
+msgid ""
+"With * indicating a match all. Such as: 192.168.10.1, 192.168.1.0/24, "
+"10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64 "
+msgstr ""
+"* 表示匹配所有。例如: 192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:"
+"db8:2de::e13, 2001:db8:1a:1110::/64"
+
+#: acls/serializers/rules/rules.py:33
+#: authentication/templates/authentication/_msg_oauth_bind.html:12
+#: authentication/templates/authentication/_msg_rest_password_success.html:8
+#: authentication/templates/authentication/_msg_rest_public_key_success.html:8
+#: settings/serializers/terminal.py:10
+msgid "IP"
+msgstr "IP"
+
+#: acls/serializers/rules/rules.py:35
+msgid "Time Period"
+msgstr "時段"
+
+#: acls/templates/acls/asset_login_reminder.html:10
+msgid ""
+"The user has just logged in to the asset. Please ensure that this is an "
+"authorized operation. If you suspect that this is an unauthorized access, "
+"please take appropriate measures immediately."
+msgstr ""
+"用戶剛剛在登錄資產。請確保這是授權的操作。如果您懷疑這是一個未經授權的訪問,"
+"請立即採取適當的措施。"
+
+#: acls/templates/acls/asset_login_reminder.html:12
+#: acls/templates/acls/user_login_reminder.html:13
+msgid "Thank you"
+msgstr "謝謝"
+
+#: acls/templates/acls/user_login_reminder.html:7 audits/models.py:194
+#: audits/models.py:263
+#: authentication/templates/authentication/_msg_different_city.html:11
+#: tickets/models/ticket/login_confirm.py:11
+msgid "Login city"
+msgstr "登錄城市"
+
+#: acls/templates/acls/user_login_reminder.html:8 audits/models.py:197
+#: audits/models.py:264 audits/serializers.py:68
+msgid "User agent"
+msgstr "用戶代理"
+
+#: acls/templates/acls/user_login_reminder.html:11
+msgid ""
+"The user has just successfully logged into the system. Please ensure that "
+"this is an authorized operation. If you suspect that this is an unauthorized "
+"access, please take appropriate measures immediately."
+msgstr ""
+"用戶剛剛成功登錄到系統。請確保這是授權的操作。如果您懷疑這是一個未經授權的訪"
+"問,請立即採取適當的措施。"
+
+#: applications/apps.py:9
+msgid "Applications"
+msgstr "應用管理"
+
+#: applications/models.py:16 xpack/plugins/cloud/models.py:37
+#: xpack/plugins/cloud/serializers/account.py:68
+msgid "Attrs"
+msgstr "屬性"
+
+#: applications/models.py:19
+msgid "Application"
+msgstr "應用程式"
+
+#: applications/models.py:23
+msgid "Can match application"
+msgstr "匹配應用"
+
+#: assets/api/asset/asset.py:180
+msgid "Cannot create asset directly, you should create a host or other"
+msgstr "不能直接創建資產, 你應該創建主機或其他資產"
+
+#: assets/api/domain.py:67
+msgid "Number required"
+msgstr "需要為數字"
+
+#: assets/api/node.py:57
+msgid "You can't update the root node name"
+msgstr "不能修改根節點名稱"
+
+#: assets/api/node.py:64
+msgid "You can't delete the root node ({})"
+msgstr "不能刪除根節點 ({})"
+
+#: assets/api/node.py:67
+msgid "Deletion failed and the node contains assets"
+msgstr "刪除失敗,節點包含資產"
+
+#: assets/api/tree.py:49 assets/serializers/node.py:42
+msgid "The same level node name cannot be the same"
+msgstr "同級別節點名字不能重複"
+
+#: assets/apps.py:9
+msgid "App assets"
+msgstr "資產管理"
+
+#: assets/automations/base/manager.py:188
+msgid "{} disabled"
+msgstr "{} 已禁用"
+
+#: assets/automations/base/manager.py:251
+msgid " - Platform {} ansible disabled"
+msgstr " - 平台 {} Ansible 已禁用, 無法執行任務"
+
+#: assets/automations/ping_gateway/manager.py:33
+#: authentication/models/connection_token.py:131
+msgid "No account"
+msgstr "沒有帳號"
+
+#: assets/automations/ping_gateway/manager.py:36
+msgid "Asset, {}, using account {}"
+msgstr "資產, {}, 使用帳號 {}"
+
+#: assets/automations/ping_gateway/manager.py:55
+#, python-brace-format
+msgid "Unable to connect to port {port} on {address}"
+msgstr "無法連接到 {port} 上的埠 {address}"
+
+#: assets/automations/ping_gateway/manager.py:58
+#: authentication/backends/oauth2/views.py:60 authentication/middleware.py:93
+#: xpack/plugins/cloud/providers/fc.py:47
+msgid "Authentication failed"
+msgstr "認證失敗"
+
+#: assets/automations/ping_gateway/manager.py:60
+#: assets/automations/ping_gateway/manager.py:86 terminal/const.py:102
+msgid "Connect failed"
+msgstr "連接失敗"
+
+#: assets/const/automation.py:6 audits/const.py:6 audits/const.py:44
+#: audits/signal_handlers/activity_log.py:62 common/utils/ip/geoip/utils.py:31
+#: common/utils/ip/geoip/utils.py:37 common/utils/ip/utils.py:104
+msgid "Unknown"
+msgstr "未知"
+
+#: assets/const/automation.py:7
+msgid "Ok"
+msgstr "成功"
+
+#: assets/const/automation.py:12
+msgid "Ping"
+msgstr "測試"
+
+#: assets/const/automation.py:13
+msgid "Ping gateway"
+msgstr "測試網關"
+
+#: assets/const/automation.py:14
+msgid "Gather facts"
+msgstr "收集資產資訊"
+
+#: assets/const/base.py:32 audits/const.py:55
+#: terminal/serializers/applet_host.py:32
+msgid "Disabled"
+msgstr "禁用"
+
+#: assets/const/base.py:33 settings/serializers/basic.py:8
+#: users/serializers/preference/koko.py:19
+#: users/serializers/preference/lina.py:39
+#: users/serializers/preference/luna.py:77
+msgid "Basic"
+msgstr "基本"
+
+#: assets/const/base.py:34 assets/const/protocol.py:268
+#: assets/models/asset/web.py:13
+msgid "Script"
+msgstr "腳本"
+
+#: assets/const/category.py:10 assets/models/asset/host.py:8
+#: settings/serializers/auth/radius.py:16 settings/serializers/auth/sms.py:71
+#: settings/serializers/feature.py:49 settings/serializers/msg.py:31
+#: terminal/models/component/endpoint.py:13 terminal/serializers/applet.py:17
+#: xpack/plugins/cloud/serializers/account_attrs.py:72
+msgid "Host"
+msgstr "主機"
+
+#: assets/const/category.py:11 assets/models/asset/device.py:8
+msgid "Device"
+msgstr "網路設備"
+
+#: assets/const/category.py:13
+msgid "Cloud service"
+msgstr "雲服務"
+
+#: assets/const/category.py:14 assets/models/asset/gpt.py:11
+#: assets/models/asset/web.py:16 audits/const.py:42
+#: terminal/models/applet/applet.py:27 users/const.py:64
+msgid "Web"
+msgstr "Web"
+
+#: assets/const/category.py:16 common/sdk/sms/endpoint.py:20
+msgid "Custom type"
+msgstr "自訂"
+
+#: assets/const/cloud.py:7
+msgid "Public cloud"
+msgstr "公有雲"
+
+#: assets/const/cloud.py:8
+msgid "Private cloud"
+msgstr "私有雲"
+
+#: assets/const/cloud.py:9
+msgid "Kubernetes"
+msgstr "Kubernetes"
+
+#: assets/const/device.py:7 terminal/models/applet/applet.py:26
+#: tickets/const.py:9
+msgid "General"
+msgstr "一般"
+
+#: assets/const/device.py:8
+msgid "Switch"
+msgstr "交換機"
+
+#: assets/const/device.py:9
+msgid "Router"
+msgstr "路由器"
+
+#: assets/const/device.py:10
+msgid "Firewall"
+msgstr "防火牆"
+
+#: assets/const/gpt.py:7
+msgid "ChatGPT"
+msgstr "ChatGPT"
+
+#: assets/const/host.py:12 rbac/tree.py:28
+msgid "Other"
+msgstr "其它"
+
+#: assets/const/protocol.py:45
+msgid "Old SSH version"
+msgstr "Old SSH version"
+
+#: assets/const/protocol.py:46
+msgid "Old SSH version like openssh 5.x or 6.x"
+msgstr "舊的 SSH 版本,例如 openssh 5.x 或 6.x"
+
+#: assets/const/protocol.py:57
+msgid "SFTP root"
+msgstr "SFTP 根路徑"
+
+#: assets/const/protocol.py:59
+#, python-brace-format
+msgid ""
+"SFTP root directory, Support variable:
- ${ACCOUNT} The connected "
+"account username
- ${HOME} The home directory of the connected account "
+"
- ${USER} The username of the user"
+msgstr ""
+"SFTP根目錄,支持變數:
-${ACCOUNT}已連接帳戶使用者名稱
-${HOME}連接帳戶"
+"的主目錄
-${USER}用戶的使用者名稱"
+
+#: assets/const/protocol.py:74
+msgid "Console"
+msgstr "控制台"
+
+#: assets/const/protocol.py:75
+msgid "Connect to console session"
+msgstr "連接到控制台會話"
+
+#: assets/const/protocol.py:79
+msgid "Any"
+msgstr "任意"
+
+#: assets/const/protocol.py:81 settings/serializers/security.py:232
+msgid "Security"
+msgstr "安全"
+
+#: assets/const/protocol.py:82
+msgid "Security layer to use for the connection"
+msgstr "連接 RDP 使用的安全層"
+
+#: assets/const/protocol.py:88
+msgid "AD domain"
+msgstr "AD 網域"
+
+#: assets/const/protocol.py:103
+msgid "Username prompt"
+msgstr "使用者名稱提示"
+
+#: assets/const/protocol.py:104
+msgid "We will send username when we see this prompt"
+msgstr "當我們看到這個提示時,我們將發送使用者名稱"
+
+#: assets/const/protocol.py:109
+msgid "Password prompt"
+msgstr "密碼提示"
+
+#: assets/const/protocol.py:110
+msgid "We will send password when we see this prompt"
+msgstr "當我們看到這個提示時,我們將發送密碼"
+
+#: assets/const/protocol.py:115
+msgid "Success prompt"
+msgstr "成功提示"
+
+#: assets/const/protocol.py:116
+msgid "We will consider login success when we see this prompt"
+msgstr "當我們看到這個提示時,我們將認為登錄成功"
+
+#: assets/const/protocol.py:127 assets/models/asset/database.py:10
+#: settings/serializers/msg.py:47
+msgid "Use SSL"
+msgstr "使用 SSL"
+
+#: assets/const/protocol.py:162
+msgid "SYSDBA"
+msgstr "SYSDBA"
+
+#: assets/const/protocol.py:163
+msgid "Connect as SYSDBA"
+msgstr "以 SYSDBA 角色連接"
+
+#: assets/const/protocol.py:178
+msgid ""
+"SQL Server version, Different versions have different connection drivers"
+msgstr "SQL Server 版本,不同版本有不同的連接驅動"
+
+#: assets/const/protocol.py:202
+msgid "Auth source"
+msgstr "認證資料庫"
+
+#: assets/const/protocol.py:203
+msgid "The database to authenticate against"
+msgstr "要進行身份驗證的資料庫"
+
+#: assets/const/protocol.py:215
+msgid "Auth username"
+msgstr "使用使用者名稱認證"
+
+#: assets/const/protocol.py:238
+msgid "Safe mode"
+msgstr "安全模式"
+
+#: assets/const/protocol.py:240
+msgid ""
+"When safe mode is enabled, some operations will be disabled, such as: New "
+"tab, right click, visit other website, etc."
+msgstr ""
+"當安全模式啟用時,一些操作將被禁用,例如:新建標籤頁、右鍵、訪問其它網站 等"
+
+#: assets/const/protocol.py:245 assets/models/asset/web.py:9
+#: assets/serializers/asset/info/spec.py:16
+msgid "Autofill"
+msgstr "自動代填"
+
+#: assets/const/protocol.py:253 assets/models/asset/web.py:10
+msgid "Username selector"
+msgstr "使用者名稱選擇器"
+
+#: assets/const/protocol.py:258 assets/models/asset/web.py:11
+msgid "Password selector"
+msgstr "密碼選擇器"
+
+#: assets/const/protocol.py:263 assets/models/asset/web.py:12
+msgid "Submit selector"
+msgstr "確認按鈕選擇器"
+
+#: assets/const/protocol.py:286
+msgid "API mode"
+msgstr "API 模式"
+
+#: assets/const/types.py:248
+msgid "All types"
+msgstr "所有類型"
+
+#: assets/const/web.py:7
+msgid "Website"
+msgstr "網站"
+
+#: assets/exceptions.py:12
+msgid "This function is not supported temporarily"
+msgstr "暫時不支持此功能"
+
+#: assets/models/_user.py:25
+msgid "SSH private key"
+msgstr "SSH金鑰"
+
+#: assets/models/_user.py:26
+msgid "SSH public key"
+msgstr "SSH公鑰"
+
+# msgid "Comment"
+# msgstr "備註"
+#: assets/models/_user.py:28 assets/models/automations/base.py:114
+#: assets/models/cmd_filter.py:41 assets/models/group.py:19
+#: audits/models.py:267 common/db/models.py:34 ops/models/base.py:54
+#: ops/models/job.py:236 users/models/user.py:1058
+msgid "Date created"
+msgstr "創建日期"
+
+#: assets/models/_user.py:29 assets/models/cmd_filter.py:42
+#: common/db/models.py:35 users/models/user.py:868
+msgid "Date updated"
+msgstr "更新日期"
+
+#: assets/models/_user.py:30 assets/models/cmd_filter.py:44
+#: assets/models/cmd_filter.py:91 assets/models/group.py:18
+#: common/db/models.py:32 users/models/user.py:857
+#: users/serializers/group.py:32
+msgid "Created by"
+msgstr "創建者"
+
+#: assets/models/_user.py:40
+msgid "Automatic managed"
+msgstr "託管密碼"
+
+#: assets/models/_user.py:41
+msgid "Manually input"
+msgstr "手動輸入"
+
+#: assets/models/_user.py:45
+msgid "Common user"
+msgstr "普通用戶"
+
+#: assets/models/_user.py:46 assets/models/_user.py:95
+msgid "Admin user"
+msgstr "特權用戶"
+
+#: assets/models/_user.py:49
+msgid "Username same with user"
+msgstr "使用者名稱與用戶相同"
+
+#: assets/models/_user.py:52 authentication/models/connection_token.py:41
+#: authentication/serializers/connect_token_secret.py:114
+#: settings/serializers/msg.py:29 terminal/models/applet/applet.py:42
+#: terminal/models/virtualapp/virtualapp.py:24
+#: terminal/serializers/session.py:21 terminal/serializers/session.py:48
+#: terminal/serializers/storage.py:71
+msgid "Protocol"
+msgstr "協議"
+
+#: assets/models/_user.py:54
+msgid "Sudo"
+msgstr "Sudo"
+
+#: assets/models/_user.py:55 ops/const.py:50 ops/const.py:61
+msgid "Shell"
+msgstr "Shell"
+
+#: assets/models/_user.py:56
+msgid "Login mode"
+msgstr "認證方式"
+
+#: assets/models/_user.py:57 terminal/serializers/storage.py:152
+msgid "SFTP Root"
+msgstr "SFTP根路徑"
+
+#: assets/models/_user.py:58
+msgid "Home"
+msgstr "家目錄"
+
+#: assets/models/_user.py:59
+msgid "System groups"
+msgstr "用戶組"
+
+#: assets/models/_user.py:62
+msgid "User switch"
+msgstr "用戶切換"
+
+#: assets/models/_user.py:63
+msgid "Switch from"
+msgstr "切換自"
+
+#: assets/models/_user.py:69
+msgid "System user"
+msgstr "系統用戶"
+
+#: assets/models/_user.py:71
+msgid "Can match system user"
+msgstr "可以匹配系統用戶"
+
+#: assets/models/asset/cloud.py:11
+msgid "Cloud"
+msgstr "雲服務"
+
+#: assets/models/asset/common.py:94 assets/models/platform.py:17
+#: settings/serializers/auth/radius.py:17 settings/serializers/auth/sms.py:72
+#: settings/serializers/msg.py:32 terminal/serializers/storage.py:133
+#: xpack/plugins/cloud/serializers/account_attrs.py:73
+msgid "Port"
+msgstr "埠"
+
+#: assets/models/asset/common.py:160 assets/serializers/asset/common.py:150
+msgid "Address"
+msgstr "地址"
+
+#: assets/models/asset/common.py:161 assets/models/platform.py:126
+#: authentication/backends/passkey/models.py:12
+#: authentication/serializers/connect_token_secret.py:118
+#: perms/serializers/user_permission.py:25 xpack/plugins/cloud/models.py:325
+msgid "Platform"
+msgstr "系統平台"
+
+#: assets/models/asset/common.py:163 assets/models/domain.py:22
+#: authentication/serializers/connect_token_secret.py:136
+#: perms/serializers/user_permission.py:28 xpack/plugins/cloud/models.py:327
+msgid "Domain"
+msgstr "網域"
+
+#: assets/models/asset/common.py:165 assets/models/automations/base.py:18
+#: assets/models/cmd_filter.py:32 assets/models/node.py:553
+#: perms/models/asset_permission.py:72 perms/serializers/permission.py:37
+#: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:326
+msgid "Node"
+msgstr "節點"
+
+#: assets/models/asset/common.py:167 assets/serializers/asset/common.py:385
+#: assets/serializers/asset/host.py:11
+msgid "Gathered info"
+msgstr "收集資產硬體資訊"
+
+#: assets/models/asset/common.py:168 assets/serializers/asset/custom.py:14
+msgid "Custom info"
+msgstr "自訂屬性"
+
+#: assets/models/asset/common.py:353
+msgid "Can refresh asset hardware info"
+msgstr "可以更新資產硬體資訊"
+
+#: assets/models/asset/common.py:354
+msgid "Can test asset connectivity"
+msgstr "可以測試資產連接性"
+
+#: assets/models/asset/common.py:355
+msgid "Can match asset"
+msgstr "可以匹配資產"
+
+#: assets/models/asset/common.py:356
+msgid "Can change asset nodes"
+msgstr "可以修改資產節點"
+
+#: assets/models/asset/custom.py:8
+msgid "Custom asset"
+msgstr "自訂資產"
+
+#: assets/models/asset/database.py:11
+msgid "CA cert"
+msgstr "CA 證書"
+
+#: assets/models/asset/database.py:12
+msgid "Client cert"
+msgstr "用戶端證書"
+
+#: assets/models/asset/database.py:13
+msgid "Client key"
+msgstr "用戶端金鑰"
+
+#: assets/models/asset/database.py:14
+msgid "Allow invalid cert"
+msgstr "忽略證書校驗"
+
+#: assets/models/asset/gpt.py:8 settings/serializers/feature.py:84
+msgid "Proxy"
+msgstr "代理"
+
+#: assets/models/automations/base.py:22 ops/models/job.py:232
+#: settings/serializers/auth/sms.py:103
+msgid "Parameters"
+msgstr "參數"
+
+#: assets/models/automations/base.py:29 assets/models/automations/base.py:111
+msgid "Automation task"
+msgstr "自動化任務"
+
+#: assets/models/automations/base.py:104
+msgid "Asset automation task"
+msgstr "資產自動化任務"
+
+#: assets/models/automations/gather_facts.py:15
+msgid "Gather asset facts"
+msgstr "收集資產資訊"
+
+#: assets/models/automations/ping.py:15
+msgid "Ping asset"
+msgstr "測試資產"
+
+#: assets/models/base.py:19
+msgid "Connectivity"
+msgstr "可連接性"
+
+#: assets/models/base.py:21 authentication/models/temp_token.py:12
+msgid "Date verified"
+msgstr "校驗日期"
+
+#: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:66
+#: perms/serializers/permission.py:34 users/models/group.py:25
+#: users/models/user.py:820
+msgid "User group"
+msgstr "用戶組"
+
+#: assets/models/cmd_filter.py:52
+msgid "Command filter"
+msgstr "命令過濾器"
+
+#: assets/models/cmd_filter.py:66
+msgid "Deny"
+msgstr "拒絕"
+
+#: assets/models/cmd_filter.py:67
+msgid "Allow"
+msgstr "允許"
+
+#: assets/models/cmd_filter.py:68
+msgid "Reconfirm"
+msgstr "覆核"
+
+#: assets/models/cmd_filter.py:72
+msgid "Filter"
+msgstr "過濾器"
+
+#: assets/models/cmd_filter.py:95
+msgid "Command filter rule"
+msgstr "命令過濾規則"
+
+#: assets/models/favorite_asset.py:17
+msgid "Favorite asset"
+msgstr "收藏的資產"
+
+#: assets/models/gateway.py:34 assets/serializers/domain.py:19
+msgid "Gateway"
+msgstr "網關"
+
+#: assets/models/group.py:27
+msgid "Asset group"
+msgstr "資產組"
+
+#: assets/models/group.py:31 assets/models/platform.py:20
+#: assets/serializers/platform.py:121
+#: xpack/plugins/cloud/providers/nutanix.py:30
+msgid "Default"
+msgstr "默認"
+
+#: assets/models/group.py:31
+msgid "Default asset group"
+msgstr "默認資產組"
+
+#: assets/models/label.py:15 rbac/const.py:6 users/models/user.py:1043
+msgid "System"
+msgstr "系統"
+
+#: assets/models/label.py:19 assets/models/node.py:539
+#: assets/serializers/cagegory.py:11 assets/serializers/cagegory.py:18
+#: assets/serializers/cagegory.py:24
+#: authentication/models/connection_token.py:29
+#: authentication/serializers/connect_token_secret.py:125
+#: common/serializers/common.py:86 labels/models.py:12 settings/models.py:34
+#: users/models/preference.py:13
+msgid "Value"
+msgstr "值"
+
+#: assets/models/label.py:40 assets/serializers/cagegory.py:10
+#: assets/serializers/cagegory.py:17 assets/serializers/cagegory.py:23
+#: assets/serializers/platform.py:119
+#: authentication/serializers/connect_token_secret.py:124
+#: common/serializers/common.py:85 labels/models.py:17 labels/models.py:33
+#: labels/serializers.py:45 settings/serializers/msg.py:90
+msgid "Label"
+msgstr "標籤"
+
+#: assets/models/node.py:169
+msgid "New node"
+msgstr "新節點"
+
+#: assets/models/node.py:467 audits/backends/db.py:65 audits/backends/db.py:66
+msgid "empty"
+msgstr "空"
+
+#: assets/models/node.py:538 perms/models/perm_node.py:28
+msgid "Key"
+msgstr "鍵"
+
+#: assets/models/node.py:540 assets/serializers/node.py:20
+msgid "Full value"
+msgstr "全稱"
+
+#: assets/models/node.py:544 perms/models/perm_node.py:30
+msgid "Parent key"
+msgstr "ssh私鑰"
+
+#: assets/models/node.py:556
+msgid "Can match node"
+msgstr "可以匹配節點"
+
+#: assets/models/platform.py:18
+msgid "Primary"
+msgstr "主要的"
+
+#: assets/models/platform.py:19
+msgid "Required"
+msgstr "必須的"
+
+#: assets/models/platform.py:21
+msgid "Public"
+msgstr "開放的"
+
+#: assets/models/platform.py:22 assets/serializers/platform.py:49
+#: settings/serializers/settings.py:95
+#: users/templates/users/reset_password.html:29
+msgid "Setting"
+msgstr "設置"
+
+#: assets/models/platform.py:39 audits/const.py:56
+#: authentication/backends/passkey/models.py:11 settings/models.py:37
+#: terminal/serializers/applet_host.py:33
+msgid "Enabled"
+msgstr "啟用"
+
+#: assets/models/platform.py:40
+msgid "Ansible config"
+msgstr "Ansible 配置"
+
+#: assets/models/platform.py:42 assets/serializers/platform.py:33
+msgid "Ping enabled"
+msgstr "啟用資產探活"
+
+#: assets/models/platform.py:43 assets/serializers/platform.py:34
+msgid "Ping method"
+msgstr "資產探活方式"
+
+#: assets/models/platform.py:44
+msgid "Ping params"
+msgstr "資產探活參數"
+
+#: assets/models/platform.py:46 assets/models/platform.py:70
+#: assets/serializers/platform.py:35
+msgid "Gather facts enabled"
+msgstr "啟用收集資產資訊"
+
+#: assets/models/platform.py:48 assets/models/platform.py:72
+#: assets/serializers/platform.py:36
+msgid "Gather facts method"
+msgstr "收集資訊方式"
+
+#: assets/models/platform.py:50 assets/models/platform.py:74
+msgid "Gather facts params"
+msgstr "收集資訊參數"
+
+#: assets/models/platform.py:52 assets/serializers/platform.py:39
+msgid "Change secret enabled"
+msgstr "啟用改密"
+
+#: assets/models/platform.py:54 assets/serializers/platform.py:40
+msgid "Change secret method"
+msgstr "改密方式"
+
+#: assets/models/platform.py:56
+msgid "Change secret params"
+msgstr "改密參數"
+
+#: assets/models/platform.py:58 assets/serializers/platform.py:41
+msgid "Push account enabled"
+msgstr "啟用帳號推送"
+
+#: assets/models/platform.py:60 assets/serializers/platform.py:42
+msgid "Push account method"
+msgstr "帳號推送方式"
+
+#: assets/models/platform.py:62
+msgid "Push account params"
+msgstr "帳號推送參數"
+
+#: assets/models/platform.py:64 assets/serializers/platform.py:37
+msgid "Verify account enabled"
+msgstr "開啟帳號驗證"
+
+#: assets/models/platform.py:66 assets/serializers/platform.py:38
+msgid "Verify account method"
+msgstr "帳號驗證方式"
+
+#: assets/models/platform.py:68
+msgid "Verify account params"
+msgstr "帳號驗證參數"
+
+#: assets/models/platform.py:76
+msgid "Remove account enabled"
+msgstr "開啟帳號移除"
+
+#: assets/models/platform.py:78
+msgid "Remove account method"
+msgstr "帳號移除方式"
+
+#: assets/models/platform.py:80
+msgid "Remove account params"
+msgstr "帳號移除參數"
+
+#: assets/models/platform.py:98 tickets/models/ticket/general.py:298
+msgid "Meta"
+msgstr "元數據"
+
+#: assets/models/platform.py:99 labels/models.py:13
+msgid "Internal"
+msgstr "內建"
+
+#: assets/models/platform.py:103 assets/serializers/platform.py:138
+msgid "Charset"
+msgstr "編碼"
+
+#: assets/models/platform.py:105 assets/serializers/platform.py:167
+msgid "Domain enabled"
+msgstr "啟用網域"
+
+#: assets/models/platform.py:107 assets/serializers/platform.py:166
+msgid "Su enabled"
+msgstr "啟用帳號切換"
+
+#: assets/models/platform.py:108 assets/serializers/platform.py:144
+msgid "Su method"
+msgstr "帳號切換方式"
+
+#: assets/models/platform.py:109 assets/serializers/platform.py:147
+msgid "Custom fields"
+msgstr "自訂屬性"
+
+#: assets/models/utils.py:18
+#, python-format
+msgid "%(value)s is not an even number"
+msgstr "%(value)s is not an even number"
+
+#: assets/notifications.py:12
+msgid ""
+"Batch update platform in assets, skipping assets that do not meet platform "
+"type"
+msgstr "資產中批次更新平台,不符合平台類型跳過的資產"
+
+#: assets/serializers/asset/common.py:127 assets/serializers/platform.py:141
+#: authentication/serializers/connect_token_secret.py:30
+#: authentication/serializers/connect_token_secret.py:75
+#: perms/models/asset_permission.py:76 perms/serializers/permission.py:42
+#: perms/serializers/user_permission.py:74 xpack/plugins/cloud/models.py:328
+#: xpack/plugins/cloud/serializers/task.py:33
+msgid "Protocols"
+msgstr "協議組"
+
+#: assets/serializers/asset/common.py:129
+#: assets/serializers/asset/common.py:151
+msgid "Node path"
+msgstr "節點路徑"
+
+#: assets/serializers/asset/common.py:148
+#: assets/serializers/asset/common.py:386
+msgid "Auto info"
+msgstr "自動化資訊"
+
+#: assets/serializers/asset/common.py:245
+msgid "Platform not exist"
+msgstr "平台不存在"
+
+#: assets/serializers/asset/common.py:281
+msgid "port out of range (0-65535)"
+msgstr "埠超出範圍 (0-65535)"
+
+#: assets/serializers/asset/common.py:288
+msgid "Protocol is required: {}"
+msgstr "協議是必填的: {}"
+
+#: assets/serializers/asset/common.py:316
+msgid "Invalid data"
+msgstr "無效的數據"
+
+#: assets/serializers/asset/database.py:13
+msgid "Default database"
+msgstr "默認資料庫"
+
+#: assets/serializers/asset/gpt.py:20
+msgid ""
+"If the server cannot directly connect to the API address, you need set up an "
+"HTTP proxy. e.g. http(s)://host:port"
+msgstr ""
+"如果伺服器不能直接訪問 api 地址,你需要設置一個 HTTP 代理。例如 http(s)://"
+"host:port"
+
+#: assets/serializers/asset/gpt.py:24
+msgid "HTTP proxy"
+msgstr "HTTP(s) 代理"
+
+#: assets/serializers/asset/gpt.py:31
+msgid "Proxy must start with http:// or https://"
+msgstr "代理必須以 http:// 或 https:// 開頭"
+
+#: assets/serializers/asset/info/gathered.py:6
+msgid "Vendor"
+msgstr "製造商"
+
+#: assets/serializers/asset/info/gathered.py:7
+msgid "Model"
+msgstr "型號"
+
+#: assets/serializers/asset/info/gathered.py:8
+#: tickets/models/ticket/general.py:297
+msgid "Serial number"
+msgstr "序號"
+
+#: assets/serializers/asset/info/gathered.py:9
+msgid "CPU model"
+msgstr "CPU型號"
+
+#: assets/serializers/asset/info/gathered.py:10
+msgid "CPU count"
+msgstr "CPU數量"
+
+#: assets/serializers/asset/info/gathered.py:11
+msgid "CPU cores"
+msgstr "CPU核數"
+
+#: assets/serializers/asset/info/gathered.py:12
+msgid "CPU vcpus"
+msgstr "CPU總數"
+
+#: assets/serializers/asset/info/gathered.py:13
+msgid "Memory"
+msgstr "記憶體"
+
+#: assets/serializers/asset/info/gathered.py:14
+msgid "Disk total"
+msgstr "硬碟大小"
+
+#: assets/serializers/asset/info/gathered.py:16
+#: authentication/serializers/connect_token_secret.py:115
+msgid "OS"
+msgstr "操作系統"
+
+#: assets/serializers/asset/info/gathered.py:17
+msgid "OS version"
+msgstr "系統版本"
+
+#: assets/serializers/asset/info/gathered.py:18
+msgid "OS arch"
+msgstr "系統架構"
+
+#: assets/serializers/cagegory.py:13
+msgid "Constraints"
+msgstr "約束"
+
+#: assets/serializers/cagegory.py:19
+msgid "Types"
+msgstr "類型"
+
+#: assets/serializers/domain.py:62 perms/serializers/permission.py:188
+msgid "Assets amount"
+msgstr "資產數量"
+
+#: assets/serializers/gateway.py:23 common/validators.py:34
+msgid "This field must be unique."
+msgstr "欄位必須唯一"
+
+#: assets/serializers/node.py:17
+msgid "value"
+msgstr "值"
+
+#: assets/serializers/node.py:31
+msgid "Can't contains: /"
+msgstr "不能包含: /"
+
+#: assets/serializers/platform.py:43
+msgid "Gather accounts enabled"
+msgstr "啟用帳號收集"
+
+#: assets/serializers/platform.py:44
+msgid "Gather accounts method"
+msgstr "收集帳號方式"
+
+#: assets/serializers/platform.py:50
+msgid "Port from addr"
+msgstr "埠來自地址"
+
+#: assets/serializers/platform.py:62
+msgid ""
+"This protocol is primary, and it must be set when adding assets. "
+"Additionally, there can only be one primary protocol."
+msgstr "該協議是主要的,添加資產時必須設置。並且只能有一個主要協議"
+
+#: assets/serializers/platform.py:67
+msgid "This protocol is required, and it must be set when adding assets."
+msgstr "該協議是必填的,添加資產時必須設置"
+
+#: assets/serializers/platform.py:70
+msgid ""
+"This protocol is default, when adding assets, it will be displayed by "
+"default."
+msgstr "該協議是預設的,添加資產時,將默認顯示"
+
+#: assets/serializers/platform.py:73
+msgid "This protocol is public, asset will show this protocol to user"
+msgstr "該協議是公開的,資產將向用戶顯示該協議並可以連接使用"
+
+#: assets/serializers/platform.py:122
+msgid "Help text"
+msgstr "幫助"
+
+#: assets/serializers/platform.py:123
+msgid "Choices"
+msgstr "選擇"
+
+#: assets/serializers/platform.py:142
+msgid "Automation"
+msgstr "自動化"
+
+#: assets/serializers/platform.py:168
+msgid "Default Domain"
+msgstr "默認網域"
+
+#: assets/serializers/platform.py:189
+msgid "type is required"
+msgstr "類型 該欄位是必填項。"
+
+#: assets/serializers/platform.py:204
+msgid "Protocols is required"
+msgstr "協議是必填的"
+
+#: assets/signal_handlers/asset.py:26 assets/tasks/ping.py:35
+msgid "Test assets connectivity "
+msgstr "測試資產可連接性"
+
+#: assets/signal_handlers/asset.py:36
+msgid "Gather asset hardware info"
+msgstr "收集資產硬體資訊"
+
+#: assets/tasks/automation.py:24
+msgid "Asset execute automation"
+msgstr "資產執行自動化"
+
+#: assets/tasks/gather_facts.py:21 assets/tasks/gather_facts.py:27
+msgid "Gather assets facts"
+msgstr "收集資產資訊"
+
+#: assets/tasks/gather_facts.py:39
+msgid "Update assets hardware info: "
+msgstr "更新資產硬體資訊"
+
+#: assets/tasks/gather_facts.py:47
+msgid "Update node asset hardware information: "
+msgstr "更新節點資產硬體資訊: "
+
+#: assets/tasks/nodes_amount.py:16
+msgid "Check the amount of assets under the node"
+msgstr "檢查節點下資產數量"
+
+#: assets/tasks/nodes_amount.py:28
+msgid ""
+"The task of self-checking is already running and cannot be started repeatedly"
+msgstr "自檢程序已經在運行,不能重複啟動"
+
+#: assets/tasks/nodes_amount.py:33
+msgid "Periodic check the amount of assets under the node"
+msgstr "週期性檢查節點下資產數量"
+
+#: assets/tasks/ping.py:20 assets/tasks/ping.py:26
+msgid "Test assets connectivity"
+msgstr "測試資產可連接性"
+
+#: assets/tasks/ping.py:42
+msgid "Test if the assets under the node are connectable "
+msgstr "測試節點下資產是否可連接"
+
+#: assets/tasks/ping_gateway.py:19 assets/tasks/ping_gateway.py:25
+#: assets/tasks/ping_gateway.py:34
+msgid "Test gateways connectivity"
+msgstr "測試網關可連接性"
+
+#: assets/tasks/utils.py:16
+msgid "Asset has been disabled, skipped: {}"
+msgstr "資產已經被禁用, 跳過: {}"
+
+#: assets/tasks/utils.py:20
+msgid "Asset may not be support ansible, skipped: {}"
+msgstr "資產或許不支持ansible, 跳過: {}"
+
+#: assets/tasks/utils.py:38
+msgid "For security, do not push user {}"
+msgstr "為了安全,禁止推送用戶 {}"
+
+#: assets/tasks/utils.py:54
+msgid "No assets matched, stop task"
+msgstr "沒有匹配到資產,結束任務"
+
+#: audits/apps.py:9
+msgid "Audits"
+msgstr "日誌審計"
+
+#: audits/backends/db.py:16
+msgid "The text content is too long. Use Elasticsearch to store operation logs"
+msgstr "文字內容太長。請使用 Elasticsearch 儲存操作日誌"
+
+#: audits/backends/db.py:91
+msgid "Tips"
+msgstr "提示"
+
+#: audits/const.py:12
+msgid "Mkdir"
+msgstr "創建目錄"
+
+#: audits/const.py:13
+msgid "Rmdir"
+msgstr "刪除目錄"
+
+#: audits/const.py:14 audits/const.py:25
+#: authentication/templates/authentication/_access_key_modal.html:65
+#: rbac/tree.py:240
+msgid "Delete"
+msgstr "刪除"
+
+#: audits/const.py:15
+msgid "Upload"
+msgstr "上傳"
+
+#: audits/const.py:16
+msgid "Rename"
+msgstr "重命名"
+
+#: audits/const.py:17
+msgid "Symlink"
+msgstr "建立軟連結"
+
+#: audits/const.py:18 audits/const.py:28 terminal/api/session/session.py:149
+msgid "Download"
+msgstr "下載"
+
+#: audits/const.py:19
+msgid "Rename dir"
+msgstr "映射目錄"
+
+#: audits/const.py:23 rbac/tree.py:238 terminal/api/session/session.py:277
+#: terminal/templates/terminal/_msg_command_warning.html:18
+#: terminal/templates/terminal/_msg_session_sharing.html:10
+msgid "View"
+msgstr "查看"
+
+#: audits/const.py:26
+#: authentication/templates/authentication/_access_key_modal.html:22
+#: rbac/tree.py:237
+msgid "Create"
+msgstr "創建"
+
+#: audits/const.py:29
+msgid "Connect"
+msgstr "連接"
+
+#: audits/const.py:30 authentication/templates/authentication/login.html:296
+#: authentication/templates/authentication/login.html:369
+#: templates/_header_bar.html:101
+msgid "Login"
+msgstr "登錄"
+
+#: audits/const.py:31 ops/const.py:9
+msgid "Change password"
+msgstr "改密"
+
+#: audits/const.py:37 tickets/const.py:45
+msgid "Approve"
+msgstr "同意"
+
+#: audits/const.py:38
+#: authentication/templates/authentication/_access_key_modal.html:155
+#: authentication/templates/authentication/_mfa_confirm_modal.html:53
+#: templates/_modal.html:22 tickets/const.py:43
+msgid "Close"
+msgstr "關閉"
+
+#: audits/const.py:43 settings/serializers/terminal.py:6
+#: terminal/models/applet/host.py:26 terminal/models/component/terminal.py:175
+#: terminal/models/virtualapp/provider.py:14 terminal/serializers/session.py:55
+#: terminal/serializers/session.py:69
+msgid "Terminal"
+msgstr "終端"
+
+#: audits/const.py:48 audits/models.py:132
+msgid "Operate log"
+msgstr "操作日誌"
+
+#: audits/const.py:49
+msgid "Session log"
+msgstr "會話日誌"
+
+#: audits/const.py:50
+msgid "Login log"
+msgstr "登錄日誌"
+
+#: audits/const.py:51 terminal/models/applet/host.py:144
+#: terminal/models/component/task.py:22
+msgid "Task"
+msgstr "任務"
+
+#: audits/const.py:57
+msgid "-"
+msgstr "-"
+
+#: audits/handler.py:116
+msgid "Yes"
+msgstr "是"
+
+#: audits/handler.py:116
+msgid "No"
+msgstr "否"
+
+#: audits/models.py:47
+msgid "Job audit log"
+msgstr "作業審計日誌"
+
+#: audits/models.py:56 audits/models.py:100 audits/models.py:175
+#: terminal/models/session/session.py:39 terminal/models/session/sharing.py:113
+msgid "Remote addr"
+msgstr "遠端地址"
+
+#: audits/models.py:61 audits/serializers.py:38
+msgid "Operate"
+msgstr "操作"
+
+#: audits/models.py:63
+msgid "Filename"
+msgstr "檔案名"
+
+#: audits/models.py:66 common/serializers/common.py:98
+msgid "File"
+msgstr "文件"
+
+#: audits/models.py:67 terminal/backends/command/models.py:21
+#: terminal/models/session/replay.py:9 terminal/models/session/sharing.py:20
+#: terminal/models/session/sharing.py:95
+#: terminal/templates/terminal/_msg_command_alert.html:10
+#: terminal/templates/terminal/_msg_command_warning.html:17
+#: tickets/models/ticket/command_confirm.py:15
+msgid "Session"
+msgstr "會話"
+
+#: audits/models.py:70
+msgid "File transfer log"
+msgstr "文件管理"
+
+#: audits/models.py:94 audits/serializers.py:86
+msgid "Resource Type"
+msgstr "資源類型"
+
+#: audits/models.py:95 audits/models.py:98 audits/models.py:144
+#: audits/serializers.py:85 labels/serializers.py:46
+msgid "Resource"
+msgstr "資源"
+
+#: audits/models.py:101 audits/models.py:147 audits/models.py:177
+#: terminal/serializers/command.py:75
+msgid "Datetime"
+msgstr "日期"
+
+#: audits/models.py:140
+msgid "Activity type"
+msgstr "活動類型"
+
+#: audits/models.py:150
+msgid "Detail"
+msgstr "詳情"
+
+#: audits/models.py:153
+msgid "Detail ID"
+msgstr "詳情 ID"
+
+#: audits/models.py:157
+msgid "Activity log"
+msgstr "活動日誌"
+
+#: audits/models.py:173
+msgid "Change by"
+msgstr "修改者"
+
+#: audits/models.py:183
+msgid "Password change log"
+msgstr "改密日誌"
+
+#: audits/models.py:190 audits/models.py:265
+msgid "Login type"
+msgstr "登錄方式"
+
+#: audits/models.py:192 audits/models.py:261
+#: tickets/models/ticket/login_confirm.py:10
+msgid "Login IP"
+msgstr "登錄 IP"
+
+#: audits/models.py:200 audits/serializers.py:52
+#: authentication/templates/authentication/_mfa_confirm_modal.html:14
+#: users/forms/profile.py:63 users/models/user.py:837
+#: users/serializers/profile.py:102
+msgid "MFA"
+msgstr "MFA"
+
+#: audits/models.py:210
+msgid "Date login"
+msgstr "登錄日期"
+
+#: audits/models.py:212 audits/models.py:266 audits/serializers.py:70
+#: audits/serializers.py:184
+msgid "Authentication backend"
+msgstr "認證方式"
+
+#: audits/models.py:256
+msgid "User login log"
+msgstr "用戶登錄日誌"
+
+#: audits/models.py:262
+msgid "Session key"
+msgstr "會話標識"
+
+#: audits/models.py:298
+msgid "User session"
+msgstr "用戶會話"
+
+#: audits/models.py:300
+msgid "Offline user session"
+msgstr "下線用戶會話"
+
+#: audits/serializers.py:33 ops/models/adhoc.py:25 ops/models/base.py:16
+#: ops/models/base.py:53 ops/models/celery.py:86 ops/models/job.py:147
+#: ops/models/job.py:235 ops/models/playbook.py:30
+#: terminal/models/session/sharing.py:25
+msgid "Creator"
+msgstr "創建者"
+
+#: audits/serializers.py:69
+msgid "Reason display"
+msgstr "原因描述"
+
+#: audits/serializers.py:134
+#, python-format
+msgid "User %s %s this resource"
+msgstr "用戶 %s %s 了當前資源"
+
+#: audits/serializers.py:172 authentication/models/connection_token.py:47
+#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:80
+#: tickets/models/ticket/apply_application.py:31
+#: tickets/models/ticket/apply_asset.py:20 users/models/user.py:855
+msgid "Date expired"
+msgstr "失效日期"
+
+#: audits/signal_handlers/activity_log.py:26
+#, python-format
+msgid "User %s use account %s login asset %s"
+msgstr "用戶 %s 使用帳號 %s 登錄資產 %s"
+
+#: audits/signal_handlers/activity_log.py:34
+#, python-format
+msgid "User %s login system %s"
+msgstr "用戶 %s 登錄系統 %s"
+
+#: audits/signal_handlers/activity_log.py:64
+#, python-format
+msgid "User %s perform a task for this resource: %s"
+msgstr "用戶 %s 在當前資源, 執行了任務 (%s)"
+
+#: audits/signal_handlers/login_log.py:33
+msgid "SSH Key"
+msgstr "SSH 金鑰"
+
+#: audits/signal_handlers/login_log.py:35 settings/serializers/auth/sso.py:13
+msgid "SSO"
+msgstr "SSO"
+
+#: audits/signal_handlers/login_log.py:36
+msgid "Auth Token"
+msgstr "認證令牌"
+
+#: audits/signal_handlers/login_log.py:37 authentication/notifications.py:73
+#: authentication/views/login.py:77 notifications/backends/__init__.py:11
+#: settings/serializers/auth/wecom.py:10 users/models/user.py:759
+#: users/models/user.py:869
+msgid "WeCom"
+msgstr "企業微信"
+
+#: audits/signal_handlers/login_log.py:38 authentication/views/feishu.py:105
+#: authentication/views/login.py:89 notifications/backends/__init__.py:14
+#: settings/serializers/auth/feishu.py:10 users/models/user.py:761
+#: users/models/user.py:871
+msgid "FeiShu"
+msgstr "飛書"
+
+#: audits/signal_handlers/login_log.py:40 authentication/views/login.py:101
+#: authentication/views/slack.py:87 notifications/backends/__init__.py:16
+#: settings/serializers/auth/slack.py:10 users/models/user.py:763
+#: users/models/user.py:873
+msgid "Slack"
+msgstr ""
+
+#: audits/signal_handlers/login_log.py:41 authentication/views/dingtalk.py:161
+#: authentication/views/login.py:83 notifications/backends/__init__.py:12
+#: settings/serializers/auth/dingtalk.py:10 users/models/user.py:760
+#: users/models/user.py:870
+msgid "DingTalk"
+msgstr "釘釘"
+
+#: audits/signal_handlers/login_log.py:42
+#: authentication/models/temp_token.py:16
+msgid "Temporary token"
+msgstr "臨時密碼"
+
+#: audits/signal_handlers/login_log.py:43 authentication/views/login.py:107
+#: settings/serializers/auth/passkey.py:8
+msgid "Passkey"
+msgstr "Passkey"
+
+#: audits/tasks.py:117
+msgid "Clean audits session task log"
+msgstr "清理資產審計會話任務日誌"
+
+#: audits/tasks.py:130
+msgid "Upload FTP file to external storage"
+msgstr "上傳 FTP 文件到外部儲存"
+
+#: authentication/api/access_key.py:39
+msgid "Access keys can be created at most 10"
+msgstr "最多可以創建10個訪問金鑰"
+
+#: authentication/api/common.py:34 settings/serializers/auth/sms.py:117
+#, python-format
+msgid "The value in the parameter must contain %s"
+msgstr "參數中的值必須包含 %s"
+
+#: authentication/api/confirm.py:50
+msgid "This action require verify your MFA"
+msgstr "該操作需要驗證您的 MFA, 請先開啟並配置"
+
+#: authentication/api/connection_token.py:265
+msgid "Reusable connection token is not allowed, global setting not enabled"
+msgstr "不允許使用可重複使用的連接令牌,未啟用全局設置"
+
+#: authentication/api/connection_token.py:379
+msgid "Anonymous account is not supported for this asset"
+msgstr "匿名帳號不支持當前資產"
+
+#: authentication/api/connection_token.py:399
+msgid "Account not found"
+msgstr "帳號未找到"
+
+#: authentication/api/connection_token.py:402
+msgid "Permission expired"
+msgstr "授權已過期"
+
+#: authentication/api/connection_token.py:435
+msgid "ACL action is reject: {}({})"
+msgstr "ACL 動作是拒絕: {}({})"
+
+#: authentication/api/connection_token.py:439
+msgid "ACL action is review"
+msgstr "ACL 動作是覆核"
+
+#: authentication/api/mfa.py:59
+msgid "Current user not support mfa type: {}"
+msgstr "當前用戶不支持 MFA 類型: {}"
+
+#: authentication/api/password.py:33 terminal/api/session/session.py:325
+#: users/views/profile/reset.py:63
+msgid "User does not exist: {}"
+msgstr "用戶不存在: {}"
+
+#: authentication/api/password.py:33 users/views/profile/reset.py:166
+msgid "No user matched"
+msgstr "沒有匹配到用戶"
+
+#: authentication/api/password.py:37
+msgid ""
+"The user is from {}, please go to the corresponding system to change the "
+"password"
+msgstr "用戶來自 {} 請去相應系統修改密碼"
+
+#: authentication/api/password.py:65
+#: authentication/templates/authentication/login.html:361
+#: users/templates/users/forgot_password.html:41
+#: users/templates/users/forgot_password.html:42
+#: users/templates/users/forgot_password_previewing.html:13
+#: users/templates/users/forgot_password_previewing.html:14
+msgid "Forgot password"
+msgstr "忘記密碼"
+
+#: authentication/apps.py:7 settings/serializers/auth/base.py:10
+msgid "Authentication"
+msgstr "認證"
+
+#: authentication/backends/custom.py:59
+#: authentication/backends/oauth2/backends.py:173
+msgid "User invalid, disabled or expired"
+msgstr "用戶無效,已禁用或已過期"
+
+#: authentication/backends/drf.py:52
+msgid "Invalid token header. No credentials provided."
+msgstr "無效的令牌頭。沒有提供任何憑據。"
+
+#: authentication/backends/drf.py:55
+msgid "Invalid token header. Sign string should not contain spaces."
+msgstr "無效的令牌頭。符號字串不應包含空格。"
+
+#: authentication/backends/drf.py:61
+msgid ""
+"Invalid token header. Sign string should not contain invalid characters."
+msgstr "無效的令牌頭。符號字串不應包含無效字元。"
+
+#: authentication/backends/drf.py:74
+msgid "Invalid token or cache refreshed."
+msgstr "刷新的令牌或快取無效。"
+
+#: authentication/backends/passkey/api.py:37
+msgid "Only register passkey for local user"
+msgstr "僅為本地用戶註冊金鑰"
+
+#: authentication/backends/passkey/api.py:65
+msgid "Auth failed"
+msgstr "認證失敗"
+
+#: authentication/backends/passkey/fido.py:148
+msgid "This key is not registered"
+msgstr "此金鑰未註冊"
+
+#: authentication/backends/passkey/models.py:13
+msgid "Added on"
+msgstr "附加"
+
+#: authentication/backends/passkey/models.py:14
+#: authentication/models/access_key.py:26
+#: authentication/models/private_token.py:8
+msgid "Date last used"
+msgstr "最後使用日期"
+
+#: authentication/backends/passkey/models.py:15
+msgid "Credential ID"
+msgstr "憑證 ID"
+
+#: authentication/confirm/password.py:16
+msgid "Authentication failed password incorrect"
+msgstr "認證失敗 (使用者名稱或密碼不正確)"
+
+#: authentication/confirm/relogin.py:10
+msgid "Login time has exceeded {} minutes, please login again"
+msgstr "登錄時長已超過 {} 分鐘,請重新登入"
+
+#: authentication/errors/const.py:18
+msgid "Username/password check failed"
+msgstr "使用者名稱/密碼 校驗失敗"
+
+#: authentication/errors/const.py:19
+msgid "Password decrypt failed"
+msgstr "密碼解密失敗"
+
+#: authentication/errors/const.py:20
+msgid "MFA failed"
+msgstr "MFA 校驗失敗"
+
+#: authentication/errors/const.py:21
+msgid "MFA unset"
+msgstr "MFA 沒有設定"
+
+#: authentication/errors/const.py:22
+msgid "Username does not exist"
+msgstr "使用者名稱不存在"
+
+#: authentication/errors/const.py:23
+msgid "Password expired"
+msgstr "密碼已過期"
+
+#: authentication/errors/const.py:24
+msgid "Disabled or expired"
+msgstr "禁用或失效"
+
+#: authentication/errors/const.py:25
+msgid "This account is inactive."
+msgstr "此帳號已禁用"
+
+#: authentication/errors/const.py:26
+msgid "This account is expired"
+msgstr "此帳號已過期"
+
+#: authentication/errors/const.py:27
+msgid "Auth backend not match"
+msgstr "沒有匹配到認證後端"
+
+#: authentication/errors/const.py:28
+msgid "ACL is not allowed"
+msgstr "登錄訪問控制不被允許"
+
+#: authentication/errors/const.py:29
+msgid "Only local users are allowed"
+msgstr "僅允許本地用戶"
+
+#: authentication/errors/const.py:39
+msgid "No session found, check your cookie"
+msgstr "會話已變更,刷新頁面"
+
+#: authentication/errors/const.py:41
+#, python-brace-format
+msgid ""
+"The username or password you entered is incorrect, please enter it again. "
+"You can also try {times_try} times (The account will be temporarily locked "
+"for {block_time} minutes)"
+msgstr ""
+"您輸入的使用者名稱或密碼不正確,請重新輸入。 您還可以嘗試 {times_try} 次 (帳"
+"號將被臨時 鎖定 {block_time} 分鐘)"
+
+#: authentication/errors/const.py:47 authentication/errors/const.py:55
+msgid ""
+"The account has been locked (please contact admin to unlock it or try again "
+"after {} minutes)"
+msgstr "帳號已被鎖定 (請聯絡管理員解鎖或{}分鐘後重試)"
+
+#: authentication/errors/const.py:51
+msgid ""
+"The address has been locked (please contact admin to unlock it or try again "
+"after {} minutes)"
+msgstr "IP 已被鎖定 (請聯絡管理員解鎖或 {} 分鐘後重試)"
+
+#: authentication/errors/const.py:59
+#, python-brace-format
+msgid ""
+"{error}, You can also try {times_try} times (The account will be temporarily "
+"locked for {block_time} minutes)"
+msgstr ""
+"{error},您還可以嘗試 {times_try} 次 (帳號將被臨時鎖定 {block_time} 分鐘)"
+
+#: authentication/errors/const.py:63
+msgid "MFA required"
+msgstr "需要 MFA 認證"
+
+#: authentication/errors/const.py:64
+msgid "MFA not set, please set it first"
+msgstr "MFA 沒有設置,請先完成設置"
+
+#: authentication/errors/const.py:65
+msgid "Login confirm required"
+msgstr "需要登錄覆核"
+
+#: authentication/errors/const.py:66
+msgid "Wait login confirm ticket for accept"
+msgstr "等待登錄覆核處理"
+
+#: authentication/errors/const.py:67
+msgid "Login confirm ticket was {}"
+msgstr "登錄覆核: {}"
+
+#: authentication/errors/failed.py:149
+msgid "Current IP and Time period is not allowed"
+msgstr "當前 IP 和時間段不被允許登錄"
+
+#: authentication/errors/failed.py:154
+msgid "Please enter MFA code"
+msgstr "請輸入 MFA 驗證碼"
+
+#: authentication/errors/failed.py:159
+msgid "Please enter SMS code"
+msgstr "請輸入簡訊驗證碼"
+
+#: authentication/errors/failed.py:164 users/exceptions.py:15
+msgid "Phone not set"
+msgstr "手機號碼沒有設置"
+
+#: authentication/errors/mfa.py:8
+msgid "SSO auth closed"
+msgstr "SSO 認證關閉了"
+
+#: authentication/errors/mfa.py:18 authentication/views/wecom.py:59
+msgid "WeCom is already bound"
+msgstr "企業微信已經綁定"
+
+#: authentication/errors/mfa.py:23 authentication/views/wecom.py:159
+#: authentication/views/wecom.py:201
+msgid "WeCom is not bound"
+msgstr "沒有綁定企業微信"
+
+#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:213
+#: authentication/views/dingtalk.py:255
+msgid "DingTalk is not bound"
+msgstr "釘釘沒有綁定"
+
+#: authentication/errors/mfa.py:33 authentication/views/feishu.py:138
+msgid "FeiShu is not bound"
+msgstr "沒有綁定飛書"
+
+#: authentication/errors/mfa.py:38 authentication/views/lark.py:48
+msgid "Lark is not bound"
+msgstr "Lark 沒有綁定"
+
+#: authentication/errors/mfa.py:43 authentication/views/slack.py:127
+msgid "Slack is not bound"
+msgstr "Slack 沒有綁定"
+
+#: authentication/errors/mfa.py:48
+msgid "Your password is invalid"
+msgstr "您的密碼無效"
+
+#: authentication/errors/mfa.py:53
+#, python-format
+msgid "Please wait for %s seconds before retry"
+msgstr "請在 %s 秒後重試"
+
+#: authentication/errors/redirect.py:85 authentication/mixins.py:323
+msgid "Your password is too simple, please change it for security"
+msgstr "你的密碼過於簡單,為了安全,請修改"
+
+#: authentication/errors/redirect.py:93 authentication/mixins.py:330
+msgid "You should to change your password before login"
+msgstr "登錄完成前,請先修改密碼"
+
+#: authentication/errors/redirect.py:101 authentication/mixins.py:337
+msgid "Your password has expired, please reset before logging in"
+msgstr "您的密碼已過期,先修改再登錄"
+
+#: authentication/forms.py:39
+msgid "Auto login"
+msgstr "自動登錄"
+
+#: authentication/forms.py:52
+msgid "MFA Code"
+msgstr "MFA 驗證碼"
+
+#: authentication/forms.py:53
+msgid "MFA type"
+msgstr "MFA 類型"
+
+#: authentication/forms.py:61
+#: authentication/templates/authentication/_captcha_field.html:15
+msgid "Captcha"
+msgstr "驗證碼"
+
+#: authentication/forms.py:66 users/forms/profile.py:27
+msgid "MFA code"
+msgstr "MFA 驗證碼"
+
+#: authentication/forms.py:68
+msgid "Dynamic code"
+msgstr "動態碼"
+
+#: authentication/mfa/base.py:7
+msgid "Please input security code"
+msgstr "請輸入動態安全碼"
+
+#: authentication/mfa/custom.py:20
+msgid "MFA Custom code invalid"
+msgstr "自訂 MFA 驗證碼校驗失敗"
+
+#: authentication/mfa/custom.py:26
+msgid "MFA custom verification code"
+msgstr "自訂 MFA 驗證碼"
+
+#: authentication/mfa/custom.py:56
+msgid "MFA custom global enabled, cannot disable"
+msgstr "自訂 MFA 全局開啟,無法被禁用"
+
+#: authentication/mfa/otp.py:7
+msgid "OTP code invalid, or server time error"
+msgstr "虛擬 MFA 驗證碼錯誤,或者伺服器端時間不對"
+
+#: authentication/mfa/otp.py:12
+msgid "OTP"
+msgstr "虛擬 MFA"
+
+#: authentication/mfa/otp.py:13
+msgid "OTP verification code"
+msgstr "虛擬 MFA 驗證碼"
+
+#: authentication/mfa/otp.py:48
+msgid "Virtual OTP based MFA"
+msgstr "虛擬 MFA(OTP)"
+
+#: authentication/mfa/radius.py:7
+msgid "Radius verify code invalid"
+msgstr "Radius 校驗失敗"
+
+#: authentication/mfa/radius.py:13
+msgid "Radius verification code"
+msgstr "Radius 動態安全碼"
+
+#: authentication/mfa/radius.py:44
+msgid "Radius global enabled, cannot disable"
+msgstr "Radius MFA 全局開啟,無法被禁用"
+
+#: authentication/mfa/sms.py:7
+msgid "SMS verify code invalid"
+msgstr "簡訊驗證碼校驗失敗"
+
+#: authentication/mfa/sms.py:12 authentication/serializers/password_mfa.py:16
+#: authentication/serializers/password_mfa.py:24
+#: settings/serializers/auth/sms.py:32 users/forms/profile.py:103
+#: users/forms/profile.py:108 users/templates/users/forgot_password.html:157
+#: users/views/profile/reset.py:100
+msgid "SMS"
+msgstr "簡訊"
+
+#: authentication/mfa/sms.py:13
+msgid "SMS verification code"
+msgstr "簡訊驗證碼"
+
+#: authentication/mfa/sms.py:57
+msgid "Set phone number to enable"
+msgstr "設置手機號碼啟用"
+
+#: authentication/mfa/sms.py:61
+msgid "Clear phone number to disable"
+msgstr "清空手機號碼禁用"
+
+#: authentication/middleware.py:94 settings/utils/ldap.py:679
+msgid "Authentication failed (before login check failed): {}"
+msgstr "認證失敗 (登錄前檢查失敗): {}"
+
+#: authentication/mixins.py:82
+msgid "User is invalid"
+msgstr "無效的用戶"
+
+#: authentication/mixins.py:97
+msgid ""
+"The administrator has enabled 'Only allow login from user source'. \n"
+" The current user source is {}. Please contact the administrator."
+msgstr "管理員已開啟'僅允許從用戶來源登錄',當前用戶來源為{},請聯絡管理員。"
+
+#: authentication/mixins.py:273
+msgid "The MFA type ({}) is not enabled"
+msgstr "該 MFA ({}) 方式沒有啟用"
+
+#: authentication/mixins.py:313
+msgid "Please change your password"
+msgstr "請修改密碼"
+
+#: authentication/models/access_key.py:22
+#: terminal/models/component/endpoint.py:110
+msgid "IP group"
+msgstr "IPグループ"
+
+#: authentication/models/connection_token.py:38
+#: terminal/serializers/storage.py:114
+msgid "Account name"
+msgstr "帳號名稱"
+
+#: authentication/models/connection_token.py:39
+msgid "Input username"
+msgstr "自訂使用者名稱"
+
+#: authentication/models/connection_token.py:40
+#: authentication/serializers/connection_token.py:18
+msgid "Input secret"
+msgstr "自訂密碼"
+
+#: authentication/models/connection_token.py:42
+msgid "Connect method"
+msgstr "連接方式"
+
+#: authentication/models/connection_token.py:43
+msgid "Connect options"
+msgstr "連接項"
+
+#: authentication/models/connection_token.py:44
+msgid "User display"
+msgstr "使用者名稱"
+
+#: authentication/models/connection_token.py:45
+msgid "Asset display"
+msgstr "資產名稱"
+
+#: authentication/models/connection_token.py:46
+msgid "Reusable"
+msgstr "可以重複使用"
+
+#: authentication/models/connection_token.py:51
+#: perms/models/asset_permission.py:83
+msgid "From ticket"
+msgstr "來自工單"
+
+#: authentication/models/connection_token.py:58
+msgid "Can expire connection token"
+msgstr "可以失效連接令牌"
+
+#: authentication/models/connection_token.py:59
+msgid "Can reuse connection token"
+msgstr "可以復用連接令牌"
+
+#: authentication/models/connection_token.py:61
+msgid "Connection token"
+msgstr "連接令牌"
+
+#: authentication/models/connection_token.py:118
+msgid "Connection token inactive"
+msgstr "連接令牌未啟用"
+
+#: authentication/models/connection_token.py:122
+msgid "Connection token expired at: {}"
+msgstr "連接令牌過期: {}"
+
+#: authentication/models/connection_token.py:125
+msgid "No user or invalid user"
+msgstr "沒有用戶或用戶失效"
+
+#: authentication/models/connection_token.py:128
+msgid "No asset or inactive asset"
+msgstr "沒有資產或資產未啟用"
+
+#: authentication/models/connection_token.py:272
+msgid "Can view super connection token secret"
+msgstr "可以查看超級連接令牌密文"
+
+#: authentication/models/connection_token.py:274
+msgid "Super connection token"
+msgstr "超級連接令牌"
+
+#: authentication/models/private_token.py:11
+msgid "Private Token"
+msgstr "私有令牌"
+
+#: authentication/models/sso_token.py:15
+msgid "Expired"
+msgstr "過期時間"
+
+#: authentication/models/sso_token.py:20
+msgid "SSO token"
+msgstr "SSO token"
+
+#: authentication/models/temp_token.py:11
+msgid "Verified"
+msgstr "已校驗"
+
+#: authentication/notifications.py:19
+msgid "Different city login reminder"
+msgstr "異地登錄提醒"
+
+#: authentication/notifications.py:52
+msgid "binding reminder"
+msgstr "綁定提醒"
+
+#: authentication/serializers/connect_token_secret.py:116
+msgid "Is builtin"
+msgstr "內建的"
+
+#: authentication/serializers/connect_token_secret.py:120
+msgid "Options"
+msgstr "選項"
+
+#: authentication/serializers/connect_token_secret.py:127
+msgid "Component"
+msgstr "組件"
+
+#: authentication/serializers/connect_token_secret.py:138
+msgid "Expired now"
+msgstr "立刻過期"
+
+#: authentication/serializers/connect_token_secret.py:169
+#: terminal/models/virtualapp/virtualapp.py:25
+msgid "Image name"
+msgstr "鏡像名稱"
+
+#: authentication/serializers/connect_token_secret.py:170
+#: terminal/models/virtualapp/virtualapp.py:27
+msgid "Image port"
+msgstr "鏡像埠"
+
+#: authentication/serializers/connect_token_secret.py:171
+#: terminal/models/virtualapp/virtualapp.py:26
+msgid "Image protocol"
+msgstr "鏡像協議"
+
+#: authentication/serializers/connection_token.py:16
+msgid "Expired time"
+msgstr "過期時間"
+
+#: authentication/serializers/connection_token.py:20
+msgid "Ticket info"
+msgstr "工單資訊"
+
+#: authentication/serializers/connection_token.py:21
+#: perms/models/asset_permission.py:77 perms/serializers/permission.py:38
+#: perms/serializers/permission.py:59
+#: tickets/models/ticket/apply_application.py:28
+#: tickets/models/ticket/apply_asset.py:18
+msgid "Actions"
+msgstr "動作"
+
+#: authentication/serializers/connection_token.py:42
+#: perms/serializers/permission.py:40 perms/serializers/permission.py:60
+#: users/serializers/user.py:100 users/serializers/user.py:177
+msgid "Is expired"
+msgstr "已過期"
+
+#: authentication/serializers/password_mfa.py:29
+#: users/templates/users/forgot_password.html:153
+msgid "The {} cannot be empty"
+msgstr "{} 不能為空"
+
+#: authentication/serializers/token.py:22
+msgid "Access IP"
+msgstr "IP 白名單"
+
+#: authentication/serializers/token.py:92 perms/serializers/permission.py:39
+#: perms/serializers/permission.py:61 users/serializers/user.py:101
+#: users/serializers/user.py:174
+msgid "Is valid"
+msgstr "是否有效"
+
+#: authentication/tasks.py:11
+msgid "Clean expired session"
+msgstr "清除過期會話"
+
+#: authentication/templates/authentication/_access_key_modal.html:6
+msgid "API key list"
+msgstr "API Key列表"
+
+#: authentication/templates/authentication/_access_key_modal.html:18
+msgid "Using api key sign api header, every requests header difference"
+msgstr "使用api key簽名請求頭,每個請求的頭部是不一樣的"
+
+#: authentication/templates/authentication/_access_key_modal.html:19
+msgid "docs"
+msgstr "文件"
+
+#: authentication/templates/authentication/_access_key_modal.html:48
+msgid "Show"
+msgstr "顯示"
+
+#: authentication/templates/authentication/_access_key_modal.html:66
+#: users/const.py:42 users/models/user.py:654 users/serializers/profile.py:92
+#: users/templates/users/user_verify_mfa.html:36
+msgid "Disable"
+msgstr "禁用"
+
+#: authentication/templates/authentication/_access_key_modal.html:67
+#: users/const.py:43 users/models/user.py:655 users/serializers/profile.py:93
+#: users/templates/users/mfa_setting.html:26
+#: users/templates/users/mfa_setting.html:68
+msgid "Enable"
+msgstr "啟用"
+
+#: authentication/templates/authentication/_access_key_modal.html:147
+msgid "Delete success"
+msgstr "刪除成功"
+
+#: authentication/templates/authentication/_captcha_field.html:8
+msgid "Play CAPTCHA as audio file"
+msgstr "語言播放驗證碼"
+
+#: authentication/templates/authentication/_mfa_confirm_modal.html:5
+msgid "MFA confirm"
+msgstr "MFA 認證校驗"
+
+#: authentication/templates/authentication/_mfa_confirm_modal.html:17
+msgid "Need MFA for view auth"
+msgstr "需要 MFA 認證來查看帳號資訊"
+
+#: authentication/templates/authentication/_mfa_confirm_modal.html:20
+#: authentication/templates/authentication/auth_fail_flash_message_standalone.html:37
+#: templates/_modal.html:23 templates/flash_message_standalone.html:37
+#: users/templates/users/user_password_verify.html:20
+msgid "Confirm"
+msgstr "確認"
+
+#: authentication/templates/authentication/_mfa_confirm_modal.html:25
+msgid "Code error"
+msgstr "代碼錯誤"
+
+#: authentication/templates/authentication/_msg_different_city.html:3
+#: authentication/templates/authentication/_msg_oauth_bind.html:3
+#: authentication/templates/authentication/_msg_reset_password.html:3
+#: authentication/templates/authentication/_msg_reset_password_code.html:9
+#: authentication/templates/authentication/_msg_rest_password_success.html:2
+#: authentication/templates/authentication/_msg_rest_public_key_success.html:2
+#: jumpserver/conf.py:465
+#: perms/templates/perms/_msg_item_permissions_expire.html:3
+#: perms/templates/perms/_msg_permed_items_expire.html:3
+#: tickets/templates/tickets/approve_check_password.html:32
+#: users/templates/users/_msg_account_expire_reminder.html:4
+#: users/templates/users/_msg_password_expire_reminder.html:4
+#: users/templates/users/_msg_reset_mfa.html:4
+#: users/templates/users/_msg_reset_ssh_key.html:4
+msgid "Hello"
+msgstr "你好"
+
+#: authentication/templates/authentication/_msg_different_city.html:6
+msgid "Your account has remote login behavior, please pay attention"
+msgstr "你的帳號存在異地登入行為,請關注。"
+
+#: authentication/templates/authentication/_msg_different_city.html:10
+msgid "Login time"
+msgstr "登錄日期"
+
+#: authentication/templates/authentication/_msg_different_city.html:16
+msgid ""
+"If you suspect that the login behavior is abnormal, please modify the "
+"account password in time."
+msgstr "若懷疑此次登錄行為異常,請及時修改帳號密碼"
+
+#: authentication/templates/authentication/_msg_oauth_bind.html:6
+msgid "Your account has just been bound to"
+msgstr "您的帳戶剛剛綁定到"
+
+#: authentication/templates/authentication/_msg_oauth_bind.html:17
+msgid "If the operation is not your own, unbind and change the password."
+msgstr "如果操作不是您本人,請解綁並且修改密碼"
+
+#: authentication/templates/authentication/_msg_reset_password.html:6
+msgid ""
+"Please click the link below to reset your password, if not your request, "
+"concern your account security"
+msgstr "請點擊下面連結重設密碼, 如果不是您申請的,請關注帳號安全"
+
+#: authentication/templates/authentication/_msg_reset_password.html:10
+msgid "Click here reset password"
+msgstr "點擊這裡重設密碼"
+
+#: authentication/templates/authentication/_msg_reset_password.html:16
+#: users/templates/users/_msg_user_created.html:22
+msgid "This link is valid for 1 hour. After it expires"
+msgstr "這個連結有效期1小時, 超過時間您可以"
+
+#: authentication/templates/authentication/_msg_reset_password.html:17
+#: users/templates/users/_msg_user_created.html:23
+msgid "request new one"
+msgstr "重新申請"
+
+#: authentication/templates/authentication/_msg_reset_password_code.html:12
+#: terminal/models/session/sharing.py:27 terminal/models/session/sharing.py:97
+#: terminal/templates/terminal/_msg_session_sharing.html:12
+#: users/forms/profile.py:106 users/templates/users/forgot_password.html:98
+msgid "Verify code"
+msgstr "驗證碼"
+
+#: authentication/templates/authentication/_msg_reset_password_code.html:15
+msgid ""
+"Copy the verification code to the Reset Password page to reset the password."
+msgstr "將驗證碼複製到重設密碼頁面,重設密碼。"
+
+#: authentication/templates/authentication/_msg_reset_password_code.html:18
+msgid "The validity period of the verification code is one minute"
+msgstr "驗證碼有效期為 1 分鐘"
+
+#: authentication/templates/authentication/_msg_rest_password_success.html:5
+msgid "Your password has just been successfully updated"
+msgstr "你的密碼剛剛成功更新"
+
+#: authentication/templates/authentication/_msg_rest_password_success.html:9
+#: authentication/templates/authentication/_msg_rest_public_key_success.html:9
+msgid "Browser"
+msgstr "瀏覽器"
+
+#: authentication/templates/authentication/_msg_rest_password_success.html:13
+msgid ""
+"If the password update was not initiated by you, your account may have "
+"security issues"
+msgstr "如果這次密碼更新不是由你發起的,那麼你的帳號可能存在安全問題"
+
+#: authentication/templates/authentication/_msg_rest_password_success.html:14
+#: authentication/templates/authentication/_msg_rest_public_key_success.html:14
+msgid "If you have any questions, you can contact the administrator"
+msgstr "如果有疑問或需求,請聯絡系統管理員"
+
+#: authentication/templates/authentication/_msg_rest_public_key_success.html:5
+msgid "Your public key has just been successfully updated"
+msgstr "你的公鑰剛剛成功更新"
+
+#: authentication/templates/authentication/_msg_rest_public_key_success.html:13
+msgid ""
+"If the public key update was not initiated by you, your account may have "
+"security issues"
+msgstr "如果這次公鑰更新不是由你發起的,那麼你的帳號可能存在安全問題"
+
+#: authentication/templates/authentication/auth_fail_flash_message_standalone.html:28
+#: templates/flash_message_standalone.html:28 tickets/const.py:18
+msgid "Cancel"
+msgstr "取消"
+
+#: authentication/templates/authentication/login.html:276
+msgid ""
+"Configuration file has problems and cannot be logged in. Please contact the "
+"administrator or view latest docs"
+msgstr "配置文件有問題,無法登錄,請聯絡管理員或查看最新文件"
+
+#: authentication/templates/authentication/login.html:277
+msgid "If you are administrator, you can update the config resolve it, set"
+msgstr "如果你是管理員,可以更新配置文件解決,設置配置項"
+
+#: authentication/templates/authentication/login.html:376
+msgid "More login options"
+msgstr "其他方式登錄"
+
+#: authentication/templates/authentication/login_mfa.html:6
+msgid "MFA Auth"
+msgstr "MFA 多因子認證"
+
+#: authentication/templates/authentication/login_mfa.html:19
+#: users/templates/users/user_otp_check_password.html:12
+#: users/templates/users/user_otp_enable_bind.html:24
+#: users/templates/users/user_otp_enable_install_app.html:31
+#: users/templates/users/user_verify_mfa.html:30
+msgid "Next"
+msgstr "下一步"
+
+#: authentication/templates/authentication/login_mfa.html:22
+msgid "Can't provide security? Please contact the administrator!"
+msgstr "如果不能提供 MFA 驗證碼,請聯絡管理員!"
+
+#: authentication/templates/authentication/login_wait_confirm.html:41
+msgid "Refresh"
+msgstr "刷新"
+
+#: authentication/templates/authentication/login_wait_confirm.html:46
+msgid "Copy link"
+msgstr "複製連結"
+
+#: authentication/templates/authentication/login_wait_confirm.html:51
+msgid "Return"
+msgstr "返回"
+
+#: authentication/templates/authentication/login_wait_confirm.html:116
+msgid "Copy success"
+msgstr "複製成功"
+
+#: authentication/templates/authentication/passkey.html:162
+msgid ""
+"This page is not served over HTTPS. Please use HTTPS to ensure security of "
+"your credentials."
+msgstr "本頁面未使用 HTTPS 協議,請使用 HTTPS 協議以確保您的憑據安全。"
+
+#: authentication/templates/authentication/passkey.html:173
+msgid "Do you want to retry ?"
+msgstr "是否重試 ?"
+
+#: authentication/utils.py:24 common/utils/ip/geoip/utils.py:24
+#: xpack/plugins/cloud/const.py:33
+msgid "LAN"
+msgstr "區域網路"
+
+#: authentication/views/base.py:73
+#: perms/templates/perms/_msg_permed_items_expire.html:21
+msgid "If you have any question, please contact the administrator"
+msgstr "如果有疑問或需求,請聯絡系統管理員"
+
+#: authentication/views/base.py:146
+#, python-format
+msgid "%s query user failed"
+msgstr "%s 查詢用戶失敗"
+
+#: authentication/views/base.py:155
+#, python-format
+msgid "The %s is already bound to another user"
+msgstr "%s 已綁定到另一個用戶"
+
+#: authentication/views/base.py:162
+#, python-format
+msgid "Binding %s successfully"
+msgstr "綁定 %s 成功"
+
+#: authentication/views/dingtalk.py:42
+msgid "DingTalk Error, Please contact your system administrator"
+msgstr "釘釘錯誤,請聯絡系統管理員"
+
+#: authentication/views/dingtalk.py:45 authentication/views/dingtalk.py:212
+msgid "DingTalk Error"
+msgstr "釘釘錯誤"
+
+#: authentication/views/dingtalk.py:57 authentication/views/feishu.py:68
+#: authentication/views/slack.py:47 authentication/views/wecom.py:55
+msgid ""
+"The system configuration is incorrect. Please contact your administrator"
+msgstr "企業配置錯誤,請聯絡系統管理員"
+
+#: authentication/views/dingtalk.py:61
+msgid "DingTalk is already bound"
+msgstr "釘釘已經綁定"
+
+#: authentication/views/dingtalk.py:130
+msgid "Invalid user_id"
+msgstr "無效的 user_id"
+
+#: authentication/views/dingtalk.py:146
+msgid "DingTalk query user failed"
+msgstr "釘釘查詢用戶失敗"
+
+#: authentication/views/dingtalk.py:155
+msgid "The DingTalk is already bound to another user"
+msgstr "該釘釘已經綁定其他用戶"
+
+#: authentication/views/dingtalk.py:162
+msgid "Binding DingTalk successfully"
+msgstr "綁定 釘釘 成功"
+
+#: authentication/views/dingtalk.py:214 authentication/views/dingtalk.py:249
+msgid "Failed to get user from DingTalk"
+msgstr "從釘釘獲取用戶失敗"
+
+#: authentication/views/dingtalk.py:256
+msgid "Please login with a password and then bind the DingTalk"
+msgstr "請使用密碼登錄,然後綁定釘釘"
+
+#: authentication/views/feishu.py:43 authentication/views/feishu.py:137
+msgid "FeiShu Error"
+msgstr "飛書錯誤"
+
+#: authentication/views/feishu.py:44
+msgid "FeiShu is already bound"
+msgstr "飛書已經綁定"
+
+#: authentication/views/feishu.py:139
+msgid "Failed to get user from FeiShu"
+msgstr "從飛書獲取用戶失敗"
+
+#: authentication/views/lark.py:19 authentication/views/lark.py:47
+msgid "Lark Error"
+msgstr "Lark 錯誤"
+
+#: authentication/views/lark.py:20
+msgid "Lark is already bound"
+msgstr "Lark 已經綁定"
+
+#: authentication/views/lark.py:49
+msgid "Failed to get user from Lark"
+msgstr "從 Lark 獲取用戶失敗"
+
+#: authentication/views/login.py:227
+msgid "Redirecting"
+msgstr "跳轉中"
+
+#: authentication/views/login.py:228
+msgid "Redirecting to {} authentication"
+msgstr "正在跳轉到 {} 認證"
+
+#: authentication/views/login.py:251
+msgid "Login timeout, please try again."
+msgstr "登錄超時,請重新登入"
+
+#: authentication/views/login.py:294
+msgid "User email already exists ({})"
+msgstr "用戶信箱已存在 ({})"
+
+#: authentication/views/login.py:372
+msgid ""
+"Wait for {} confirm, You also can copy link to her/him
\n"
+" Don't close this page"
+msgstr ""
+"等待 {} 確認, 你也可以複製連結發給他/她
\n"
+" 不要關閉本頁面"
+
+#: authentication/views/login.py:377
+msgid "No ticket found"
+msgstr "沒有發現工單"
+
+#: authentication/views/login.py:413
+msgid "Logout success"
+msgstr "退出登錄成功"
+
+#: authentication/views/login.py:414
+msgid "Logout success, return login page"
+msgstr "退出登錄成功,返回到登入頁面"
+
+#: authentication/views/slack.py:35 authentication/views/slack.py:126
+msgid "Slack Error"
+msgstr "Slack 錯誤"
+
+#: authentication/views/slack.py:63
+msgid "Slack is already bound"
+msgstr "Slack 已經綁定"
+
+#: authentication/views/slack.py:128
+msgid "Failed to get user from Slack"
+msgstr "從 Slack 獲取用戶失敗"
+
+#: authentication/views/wecom.py:40
+msgid "WeCom Error, Please contact your system administrator"
+msgstr "企業微信錯誤,請聯絡系統管理員"
+
+#: authentication/views/wecom.py:43 authentication/views/wecom.py:158
+msgid "WeCom Error"
+msgstr "企業微信錯誤"
+
+#: authentication/views/wecom.py:118
+msgid "Wecom"
+msgstr ""
+
+#: authentication/views/wecom.py:160 authentication/views/wecom.py:195
+msgid "Failed to get user from WeCom"
+msgstr "從企業微信獲取用戶失敗"
+
+#: authentication/views/wecom.py:202
+msgid "Please login with a password and then bind the WeCom"
+msgstr "請使用密碼登錄,然後綁定企業微信"
+
+#: common/api/action.py:51
+msgid "Request file format may be wrong"
+msgstr "上傳的檔案格式錯誤 或 其它類型資源的文件"
+
+#: common/const/choices.py:10
+msgid "Manual trigger"
+msgstr "手動觸發"
+
+#: common/const/choices.py:11
+msgid "Timing trigger"
+msgstr "定時觸發"
+
+#: common/const/choices.py:15
+msgid "Ready"
+msgstr "準備"
+
+#: common/const/choices.py:17 ops/const.py:73
+msgid "Running"
+msgstr "運行中"
+
+#: common/const/choices.py:21
+msgid "Canceled"
+msgstr "取消"
+
+#: common/const/common.py:5
+#, python-format
+msgid "%(name)s was created successfully"
+msgstr "%(name)s 創建成功"
+
+#: common/const/common.py:6
+#, python-format
+msgid "%(name)s was updated successfully"
+msgstr "%(name)s 更新成功"
+
+#: common/db/encoder.py:11
+msgid "gettext_lazy"
+msgstr "gettext_lazy"
+
+#: common/db/fields.py:106
+msgid "Marshal dict data to char field"
+msgstr "編碼 dict 為 char"
+
+#: common/db/fields.py:110
+msgid "Marshal dict data to text field"
+msgstr "編碼 dict 為 text"
+
+#: common/db/fields.py:122
+msgid "Marshal list data to char field"
+msgstr "編碼 list 為 char"
+
+#: common/db/fields.py:126
+msgid "Marshal list data to text field"
+msgstr "編碼 list 為 text"
+
+#: common/db/fields.py:130
+msgid "Marshal data to char field"
+msgstr "編碼數據為 char"
+
+#: common/db/fields.py:134
+msgid "Marshal data to text field"
+msgstr "編碼數據為 text"
+
+#: common/db/fields.py:167
+msgid "Encrypt field using Secret Key"
+msgstr "加密的欄位"
+
+#: common/db/fields.py:582
+msgid ""
+"Invalid JSON data for JSONManyToManyField, should be like {'type': 'all'} or "
+"{'type': 'ids', 'ids': []} or {'type': 'attrs', 'attrs': [{'name': 'ip', "
+"'match': 'exact', 'value': '1.1.1.1'}}"
+msgstr ""
+"JSON 多對多欄位無效,應為 {'type': 'all'} 或 {'type': 'ids', 'ids': []} 或 "
+"{'type': 'attrs', 'attrs': [{'name': 'ip', 'match': 'exact', 'value': "
+"'1.1.1.1'}}"
+
+#: common/db/fields.py:589
+msgid "Invalid type, should be \"all\", \"ids\" or \"attrs\""
+msgstr "無效類型,應為 all、ids 或 attrs"
+
+#: common/db/fields.py:592
+msgid "Invalid ids for ids, should be a list"
+msgstr "無效的ID,應為列表"
+
+#: common/db/fields.py:594 common/db/fields.py:599
+#: common/serializers/fields.py:133 tickets/serializers/ticket/common.py:58
+#: xpack/plugins/cloud/serializers/account_attrs.py:56
+#: xpack/plugins/cloud/serializers/account_attrs.py:79
+#: xpack/plugins/cloud/serializers/account_attrs.py:150
+msgid "This field is required."
+msgstr "該欄位是必填項。"
+
+#: common/db/fields.py:597 common/db/fields.py:602
+msgid "Invalid attrs, should be a list of dict"
+msgstr "無效的屬性,應為dict列表"
+
+#: common/db/fields.py:604
+msgid "Invalid attrs, should be has name and value"
+msgstr "無效屬性,應具有名稱和值"
+
+#: common/db/mixins.py:32
+msgid "is discard"
+msgstr "忽略的"
+
+#: common/db/mixins.py:33
+msgid "discard time"
+msgstr "忽略時間"
+
+#: common/db/models.py:33 users/models/user.py:858
+msgid "Updated by"
+msgstr "最後更新者"
+
+#: common/db/validators.py:9
+msgid "Invalid port range, should be like and within {}-{}"
+msgstr "無效的埠範圍,應該在 {}-{} 之內"
+
+#: common/drf/exc_handlers.py:26
+msgid "Object"
+msgstr "對象"
+
+#: common/drf/metadata.py:127
+msgid "Organization ID"
+msgstr "組織 ID"
+
+#: common/drf/parsers/base.py:21
+msgid "The file content overflowed (The maximum length `{}` bytes)"
+msgstr "文件內容太大 (最大長度 `{}` 位元組)"
+
+#: common/drf/parsers/base.py:199
+msgid "Parse file error: {}"
+msgstr "解析文件錯誤: {}"
+
+#: common/drf/parsers/excel.py:14
+msgid "Invalid excel file"
+msgstr "無效的 excel 文件"
+
+#: common/drf/renders/base.py:208
+msgid ""
+"{} - The encryption password has not been set - please go to personal "
+"information -> file encryption password to set the encryption password"
+msgstr "{} - 未設置加密密碼 - 請前往個人資訊 -> 文件加密密碼中設置加密密碼"
+
+#: common/exceptions.py:15
+#, python-format
+msgid "%s object does not exist."
+msgstr "%s對象不存在"
+
+#: common/exceptions.py:25
+msgid "Someone else is doing this. Please wait for complete"
+msgstr "其他人正在操作,請等待他人完成"
+
+#: common/exceptions.py:30
+msgid "Your request timeout"
+msgstr "您的請求超時了"
+
+#: common/exceptions.py:35
+msgid "M2M reverse not allowed"
+msgstr "多對多反向是不被允許的"
+
+#: common/exceptions.py:41
+msgid "Is referenced by other objects and cannot be deleted"
+msgstr "被其他對象關聯,不能刪除"
+
+#: common/exceptions.py:51
+msgid "This action require confirm current user"
+msgstr "此操作需要確認當前用戶"
+
+#: common/exceptions.py:59
+msgid "Unexpect error occur"
+msgstr "發生意外錯誤"
+
+#: common/plugins/es.py:31
+msgid "Invalid elasticsearch config"
+msgstr "無效的 Elasticsearch 配置"
+
+#: common/plugins/es.py:36
+msgid "Not Support Elasticsearch8"
+msgstr "不支持 Elasticsearch8"
+
+#: common/sdk/im/exceptions.py:23
+msgid "Network error, please contact system administrator"
+msgstr "網路錯誤,請聯絡系統管理員"
+
+#: common/sdk/im/slack/__init__.py:77
+msgid "Unknown error occur"
+msgstr "發生未知錯誤"
+
+#: common/sdk/im/wecom/__init__.py:16
+msgid "WeCom error, please contact system administrator"
+msgstr "企業微信錯誤,請聯絡系統管理員"
+
+#: common/sdk/sms/alibaba.py:56
+msgid "Signature does not match"
+msgstr "簽名不匹配"
+
+#: common/sdk/sms/cmpp2.py:44
+msgid "sp_id is 6 bits"
+msgstr "SP_id 為6位"
+
+#: common/sdk/sms/cmpp2.py:214
+msgid "Failed to connect to the CMPP gateway server, err: {}"
+msgstr "連接網關伺服器錯誤,錯誤:{}"
+
+#: common/sdk/sms/custom_file.py:41
+msgid "The custom sms file is invalid"
+msgstr "自訂簡訊文件無效"
+
+#: common/sdk/sms/custom_file.py:47
+#, python-format
+msgid "SMS sending failed[%s]: %s"
+msgstr "簡訊發送失敗[%s]: %s"
+
+#: common/sdk/sms/endpoint.py:16
+msgid "Alibaba cloud"
+msgstr "阿里雲"
+
+#: common/sdk/sms/endpoint.py:17
+msgid "Tencent cloud"
+msgstr "騰訊雲"
+
+#: common/sdk/sms/endpoint.py:18 xpack/plugins/cloud/const.py:13
+msgid "Huawei Cloud"
+msgstr "華為雲"
+
+#: common/sdk/sms/endpoint.py:19
+msgid "CMPP v2.0"
+msgstr "CMPP v2.0"
+
+#: common/sdk/sms/endpoint.py:21
+msgid "Custom type (File)"
+msgstr "自訂 (文件)"
+
+#: common/sdk/sms/endpoint.py:32
+msgid "SMS provider not support: {}"
+msgstr "簡訊服務商不支持:{}"
+
+#: common/sdk/sms/endpoint.py:54
+msgid "SMS verification code signature or template invalid"
+msgstr "簡訊驗證碼簽名或模板無效"
+
+#: common/sdk/sms/exceptions.py:8
+msgid "The verification code has expired. Please resend it"
+msgstr "驗證碼已過期,請重新發送"
+
+#: common/sdk/sms/exceptions.py:13
+msgid "The verification code is incorrect"
+msgstr "驗證碼錯誤"
+
+#: common/sdk/sms/exceptions.py:18
+msgid "Please wait {} seconds before sending"
+msgstr "請在 {} 秒後發送"
+
+#: common/serializers/common.py:90
+msgid "Children"
+msgstr "節點"
+
+#: common/serializers/fields.py:134
+#, python-brace-format
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "錯誤的 pk \"{pk_value}\" - 對象不存在"
+
+#: common/serializers/fields.py:135
+#, python-brace-format
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "錯誤類型。期望 pk 值,收到 {data_type}。"
+
+#: common/serializers/fields.py:209
+msgid "Invalid data type, should be list"
+msgstr "錯誤的數據類型,應該是列表"
+
+#: common/serializers/fields.py:224
+msgid "Invalid choice: {}"
+msgstr "無效選項: {}"
+
+#: common/serializers/mixin.py:397 labels/apps.py:8
+msgid "Labels"
+msgstr "標籤管理"
+
+# msgid "Labels"
+# msgstr "標籤管理"
+#: common/tasks.py:31 common/utils/verify_code.py:16
+msgid "Send email"
+msgstr "發件郵件"
+
+#: common/tasks.py:58
+msgid "Send email attachment"
+msgstr "發送郵件附件"
+
+#: common/tasks.py:80 terminal/tasks.py:58
+msgid "Upload session replay to external storage"
+msgstr "上傳會話錄影到外部儲存"
+
+#: common/utils/ip/geoip/utils.py:26
+msgid "Invalid ip"
+msgstr "無效 IP"
+
+#: common/utils/ip/utils.py:98
+msgid "Invalid address"
+msgstr "無效地址"
+
+#: common/utils/translate.py:45
+#, python-format
+msgid "Hello %s"
+msgstr "你好 %s"
+
+#: common/validators.py:16
+msgid "Special char not allowed"
+msgstr "不能包含特殊字元"
+
+#: common/validators.py:42
+msgid "Should not contains special characters"
+msgstr "不能包含特殊字元"
+
+#: common/validators.py:47
+msgid "The mobile phone number format is incorrect"
+msgstr "手機號碼格式不正確"
+
+#: jumpserver/conf.py:459
+#, python-brace-format
+msgid "The verification code is: {code}"
+msgstr "驗證碼為: {code}"
+
+#: jumpserver/conf.py:464
+msgid "Create account successfully"
+msgstr "創建帳號成功"
+
+#: jumpserver/conf.py:466
+msgid "Your account has been created successfully"
+msgstr "你的帳號已創建成功"
+
+#: jumpserver/context_processor.py:14
+msgid "JumpServer Open Source Bastion Host"
+msgstr "JumpServer 開源堡壘機"
+
+#: jumpserver/views/celery_flower.py:22
+msgid "Flower service unavailable, check it
"
+msgstr "Flower 服務不可用,請檢查"
+
+#: jumpserver/views/other.py:26
+msgid ""
+"Luna is a separately deployed program, you need to deploy Luna, koko, "
+"configure nginx for url distribution,
If you see this page, "
+"prove that you are not accessing the nginx listening port. Good luck."
+msgstr ""
+"Luna是單獨部署的一個程序,你需要部署luna,koko,
如果你看到了"
+"這個頁面,證明你訪問的不是nginx監聽的埠,祝你好運
"
+
+#: jumpserver/views/other.py:70
+msgid "Websocket server run on port: {}, you should proxy it on nginx"
+msgstr "Websocket 服務運行在埠: {}, 請檢查nginx是否代理是否設置"
+
+#: jumpserver/views/other.py:84
+msgid ""
+"Koko is a separately deployed program, you need to deploy Koko, "
+"configure nginx for url distribution,
If you see this page, "
+"prove that you are not accessing the nginx listening port. Good luck."
+msgstr ""
+"Koko是單獨部署的一個程序,你需要部署Koko, 並確保nginx配置轉發, "
+"div>
如果你看到了這個頁面,證明你訪問的不是nginx監聽的埠,祝你好運
"
+
+#: labels/models.py:36
+msgid "Resource ID"
+msgstr "資源 ID"
+
+#: labels/models.py:41
+msgid "Labeled resource"
+msgstr "關聯的資源"
+
+#: labels/serializers.py:22
+msgid "Resource count"
+msgstr "資源數量"
+
+#: labels/serializers.py:28
+msgid "Cannot contain \":,\""
+msgstr "不能包含\":,\""
+
+#: labels/serializers.py:43
+msgid "Resource type"
+msgstr "資源類型"
+
+#: notifications/backends/__init__.py:13
+msgid "Site message"
+msgstr "站內信"
+
+#: notifications/models/notification.py:14
+msgid "receive backend"
+msgstr "消息後端"
+
+#: notifications/models/notification.py:18
+msgid "User message"
+msgstr "用戶消息"
+
+#: notifications/models/notification.py:21
+msgid "{} subscription"
+msgstr "{} 訂閱"
+
+#: notifications/models/notification.py:34
+msgid "System message"
+msgstr "系統資訊"
+
+#: notifications/notifications.py:46
+msgid "Publish the station message"
+msgstr "發布站內消息"
+
+#: ops/ansible/inventory.py:106 ops/models/job.py:63
+msgid "No account available"
+msgstr "無可用帳號"
+
+#: ops/ansible/inventory.py:280
+msgid "Ansible disabled"
+msgstr "Ansible 已禁用"
+
+#: ops/ansible/inventory.py:296
+msgid "Skip hosts below:"
+msgstr "跳過以下主機: "
+
+#: ops/api/celery.py:66 ops/api/celery.py:81
+msgid "Waiting task start"
+msgstr "等待任務開始"
+
+#: ops/api/celery.py:262
+msgid "Task {} not found"
+msgstr "任務 {} 不存在"
+
+#: ops/api/celery.py:267
+msgid "Task {} args or kwargs error"
+msgstr "任務 {} 執行參數錯誤"
+
+#: ops/api/job.py:81
+#, python-brace-format
+msgid ""
+"Asset ({asset}) must have at least one of the following protocols added: "
+"SSH, SFTP, or WinRM"
+msgstr "资产({asset})至少要添加ssh,sftp,winrm其中一种协议"
+
+#: ops/api/job.py:82
+#, python-brace-format
+msgid "Asset ({asset}) authorization is missing SSH, SFTP, or WinRM protocol"
+msgstr "资产({asset})授权缺少ssh,sftp或winrm协议"
+
+#: ops/api/job.py:83
+#, python-brace-format
+msgid "Asset ({asset}) authorization lacks upload permissions"
+msgstr "资产({asset})授权缺少上传权限"
+
+#: ops/api/job.py:168
+msgid "Duplicate file exists"
+msgstr "存在同名文件"
+
+#: ops/api/job.py:173
+#, python-brace-format
+msgid ""
+"File size exceeds maximum limit. Please select a file smaller than {limit}MB"
+msgstr "檔案大小超過最大限制。請選擇小於 {limit}MB 的文件。"
+
+#: ops/api/job.py:237
+msgid ""
+"The task is being created and cannot be interrupted. Please try again later."
+msgstr "正在創建任務,無法中斷,請稍後重試。"
+
+#: ops/api/playbook.py:39
+msgid "Currently playbook is being used in a job"
+msgstr "當前 playbook 正在作業中使用"
+
+#: ops/api/playbook.py:96
+msgid "Unsupported file content"
+msgstr "不支持的文件內容"
+
+#: ops/api/playbook.py:98 ops/api/playbook.py:144 ops/api/playbook.py:192
+msgid "Invalid file path"
+msgstr "無效的文件路徑"
+
+#: ops/api/playbook.py:170
+msgid "This file can not be rename"
+msgstr "該文件不能重命名"
+
+#: ops/api/playbook.py:189
+msgid "File already exists"
+msgstr "文件已存在"
+
+#: ops/api/playbook.py:207
+msgid "File key is required"
+msgstr "文件金鑰該欄位是必填項。"
+
+#: ops/api/playbook.py:210
+msgid "This file can not be delete"
+msgstr "無法刪除此文件"
+
+#: ops/apps.py:9 ops/notifications.py:18 rbac/tree.py:57
+msgid "App ops"
+msgstr "作業中心"
+
+#: ops/const.py:6
+msgid "Push"
+msgstr "推送"
+
+#: ops/const.py:7
+msgid "Verify"
+msgstr "校驗"
+
+#: ops/const.py:8
+msgid "Collect"
+msgstr "收集"
+
+#: ops/const.py:19
+msgid "Custom password"
+msgstr "自訂密碼"
+
+#: ops/const.py:20
+msgid "All assets use the same random password"
+msgstr "隨機一個"
+
+#: ops/const.py:21
+msgid "All assets use different random password"
+msgstr "各自隨機"
+
+#: ops/const.py:33
+msgid "Blank"
+msgstr "空白"
+
+#: ops/const.py:34
+msgid "VCS"
+msgstr "VCS"
+
+#: ops/const.py:38 ops/models/adhoc.py:44
+msgid "Adhoc"
+msgstr "命令"
+
+#: ops/const.py:39 ops/models/job.py:145
+msgid "Playbook"
+msgstr "Playbook"
+
+#: ops/const.py:40
+msgid "Upload File"
+msgstr "上傳"
+
+#: ops/const.py:44
+msgid "Privileged Only"
+msgstr "僅限特權帳號"
+
+#: ops/const.py:45
+msgid "Privileged First"
+msgstr "特權帳號優先"
+
+#: ops/const.py:51 ops/const.py:62
+msgid "Powershell"
+msgstr "PowerShell"
+
+#: ops/const.py:52 ops/const.py:63
+msgid "Python"
+msgstr "Python"
+
+#: ops/const.py:53 ops/const.py:64
+msgid "MySQL"
+msgstr "MySQL"
+
+#: ops/const.py:54 ops/const.py:66
+msgid "PostgreSQL"
+msgstr "PostgreSQL"
+
+#: ops/const.py:55 ops/const.py:67
+msgid "SQLServer"
+msgstr "SQLServer"
+
+#: ops/const.py:56 ops/const.py:69
+msgid "Raw"
+msgstr "Raw"
+
+#: ops/const.py:57
+msgid "HUAWEI"
+msgstr ""
+
+#: ops/const.py:65
+msgid "MariaDB"
+msgstr "MariaDB"
+
+#: ops/const.py:68
+msgid "Oracle"
+msgstr "Oracle"
+
+#: ops/const.py:75
+msgid "Timeout"
+msgstr "超時"
+
+#: ops/exception.py:6
+msgid "no valid program entry found."
+msgstr "沒有可用程序入口"
+
+#: ops/mixin.py:26 ops/mixin.py:90 settings/serializers/auth/ldap.py:73
+msgid "Cycle perform"
+msgstr "週期執行"
+
+#: ops/mixin.py:30 ops/mixin.py:88 ops/mixin.py:107
+#: settings/serializers/auth/ldap.py:70
+msgid "Regularly perform"
+msgstr "定期執行"
+
+#: ops/mixin.py:110
+msgid "Interval"
+msgstr "間隔"
+
+#: ops/mixin.py:120
+msgid "* Please enter a valid crontab expression"
+msgstr "* 請輸入有效的 crontab 表達式"
+
+#: ops/mixin.py:127
+msgid "Range {} to {}"
+msgstr "輸入在 {} - {} 範圍之間"
+
+#: ops/mixin.py:138
+msgid "Require periodic or regularly perform setting"
+msgstr "需要週期或定期設置"
+
+#: ops/models/adhoc.py:21
+msgid "Pattern"
+msgstr "模式"
+
+#: ops/models/adhoc.py:23 ops/models/job.py:142
+msgid "Module"
+msgstr "模組"
+
+#: ops/models/adhoc.py:24 ops/models/celery.py:81 ops/models/job.py:140
+#: terminal/models/component/task.py:14
+msgid "Args"
+msgstr "參數"
+
+# msgid "Creator"
+# msgstr "創建者"
+#: ops/models/base.py:19
+msgid "Account policy"
+msgstr "帳號策略"
+
+#: ops/models/base.py:20
+msgid "Last execution"
+msgstr "最後執行"
+
+#: ops/models/base.py:22 ops/serializers/job.py:17
+msgid "Date last run"
+msgstr "最後運行日期"
+
+#: ops/models/base.py:51 ops/models/job.py:233
+#: xpack/plugins/cloud/models.py:198
+msgid "Result"
+msgstr "結果"
+
+#: ops/models/base.py:52 ops/models/job.py:234
+msgid "Summary"
+msgstr "匯總"
+
+#: ops/models/celery.py:16
+msgid "Date last publish"
+msgstr "發布日期"
+
+#: ops/models/celery.py:70
+msgid "Celery Task"
+msgstr "Celery 任務"
+
+#: ops/models/celery.py:73
+msgid "Can view task monitor"
+msgstr "可以查看任務監控"
+
+#: ops/models/celery.py:82 terminal/models/component/task.py:15
+msgid "Kwargs"
+msgstr "其它參數"
+
+#: ops/models/celery.py:84 terminal/models/session/sharing.py:128
+#: tickets/const.py:25
+msgid "Finished"
+msgstr "結束"
+
+#: ops/models/celery.py:87
+msgid "Date published"
+msgstr "發布日期"
+
+#: ops/models/celery.py:112
+msgid "Celery Task Execution"
+msgstr "Celery 任務執行"
+
+#: ops/models/job.py:143
+msgid "Chdir"
+msgstr "運行目錄"
+
+#: ops/models/job.py:144
+msgid "Timeout (Seconds)"
+msgstr "超時時間 (秒)"
+
+#: ops/models/job.py:149
+msgid "Use Parameter Define"
+msgstr "使用參數定義"
+
+#: ops/models/job.py:150
+msgid "Parameters define"
+msgstr "參數定義"
+
+#: ops/models/job.py:151
+msgid "Runas"
+msgstr "運行用戶"
+
+#: ops/models/job.py:153
+msgid "Runas policy"
+msgstr "用戶策略"
+
+#: ops/models/job.py:217
+msgid "Job"
+msgstr "作業"
+
+#: ops/models/job.py:240
+msgid "Material"
+msgstr "Material"
+
+#: ops/models/job.py:242
+msgid "Material Type"
+msgstr "Material 類型"
+
+#: ops/models/job.py:539
+msgid "Job Execution"
+msgstr "作業執行"
+
+#: ops/models/playbook.py:33
+msgid "CreateMethod"
+msgstr "創建方式"
+
+#: ops/models/playbook.py:34
+msgid "VCS URL"
+msgstr "VCS URL"
+
+#: ops/notifications.py:19
+msgid "Server performance"
+msgstr "監控告警"
+
+#: ops/notifications.py:25
+msgid "Terminal health check warning"
+msgstr "終端健康狀況檢查警告"
+
+#: ops/notifications.py:70
+#, python-brace-format
+msgid "The terminal is offline: {name}"
+msgstr "終端已離線: {name}"
+
+#: ops/notifications.py:75
+#, python-brace-format
+msgid "Disk used more than {max_threshold}%: => {value}"
+msgstr "硬碟使用率超過 {max_threshold}%: => {value}"
+
+#: ops/notifications.py:80
+#, python-brace-format
+msgid "Memory used more than {max_threshold}%: => {value}"
+msgstr "記憶體使用率超過 {max_threshold}%: => {value}"
+
+#: ops/notifications.py:85
+#, python-brace-format
+msgid "CPU load more than {max_threshold}: => {value}"
+msgstr "CPU 使用率超過 {max_threshold}: => {value}"
+
+#: ops/serializers/job.py:15
+msgid "Run after save"
+msgstr "保存後執行"
+
+#: ops/serializers/job.py:69
+msgid "Job type"
+msgstr "任務類型"
+
+#: ops/serializers/job.py:72 terminal/serializers/session.py:56
+msgid "Is finished"
+msgstr "是否完成"
+
+#: ops/serializers/job.py:73
+#: settings/templates/ldap/_msg_import_ldap_user.html:7
+msgid "Time cost"
+msgstr "花費時間"
+
+#: ops/serializers/job.py:87
+msgid "You do not have permission for the current job."
+msgstr "你沒有當前作業的權限。"
+
+#: ops/tasks.py:38
+msgid "Run ansible task"
+msgstr "運行 Ansible 任務"
+
+#: ops/tasks.py:72
+msgid "Run ansible task execution"
+msgstr "開始執行 Ansible 任務"
+
+#: ops/tasks.py:94
+msgid "Clear celery periodic tasks"
+msgstr "清理週期任務"
+
+#: ops/tasks.py:115
+msgid "Create or update periodic tasks"
+msgstr "創建或更新週期任務"
+
+#: ops/tasks.py:123
+msgid "Periodic check service performance"
+msgstr "週期檢測服務性能"
+
+#: ops/tasks.py:129
+msgid "Clean up unexpected jobs"
+msgstr "清理異常作業"
+
+#: ops/tasks.py:136
+msgid "Clean job_execution db record"
+msgstr "清理作業中心執行歷史"
+
+#: ops/templates/ops/celery_task_log.html:4
+msgid "Task log"
+msgstr "任務列表"
+
+#: ops/variables.py:24
+msgid "The current user`s username of JumpServer"
+msgstr "JumpServer 當前用戶的使用者名稱"
+
+#: ops/variables.py:25
+msgid "The id of the asset in the JumpServer"
+msgstr "JumpServer 中資產的 id"
+
+#: ops/variables.py:26
+msgid "The type of the asset in the JumpServer"
+msgstr "JumpServer 中資產的類型"
+
+#: ops/variables.py:27
+msgid "The category of the asset in the JumpServer"
+msgstr "JumpServer 中資產的類別"
+
+#: ops/variables.py:28
+msgid "The name of the asset in the JumpServer"
+msgstr "JumpServer 中資產的名稱"
+
+#: ops/variables.py:29
+msgid "Address used to connect this asset in JumpServer"
+msgstr "在 JumpServer 中用於連接此資產的地址"
+
+#: ops/variables.py:30
+msgid "Port used to connect this asset in JumpServer"
+msgstr "在 JumpServer 中用於連接此資產的埠"
+
+#: ops/variables.py:31
+msgid "ID of the job"
+msgstr "Job ID"
+
+#: ops/variables.py:32
+msgid "Name of the job"
+msgstr "Job 名稱"
+
+#: orgs/api.py:61
+msgid "The current organization ({}) cannot be deleted"
+msgstr "當前組織 ({}) 不能被刪除"
+
+#: orgs/api.py:66
+msgid ""
+"LDAP synchronization is set to the current organization. Please switch to "
+"another organization before deleting"
+msgstr "LDAP 同步設定組織為當前組織,請切換其他組織後再進行刪除操作"
+
+#: orgs/api.py:76
+msgid "The organization have resource ({}) cannot be deleted"
+msgstr "組織存在資源 ({}) 不能被刪除"
+
+#: orgs/apps.py:7 rbac/tree.py:128
+msgid "App organizations"
+msgstr "組織管理"
+
+#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:91
+#: rbac/const.py:7 rbac/models/rolebinding.py:56
+#: rbac/serializers/rolebinding.py:44 settings/serializers/auth/ldap.py:63
+#: terminal/templates/terminal/_msg_command_warning.html:21
+#: terminal/templates/terminal/_msg_session_sharing.html:14
+#: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:60
+msgid "Organization"
+msgstr "組織"
+
+#: orgs/mixins/serializers.py:26 rbac/serializers/rolebinding.py:27
+msgid "Org name"
+msgstr "組織名稱"
+
+#: orgs/models.py:14
+msgid "GLOBAL"
+msgstr "全局組織"
+
+#: orgs/models.py:16
+msgid "DEFAULT"
+msgstr "默認組織"
+
+#: orgs/models.py:18
+msgid "SYSTEM"
+msgstr "系統組織"
+
+#: orgs/models.py:83 rbac/models/role.py:36 settings/models.py:183
+#: terminal/models/applet/applet.py:41
+msgid "Builtin"
+msgstr "內建的"
+
+#: orgs/models.py:93
+msgid "Can view root org"
+msgstr "可以查看全局組織"
+
+#: orgs/models.py:94
+msgid "Can view all joined org"
+msgstr "可以查看所有加入的組織"
+
+#: orgs/models.py:236
+msgid "Can not delete virtual org"
+msgstr "無法刪除虛擬組織"
+
+#: orgs/tasks.py:9
+msgid "Refresh organization cache"
+msgstr "刷新組織快取"
+
+#: perms/apps.py:9
+msgid "App permissions"
+msgstr "授權管理"
+
+#: perms/const.py:12
+msgid "Connect (All protocols)"
+msgstr "連接 (所有協議)"
+
+#: perms/const.py:13
+msgid "Upload (RDP, SFTP)"
+msgstr "上傳 (RDP, SFTP)"
+
+#: perms/const.py:14
+msgid "Download (RDP, SFTP)"
+msgstr "下載 (RDP, SFTP)"
+
+#: perms/const.py:15
+msgid "Copy (RDP, VNC)"
+msgstr "複製 (RDP, VNC)"
+
+#: perms/const.py:16
+msgid "Paste (RDP, VNC)"
+msgstr "黏貼 (RDP, VNC)"
+
+#: perms/const.py:17
+msgid "Delete (SFTP)"
+msgstr "刪除 (SFTP)"
+
+#: perms/const.py:18
+msgid "Share (SSH)"
+msgstr "分享 (SSH)"
+
+#: perms/const.py:28
+msgid "Transfer"
+msgstr "文件傳輸"
+
+#: perms/const.py:29
+msgid "Clipboard"
+msgstr "剪貼板"
+
+#: perms/models/asset_permission.py:89
+msgid "Asset permission"
+msgstr "資產授權"
+
+#: perms/models/perm_node.py:68
+msgid "Ungrouped"
+msgstr "未分組"
+
+#: perms/models/perm_node.py:70
+msgid "Favorite"
+msgstr "收藏夾"
+
+#: perms/models/perm_node.py:121
+msgid "Permed asset"
+msgstr "授權的資產"
+
+#: perms/models/perm_node.py:123
+msgid "Can view my assets"
+msgstr "可以查看我的資產"
+
+#: perms/models/perm_node.py:124
+msgid "Can view user assets"
+msgstr "可以查看用戶授權的資產"
+
+#: perms/models/perm_node.py:125
+msgid "Can view usergroup assets"
+msgstr "可以查看用戶組授權的資產"
+
+#: perms/models/perm_node.py:136
+msgid "Permed account"
+msgstr "授權帳號"
+
+#: perms/notifications.py:12 perms/notifications.py:44
+msgid "today"
+msgstr "今天"
+
+#: perms/notifications.py:12 perms/notifications.py:44
+#: settings/serializers/feature.py:118
+msgid "day"
+msgstr "天"
+
+#: perms/notifications.py:15
+msgid "You permed assets is about to expire"
+msgstr "你授權的資產即將到期"
+
+#: perms/notifications.py:20
+msgid "permed assets"
+msgstr "授權的資產"
+
+#: perms/notifications.py:59
+msgid "Asset permissions is about to expire"
+msgstr "資產授權規則將要過期"
+
+#: perms/notifications.py:64
+msgid "asset permissions of organization {}"
+msgstr "組織 ({}) 的資產授權"
+
+#: perms/serializers/permission.py:186 rbac/serializers/role.py:27
+#: users/serializers/group.py:54 users/serializers/group.py:60
+msgid "Users amount"
+msgstr "用戶數量"
+
+#: perms/serializers/permission.py:187
+msgid "User groups amount"
+msgstr "用戶組數量"
+
+#: perms/serializers/permission.py:189
+msgid "Nodes amount"
+msgstr "節點數量"
+
+#: perms/tasks.py:27
+msgid "Check asset permission expired"
+msgstr "校驗資產授權規則已過期"
+
+#: perms/tasks.py:40
+msgid "Send asset permission expired notification"
+msgstr "發送資產權限過期通知"
+
+#: perms/templates/perms/_msg_item_permissions_expire.html:7
+#: perms/templates/perms/_msg_permed_items_expire.html:7
+#, python-format
+msgid ""
+"\n"
+" The following %(item_type)s will expire in %(count)s\n"
+" "
+msgstr ""
+"\n"
+" 以下 %(item_type)s 即將在 %(count)s 後過期\n"
+" "
+
+#: rbac/api/role.py:35
+msgid "Internal role, can't be destroy"
+msgstr "內部角色,不能刪除"
+
+#: rbac/api/role.py:40
+msgid "The role has been bound to users, can't be destroy"
+msgstr "角色已綁定用戶,不能刪除"
+
+#: rbac/api/role.py:105
+msgid "Internal role, can't be update"
+msgstr "內部角色,不能更新"
+
+#: rbac/api/rolebinding.py:45
+msgid "{} at least one system role"
+msgstr "{} 至少有一個系統角色"
+
+#: rbac/apps.py:7
+msgid "RBAC"
+msgstr "RBAC"
+
+#: rbac/builtin.py:115
+msgid "SystemAdmin"
+msgstr "系統管理員"
+
+#: rbac/builtin.py:118
+msgid "SystemAuditor"
+msgstr "系統審計員"
+
+#: rbac/builtin.py:121
+msgid "SystemComponent"
+msgstr "系統組件"
+
+#: rbac/builtin.py:127
+msgid "OrgAdmin"
+msgstr "組織管理員"
+
+#: rbac/builtin.py:130
+msgid "OrgAuditor"
+msgstr "組織審計員"
+
+#: rbac/builtin.py:133
+msgid "OrgUser"
+msgstr "組織用戶"
+
+#: rbac/models/menu.py:13
+msgid "Menu permission"
+msgstr "菜單授權"
+
+#: rbac/models/menu.py:15
+msgid "Can view console view"
+msgstr "可以顯示控制台"
+
+#: rbac/models/menu.py:16
+msgid "Can view audit view"
+msgstr "可以顯示審計台"
+
+#: rbac/models/menu.py:17
+msgid "Can view workbench view"
+msgstr "可以顯示工作檯"
+
+#: rbac/models/menu.py:18
+msgid "Can view web terminal"
+msgstr "Web終端"
+
+#: rbac/models/menu.py:19
+msgid "Can view file manager"
+msgstr "文件管理"
+
+#: rbac/models/menu.py:20
+msgid "Can view System Tools"
+msgstr "可以查看系統工具"
+
+#: rbac/models/permission.py:78 rbac/models/role.py:34
+msgid "Permissions"
+msgstr "授權"
+
+#: rbac/models/role.py:31 rbac/models/rolebinding.py:46
+#: rbac/serializers/role.py:12 settings/serializers/auth/oauth2.py:36
+msgid "Scope"
+msgstr "範圍"
+
+#: rbac/models/role.py:46 rbac/models/rolebinding.py:52
+#: users/models/user.py:824
+msgid "Role"
+msgstr "角色"
+
+#: rbac/models/role.py:144
+msgid "System role"
+msgstr "系統角色"
+
+#: rbac/models/role.py:152
+msgid "Organization role"
+msgstr "組織角色"
+
+#: rbac/models/rolebinding.py:62
+msgid "Role binding"
+msgstr "角色綁定"
+
+#: rbac/models/rolebinding.py:161
+msgid "All organizations"
+msgstr "所有組織"
+
+#: rbac/models/rolebinding.py:193
+msgid ""
+"User last role in org, can not be delete, you can remove user from org "
+"instead"
+msgstr "用戶最後一個角色,不能刪除,你可以將用戶從組織移除"
+
+#: rbac/models/rolebinding.py:200
+msgid "Organization role binding"
+msgstr "組織角色綁定"
+
+#: rbac/models/rolebinding.py:215
+msgid "System role binding"
+msgstr "系統角色綁定"
+
+#: rbac/serializers/permission.py:25 users/serializers/profile.py:108
+msgid "Perms"
+msgstr "權限"
+
+#: rbac/serializers/role.py:28 terminal/models/applet/applet.py:34
+#: terminal/models/virtualapp/virtualapp.py:20
+msgid "Display name"
+msgstr "顯示名稱"
+
+#: rbac/serializers/rolebinding.py:60
+msgid "Has bound this role"
+msgstr "已經綁定"
+
+#: rbac/tree.py:17 rbac/tree.py:18
+msgid "All permissions"
+msgstr "所有權限"
+
+#: rbac/tree.py:24
+msgid "Console view"
+msgstr "控制台"
+
+#: rbac/tree.py:25
+msgid "Workbench view"
+msgstr "工作檯"
+
+#: rbac/tree.py:26
+msgid "Audit view"
+msgstr "審計台"
+
+#: rbac/tree.py:27 settings/models.py:159
+msgid "System setting"
+msgstr "系統設置"
+
+#: rbac/tree.py:37
+msgid "Session audits"
+msgstr "會話審計"
+
+#: rbac/tree.py:49
+msgid "Cloud import"
+msgstr "雲同步"
+
+#: rbac/tree.py:50
+msgid "Backup account"
+msgstr "備份帳號"
+
+#: rbac/tree.py:51
+msgid "Gather account"
+msgstr "收集帳號"
+
+#: rbac/tree.py:53
+msgid "Asset change auth"
+msgstr "資產改密"
+
+#: rbac/tree.py:54
+msgid "Terminal setting"
+msgstr "終端設置"
+
+#: rbac/tree.py:55
+msgid "Task Center"
+msgstr "任務中心"
+
+#: rbac/tree.py:56
+msgid "My assets"
+msgstr "我的資產"
+
+#: rbac/tree.py:58 terminal/models/applet/applet.py:52
+#: terminal/models/applet/applet.py:317 terminal/models/applet/host.py:30
+#: terminal/serializers/applet.py:15
+msgid "Applet"
+msgstr "遠程應用"
+
+#: rbac/tree.py:129
+msgid "Ticket comment"
+msgstr "工單評論"
+
+#: rbac/tree.py:130 settings/serializers/feature.py:109
+#: tickets/models/ticket/general.py:305
+msgid "Ticket"
+msgstr "工單管理"
+
+#: rbac/tree.py:131
+msgid "Common setting"
+msgstr "一般設定"
+
+#: rbac/tree.py:132
+msgid "View permission tree"
+msgstr "查看授權樹"
+
+#: settings/api/chat.py:40
+msgid "Chat AI is not enabled"
+msgstr "聊天 AI 沒有開啟"
+
+#: settings/api/chat.py:79 settings/api/dingtalk.py:31
+#: settings/api/feishu.py:36 settings/api/slack.py:34 settings/api/sms.py:160
+#: settings/api/vault.py:40 settings/api/wecom.py:37
+msgid "Test success"
+msgstr "測試成功"
+
+#: settings/api/email.py:22
+msgid "Test mail sent to {}, please check"
+msgstr "郵件已經發送{}, 請檢查"
+
+#: settings/api/ldap.py:89
+msgid ""
+"Users are not synchronized, please click the user synchronization button"
+msgstr "用戶未同步,請點擊同步用戶按鈕"
+
+#: settings/api/sms.py:142
+msgid "Invalid SMS platform"
+msgstr "無效的簡訊平台"
+
+#: settings/api/sms.py:148
+msgid "test_phone is required"
+msgstr "測試手機號碼 該欄位是必填項。"
+
+#: settings/apps.py:7
+msgid "Settings"
+msgstr "系統設置"
+
+#: settings/models.py:36 users/models/preference.py:14
+msgid "Encrypted"
+msgstr "加密的"
+
+#: settings/models.py:161
+msgid "Can change email setting"
+msgstr "郵件設置"
+
+#: settings/models.py:162
+msgid "Can change auth setting"
+msgstr "認證設置"
+
+#: settings/models.py:163
+msgid "Can change auth ops"
+msgstr "任務中心設置"
+
+#: settings/models.py:164
+msgid "Can change auth ticket"
+msgstr "工單設置"
+
+#: settings/models.py:165
+msgid "Can change virtual app setting"
+msgstr "可以更改虛擬應用設定"
+
+#: settings/models.py:166
+msgid "Can change auth announcement"
+msgstr "公告設置"
+
+#: settings/models.py:167
+msgid "Can change vault setting"
+msgstr "可以更改 vault 設置"
+
+#: settings/models.py:168
+msgid "Can change chat ai setting"
+msgstr "可以修改聊天 AI 設置"
+
+#: settings/models.py:169
+msgid "Can change system msg sub setting"
+msgstr "消息訂閱設置"
+
+#: settings/models.py:170
+msgid "Can change sms setting"
+msgstr "簡訊設置"
+
+#: settings/models.py:171
+msgid "Can change security setting"
+msgstr "安全設定"
+
+#: settings/models.py:172
+msgid "Can change clean setting"
+msgstr "定期清理"
+
+#: settings/models.py:173
+msgid "Can change interface setting"
+msgstr "界面設置"
+
+#: settings/models.py:174
+msgid "Can change license setting"
+msgstr "許可證設置"
+
+#: settings/models.py:175
+msgid "Can change terminal setting"
+msgstr "終端設置"
+
+#: settings/models.py:176
+msgid "Can change other setting"
+msgstr "其它設置"
+
+#: settings/models.py:186
+msgid "Chat prompt"
+msgstr "聊天提示"
+
+#: settings/notifications.py:23
+msgid "Notification of Synchronized LDAP User Task Results"
+msgstr "同步LDAP用戶任務結果通知"
+
+#: settings/serializers/auth/base.py:12
+msgid "LDAP Auth"
+msgstr "LDAP 認證"
+
+#: settings/serializers/auth/base.py:13
+msgid "CAS Auth"
+msgstr "CAS 認證"
+
+#: settings/serializers/auth/base.py:14
+msgid "OPENID Auth"
+msgstr "OIDC 認證"
+
+#: settings/serializers/auth/base.py:15
+msgid "SAML2 Auth"
+msgstr "SAML2 認證"
+
+#: settings/serializers/auth/base.py:16
+msgid "OAuth2 Auth"
+msgstr "OAuth2 認證"
+
+#: settings/serializers/auth/base.py:17
+msgid "RADIUS Auth"
+msgstr "RADIUS 認證"
+
+#: settings/serializers/auth/base.py:18
+msgid "DingTalk Auth"
+msgstr "釘釘 認證"
+
+#: settings/serializers/auth/base.py:19
+msgid "FeiShu Auth"
+msgstr "飛書 認證"
+
+#: settings/serializers/auth/base.py:20
+msgid "Lark Auth"
+msgstr "Lark 認證"
+
+#: settings/serializers/auth/base.py:21
+msgid "Slack Auth"
+msgstr "Slack 認證"
+
+#: settings/serializers/auth/base.py:22
+msgid "WeCom Auth"
+msgstr "企業微信 認證"
+
+#: settings/serializers/auth/base.py:23
+msgid "SSO Auth"
+msgstr "SSO 令牌認證"
+
+#: settings/serializers/auth/base.py:24
+msgid "Passkey Auth"
+msgstr "Passkey 認證"
+
+#: settings/serializers/auth/base.py:27
+msgid "Forgot password url"
+msgstr "忘記密碼 URL"
+
+#: settings/serializers/auth/base.py:30
+msgid "Enable login redirect msg"
+msgstr "啟用登錄跳轉提示"
+
+#: settings/serializers/auth/cas.py:10
+msgid "CAS"
+msgstr "CAS"
+
+#: settings/serializers/auth/cas.py:12
+msgid "Enable CAS Auth"
+msgstr "啟用 CAS 認證"
+
+#: settings/serializers/auth/cas.py:13 settings/serializers/auth/oidc.py:54
+msgid "Server url"
+msgstr "服務端地址"
+
+#: settings/serializers/auth/cas.py:16
+msgid "Proxy server url"
+msgstr "回調地址"
+
+#: settings/serializers/auth/cas.py:18 settings/serializers/auth/oauth2.py:54
+#: settings/serializers/auth/saml2.py:33
+msgid "Logout completely"
+msgstr "同步註銷"
+
+#: settings/serializers/auth/cas.py:23
+msgid "Username attr"
+msgstr "使用者名稱屬性"
+
+#: settings/serializers/auth/cas.py:26
+msgid "Enable attributes map"
+msgstr "啟用屬性映射"
+
+#: settings/serializers/auth/cas.py:28 settings/serializers/auth/saml2.py:32
+msgid "Rename attr"
+msgstr "映射屬性"
+
+#: settings/serializers/auth/cas.py:29
+msgid "Create user if not"
+msgstr "創建用戶(如果不存在)"
+
+#: settings/serializers/auth/dingtalk.py:15
+msgid "Enable DingTalk Auth"
+msgstr "啟用釘釘認證"
+
+#: settings/serializers/auth/feishu.py:12
+msgid "Enable FeiShu Auth"
+msgstr "啟用飛書認證"
+
+#: settings/serializers/auth/lark.py:12
+msgid "Enable Lark Auth"
+msgstr "啟用 Lark 認證"
+
+#: settings/serializers/auth/ldap.py:39
+msgid "LDAP"
+msgstr "LDAP"
+
+#: settings/serializers/auth/ldap.py:42
+msgid "LDAP server"
+msgstr "LDAP 地址"
+
+#: settings/serializers/auth/ldap.py:43
+msgid "eg: ldap://localhost:389"
+msgstr "如: ldap://localhost:389"
+
+#: settings/serializers/auth/ldap.py:45
+msgid "Bind DN"
+msgstr "綁定 DN"
+
+#: settings/serializers/auth/ldap.py:50
+msgid "User OU"
+msgstr "用戶 OU"
+
+#: settings/serializers/auth/ldap.py:51
+msgid "Use | split multi OUs"
+msgstr "多個 OU 使用 | 分割"
+
+#: settings/serializers/auth/ldap.py:54
+msgid "User search filter"
+msgstr "用戶過濾器"
+
+#: settings/serializers/auth/ldap.py:55
+#, python-format
+msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)"
+msgstr "可能的選項是(cn或uid或sAMAccountName=%(user)s)"
+
+#: settings/serializers/auth/ldap.py:58 settings/serializers/auth/oauth2.py:56
+#: settings/serializers/auth/oidc.py:37
+msgid "User attr map"
+msgstr "用戶屬性映射"
+
+#: settings/serializers/auth/ldap.py:59
+msgid ""
+"User attr map present how to map LDAP user attr to jumpserver, username,name,"
+"email is jumpserver attr"
+msgstr ""
+"用戶屬性映射代表怎樣將LDAP中用戶屬性映射到jumpserver用戶上,username, name,"
+"email 是jumpserver的用戶需要屬性"
+
+#: settings/serializers/auth/ldap.py:77
+msgid "Connect timeout (s)"
+msgstr "連接超時時間 (秒)"
+
+#: settings/serializers/auth/ldap.py:82
+msgid "User DN cache timeout (s)"
+msgstr "快取逾時時間 (秒)"
+
+#: settings/serializers/auth/ldap.py:84
+msgid ""
+"Caching the User DN obtained during user login authentication can "
+"effectivelyimprove the speed of user authentication., 0 means no cache"
+msgstr ""
+"對於使用者登錄認證時查詢的使用者 DN 進行快取,可以有效提高使用者認證的速度"
+
+#: settings/serializers/auth/ldap.py:88
+msgid "Search paged size (piece)"
+msgstr "搜索分頁數量 (條)"
+
+#: settings/serializers/auth/ldap.py:93
+msgid "Enable LDAP auth"
+msgstr "啟用 LDAP 認證"
+
+#: settings/serializers/auth/oauth2.py:18
+msgid "OAuth2"
+msgstr "OAuth2"
+
+#: settings/serializers/auth/oauth2.py:21
+msgid "Enable OAuth2 Auth"
+msgstr "啟用 OAuth2 認證"
+
+#: settings/serializers/auth/oauth2.py:24
+msgid "Logo"
+msgstr "圖示"
+
+#: settings/serializers/auth/oauth2.py:27
+msgid "Service provider"
+msgstr "服務提供商"
+
+#: settings/serializers/auth/oauth2.py:30 settings/serializers/auth/oidc.py:19
+msgid "Client Id"
+msgstr "用戶端 ID"
+
+#: settings/serializers/auth/oauth2.py:33 settings/serializers/auth/oidc.py:22
+#: xpack/plugins/cloud/serializers/account_attrs.py:38
+msgid "Client Secret"
+msgstr "用戶端金鑰"
+
+#: settings/serializers/auth/oauth2.py:39 settings/serializers/auth/oidc.py:68
+msgid "Provider auth endpoint"
+msgstr "授權端點地址"
+
+#: settings/serializers/auth/oauth2.py:42 settings/serializers/auth/oidc.py:71
+msgid "Provider token endpoint"
+msgstr "token 端點地址"
+
+#: settings/serializers/auth/oauth2.py:45 settings/serializers/auth/oidc.py:30
+msgid "Client authentication method"
+msgstr "用戶端認證方式"
+
+#: settings/serializers/auth/oauth2.py:49 settings/serializers/auth/oidc.py:77
+msgid "Provider userinfo endpoint"
+msgstr "用戶資訊端點地址"
+
+#: settings/serializers/auth/oauth2.py:52 settings/serializers/auth/oidc.py:80
+msgid "Provider end session endpoint"
+msgstr "註銷會話端點地址"
+
+#: settings/serializers/auth/oauth2.py:59 settings/serializers/auth/oidc.py:98
+#: settings/serializers/auth/saml2.py:34
+msgid "Always update user"
+msgstr "總是更新用戶資訊"
+
+#: settings/serializers/auth/oidc.py:12
+msgid "OIDC"
+msgstr "OIDC"
+
+#: settings/serializers/auth/oidc.py:16
+msgid "Base site url"
+msgstr "JumpServer 地址"
+
+#: settings/serializers/auth/oidc.py:32
+msgid "Share session"
+msgstr "共享會話"
+
+#: settings/serializers/auth/oidc.py:34
+msgid "Ignore ssl verification"
+msgstr "忽略 SSL 證書驗證"
+
+#: settings/serializers/auth/oidc.py:38
+msgid ""
+"User attr map present how to map OpenID user attr to jumpserver, username,"
+"name,email is jumpserver attr"
+msgstr ""
+"用戶屬性映射代表怎樣將OpenID中用戶屬性映射到jumpserver用戶上,username, name,"
+"email 是jumpserver的用戶需要屬性"
+
+#: settings/serializers/auth/oidc.py:41
+msgid "Enable PKCE"
+msgstr "啟用 PKCE"
+
+#: settings/serializers/auth/oidc.py:43
+msgid "Code challenge method"
+msgstr "驗證校驗碼方式"
+
+#: settings/serializers/auth/oidc.py:51
+msgid "Use Keycloak"
+msgstr "使用 Keycloak"
+
+#: settings/serializers/auth/oidc.py:57
+msgid "Realm name"
+msgstr "域"
+
+#: settings/serializers/auth/oidc.py:63
+msgid "Enable OPENID Auth"
+msgstr "啟用 OIDC 認證"
+
+#: settings/serializers/auth/oidc.py:65
+msgid "Provider endpoint"
+msgstr "端點地址"
+
+#: settings/serializers/auth/oidc.py:74
+msgid "Provider jwks endpoint"
+msgstr "jwks 端點地址"
+
+#: settings/serializers/auth/oidc.py:83
+msgid "Provider sign alg"
+msgstr "簽名算法"
+
+#: settings/serializers/auth/oidc.py:86
+msgid "Provider sign key"
+msgstr "簽名 Key"
+
+#: settings/serializers/auth/oidc.py:88
+msgid "Scopes"
+msgstr "連接範圍"
+
+#: settings/serializers/auth/oidc.py:90
+msgid "Id token max age (s)"
+msgstr "令牌有效時間 (秒)"
+
+#: settings/serializers/auth/oidc.py:93
+msgid "Id token include claims"
+msgstr "聲明"
+
+#: settings/serializers/auth/oidc.py:95
+msgid "Use state"
+msgstr "使用狀態"
+
+#: settings/serializers/auth/oidc.py:96
+msgid "Use nonce"
+msgstr "臨時使用"
+
+#: settings/serializers/auth/passkey.py:11
+msgid "Enable passkey Auth"
+msgstr "啟用 Passkey 認證"
+
+#: settings/serializers/auth/passkey.py:12
+msgid "Only SSL domain can use passkey auth"
+msgstr "只有 SSL 域名可以使用 Passkey(通行金鑰)認證"
+
+#: settings/serializers/auth/passkey.py:15
+msgid "FIDO server ID"
+msgstr "Passkey 服務域名"
+
+#: settings/serializers/auth/passkey.py:17
+msgid ""
+"The hostname can using passkey auth, If not set, will use request host and "
+"the request host in DOMAINS, If multiple domains, use comma to separate"
+msgstr ""
+"可以使用 Passkey 認證的域名,如果不設置,將使用請求主機(主機名在可信域 "
+"DOMAINS中), 如果有多個域名,使用逗號分隔, 不需要埠號"
+
+#: settings/serializers/auth/passkey.py:22
+msgid "FIDO server name"
+msgstr "Passkey 服務名稱"
+
+#: settings/serializers/auth/radius.py:13
+msgid "Radius"
+msgstr "Radius"
+
+#: settings/serializers/auth/radius.py:15
+msgid "Enable Radius Auth"
+msgstr "啟用 Radius 認證"
+
+#: settings/serializers/auth/radius.py:21
+msgid "OTP in Radius"
+msgstr "使用 Radius OTP"
+
+#: settings/serializers/auth/saml2.py:10
+msgid "SAML2"
+msgstr "SAML2"
+
+#: settings/serializers/auth/saml2.py:13
+msgid "Enable SAML2 Auth"
+msgstr "啟用 SAML2 認證"
+
+#: settings/serializers/auth/saml2.py:16
+msgid "IDP metadata URL"
+msgstr "IDP metadata 地址"
+
+#: settings/serializers/auth/saml2.py:19
+msgid "IDP metadata XML"
+msgstr "IDP metadata XML"
+
+#: settings/serializers/auth/saml2.py:22
+msgid "SP advanced settings"
+msgstr "進階設定"
+
+#: settings/serializers/auth/saml2.py:26
+msgid "SP private key"
+msgstr "SP 金鑰"
+
+#: settings/serializers/auth/saml2.py:30
+msgid "SP cert"
+msgstr "SP 證書"
+
+#: settings/serializers/auth/slack.py:12
+msgid "Enable Slack Auth"
+msgstr "啟用 Slack 認證"
+
+#: settings/serializers/auth/sms.py:17
+msgid "Enable SMS"
+msgstr "啟用 SMS"
+
+#: settings/serializers/auth/sms.py:19
+msgid "SMS provider / Protocol"
+msgstr "簡訊服務商 / 協議"
+
+#: settings/serializers/auth/sms.py:22
+msgid "SMS code length"
+msgstr "驗證碼長度"
+
+#: settings/serializers/auth/sms.py:27 settings/serializers/auth/sms.py:49
+#: settings/serializers/auth/sms.py:57 settings/serializers/auth/sms.py:66
+#: settings/serializers/auth/sms.py:77 settings/serializers/msg.py:83
+msgid "Signature"
+msgstr "簽名"
+
+#: settings/serializers/auth/sms.py:28 settings/serializers/auth/sms.py:50
+#: settings/serializers/auth/sms.py:58 settings/serializers/auth/sms.py:67
+msgid "Template code"
+msgstr "模板"
+
+#: settings/serializers/auth/sms.py:35
+msgid "Test phone"
+msgstr "測試手機號碼"
+
+#: settings/serializers/auth/sms.py:64
+msgid "App Access Address"
+msgstr "應用地址"
+
+#: settings/serializers/auth/sms.py:65
+msgid "Signature channel number"
+msgstr "簽名通道號"
+
+#: settings/serializers/auth/sms.py:73
+msgid "Enterprise code(SP id)"
+msgstr "企業代碼(SP id)"
+
+#: settings/serializers/auth/sms.py:74
+msgid "Shared secret(Shared secret)"
+msgstr "共享密碼(Shared secret)"
+
+#: settings/serializers/auth/sms.py:75
+msgid "Original number(Src id)"
+msgstr "原始號碼(Src id)"
+
+#: settings/serializers/auth/sms.py:76
+msgid "Business type(Service id)"
+msgstr "業務類型(Service id)"
+
+#: settings/serializers/auth/sms.py:80
+#, python-brace-format
+msgid ""
+"Template need contain {code} and Signature + template length does not exceed "
+"67 words. For example, your verification code is {code}, which is valid for "
+"5 minutes. Please do not disclose it to others."
+msgstr ""
+"模板需要包含 {code},並且模板+簽名長度不能超過67個字。例如, 您的驗證碼是 "
+"{code}, 有效期為5分鐘。請不要洩露給其他人。"
+
+#: settings/serializers/auth/sms.py:89
+#, python-brace-format
+msgid "The template needs to contain {code}"
+msgstr "模板需要包含 {code}"
+
+#: settings/serializers/auth/sms.py:92
+msgid "Signature + Template must not exceed 65 words"
+msgstr "模板+簽名不能超過65個字"
+
+#: settings/serializers/auth/sms.py:101
+msgid "URL"
+msgstr "URL"
+
+#: settings/serializers/auth/sms.py:106
+msgid "Request method"
+msgstr "請求方式"
+
+#: settings/serializers/auth/sso.py:16
+msgid "Enable SSO auth"
+msgstr "啟用 SSO 令牌認證"
+
+#: settings/serializers/auth/sso.py:17
+msgid "Other service can using SSO token login to JumpServer without password"
+msgstr "其它系統可以使用 SSO Token 對接 JumpServer, 免去登錄的過程"
+
+#: settings/serializers/auth/sso.py:20
+msgid "SSO auth key TTL"
+msgstr "令牌有效期"
+
+#: settings/serializers/auth/sso.py:20
+#: xpack/plugins/cloud/serializers/account_attrs.py:200
+msgid "Unit: second"
+msgstr "單位: 秒"
+
+#: settings/serializers/auth/wecom.py:15
+msgid "Enable WeCom Auth"
+msgstr "啟用企業微信認證"
+
+#: settings/serializers/basic.py:11
+msgid "Site url"
+msgstr "當前站點 URL"
+
+#: settings/serializers/basic.py:13
+msgid ""
+"External URL, email links or other system callbacks are used to access it, "
+"eg: http://dev.jumpserver.org:8080"
+msgstr ""
+"外部可訪問的 URL, 用於郵件連結或其它系統回調, 例如: http://dev.jumpserver."
+"org:8080"
+
+#: settings/serializers/basic.py:18
+msgid "User guide url"
+msgstr "用戶嚮導URL"
+
+#: settings/serializers/basic.py:19
+msgid "User first login update profile done redirect to it"
+msgstr "用戶第一次登錄,修改profile後重定向到地址, 可以是 wiki 或 其他說明文件"
+
+#: settings/serializers/basic.py:22
+msgid "Global organization name"
+msgstr "全局組織名"
+
+#: settings/serializers/basic.py:23
+msgid "The name of global organization to display"
+msgstr "全局組織的顯示名稱,預設為 全局組織"
+
+#: settings/serializers/basic.py:26
+msgid "Help Docs URL"
+msgstr "文件連結"
+
+#: settings/serializers/basic.py:27
+msgid "default: http://docs.jumpserver.org"
+msgstr "默認: http://dev.jumpserver.org:8080"
+
+#: settings/serializers/basic.py:30
+msgid "Help Support URL"
+msgstr "支持連結"
+
+#: settings/serializers/basic.py:31
+msgid "default: http://www.jumpserver.org/support/"
+msgstr "默認: http://www.jumpserver.org/support/"
+
+#: settings/serializers/basic.py:44
+msgid "Organization name already exists"
+msgstr "組織名稱已存在"
+
+#: settings/serializers/cleaning.py:11
+msgid "Period clean"
+msgstr "定時清掃"
+
+#: settings/serializers/cleaning.py:15
+msgid "Login log keep days (day)"
+msgstr "登錄日誌 (天)"
+
+#: settings/serializers/cleaning.py:19
+msgid "Task log keep days (day)"
+msgstr "任務日誌 (天)"
+
+#: settings/serializers/cleaning.py:23
+msgid "Operate log keep days (day)"
+msgstr "操作日誌 (天)"
+
+#: settings/serializers/cleaning.py:27
+msgid "password change log keep days (day)"
+msgstr "改密日誌 (天)"
+
+#: settings/serializers/cleaning.py:31
+msgid "FTP log keep days (day)"
+msgstr "上傳下載 (天)"
+
+#: settings/serializers/cleaning.py:35
+msgid "Cloud sync record keep days (day)"
+msgstr "雲同步記錄 (天)"
+
+#: settings/serializers/cleaning.py:39
+msgid "job execution keep days (day)"
+msgstr "作業中心執行歷史 (天)"
+
+#: settings/serializers/cleaning.py:43
+msgid "Activity log keep days (day)"
+msgstr "活動記錄 (天)"
+
+#: settings/serializers/cleaning.py:46
+msgid "Session keep duration (day)"
+msgstr "會話日誌 (天)"
+
+#: settings/serializers/cleaning.py:48
+msgid ""
+"Session, record, command will be delete if more than duration, only in "
+"database, OSS will not be affected."
+msgstr ""
+"會話、錄影,命令記錄超過該時長將會被清除 (影響資料庫儲存,OSS 等不受影響)"
+
+#: settings/serializers/feature.py:18
+msgid "Subject"
+msgstr "主題"
+
+#: settings/serializers/feature.py:22
+msgid "More url"
+msgstr "更多資訊 URL"
+
+#: settings/serializers/feature.py:36 settings/serializers/feature.py:39
+msgid "Announcement"
+msgstr "公告"
+
+#: settings/serializers/feature.py:38
+msgid "Enable announcement"
+msgstr "啟用公告"
+
+#: settings/serializers/feature.py:46
+msgid "Enable Vault"
+msgstr "啟用 Vault"
+
+#: settings/serializers/feature.py:55
+msgid "Mount Point"
+msgstr "掛載點"
+
+#: settings/serializers/feature.py:60
+msgid "Historical accounts retained count"
+msgstr "歷史帳號保留數量"
+
+#: settings/serializers/feature.py:62
+msgid ""
+"If the specific value is less than 999, the system will automatically "
+"perform a task every night: check and delete historical accounts that exceed "
+"the predetermined number. If the value reaches or exceeds 999, no historical "
+"account deletion will be performed."
+msgstr ""
+"若特定數值小於999,系統將在每日晚間自動執行任務:檢查並刪除超出預定數量的歷史"
+"帳號。如果該數值達到或超過999,則不進行任何歷史帳號的刪除操作。"
+
+#: settings/serializers/feature.py:71
+msgid "Chat AI"
+msgstr "聊天 AI"
+
+#: settings/serializers/feature.py:75
+msgid "Enable Chat AI"
+msgstr "啟動聊天 AI"
+
+#: settings/serializers/feature.py:78
+msgid "Base Url"
+msgstr "基本地址"
+
+#: settings/serializers/feature.py:81 templates/_header_bar.html:96
+msgid "API Key"
+msgstr "API Key"
+
+#: settings/serializers/feature.py:87
+msgid "GPT Model"
+msgstr "GPT 模型"
+
+#: settings/serializers/feature.py:111
+msgid "Enable tickets"
+msgstr "啟用工單"
+
+#: settings/serializers/feature.py:112
+msgid "No login approval"
+msgstr "免登錄審批"
+
+#: settings/serializers/feature.py:115
+msgid "Ticket authorize default time"
+msgstr "默認工單授權時間"
+
+#: settings/serializers/feature.py:118
+msgid "hour"
+msgstr "時"
+
+#: settings/serializers/feature.py:119
+msgid "Ticket authorize default time unit"
+msgstr "默認工單授權時間單位"
+
+#: settings/serializers/feature.py:124
+msgid "Feature"
+msgstr "功能"
+
+#: settings/serializers/feature.py:127
+msgid "Operation center"
+msgstr "作業中心"
+
+#: settings/serializers/feature.py:128
+msgid "Allow user run batch command or not using ansible"
+msgstr "是否允許用戶使用 ansible 執行批次命令"
+
+#: settings/serializers/feature.py:132
+msgid "Operation center command blacklist"
+msgstr "作業中心命令黑名單"
+
+#: settings/serializers/feature.py:133
+msgid "Commands that are not allowed execute."
+msgstr "不允許執行的命令"
+
+#: settings/serializers/feature.py:138
+#: terminal/models/virtualapp/provider.py:17
+#: terminal/models/virtualapp/virtualapp.py:36
+#: terminal/models/virtualapp/virtualapp.py:97
+#: terminal/serializers/virtualapp.py:32
+msgid "Virtual app"
+msgstr "虛擬應用"
+
+#: settings/serializers/feature.py:141
+msgid "Enable virtual app"
+msgstr "啟用虛擬應用"
+
+#: settings/serializers/msg.py:25
+msgid "SMTP"
+msgstr "SMTP"
+
+#: settings/serializers/msg.py:26
+msgid "EXCHANGE"
+msgstr ""
+
+#: settings/serializers/msg.py:36
+msgid "Tips: Some provider use token except password"
+msgstr "提示:一些郵件提供商需要輸入的是授權碼"
+
+#: settings/serializers/msg.py:39
+msgid "Send user"
+msgstr "發件人"
+
+#: settings/serializers/msg.py:40
+msgid "Tips: Send mail account, default SMTP account as the send account"
+msgstr "提示:發送郵件帳號,預設使用 SMTP 帳號作為發送帳號"
+
+#: settings/serializers/msg.py:43
+msgid "Test recipient"
+msgstr "測試收件人"
+
+#: settings/serializers/msg.py:44
+msgid "Tips: Used only as a test mail recipient"
+msgstr "提示:僅用來作為測試郵件收件人"
+
+#: settings/serializers/msg.py:48
+msgid "If SMTP port is 465, may be select"
+msgstr "如果SMTP埠是465,通常需要啟用 SSL"
+
+#: settings/serializers/msg.py:51
+msgid "Use TLS"
+msgstr "使用 TLS"
+
+#: settings/serializers/msg.py:52
+msgid "If SMTP port is 587, may be select"
+msgstr "如果SMTP埠是587,通常需要啟用 TLS"
+
+#: settings/serializers/msg.py:55
+msgid "Subject prefix"
+msgstr "主題前綴"
+
+#: settings/serializers/msg.py:58
+msgid "Email suffix"
+msgstr "郵件後綴"
+
+#: settings/serializers/msg.py:59
+msgid ""
+"This is used by default if no email is returned during SSO authentication"
+msgstr "SSO認證時,如果沒有返回郵件地址,將使用該後綴"
+
+#: settings/serializers/msg.py:68
+msgid "Create user email subject"
+msgstr "郵件主題"
+
+#: settings/serializers/msg.py:69
+msgid ""
+"Tips: When creating a user, send the subject of the email (eg:Create account "
+"successfully)"
+msgstr "提示: 創建用戶時,發送設置密碼郵件的主題 (例如: 創建用戶成功)"
+
+#: settings/serializers/msg.py:73
+msgid "Create user honorific"
+msgstr "郵件問候語"
+
+#: settings/serializers/msg.py:74
+msgid "Tips: When creating a user, send the honorific of the email (eg:Hello)"
+msgstr "提示: 創建用戶時,發送設置密碼郵件的敬語 (例如: 你好)"
+
+#: settings/serializers/msg.py:78
+msgid "Create user email content"
+msgstr "郵件的內容"
+
+#: settings/serializers/msg.py:80
+#, python-brace-format
+msgid ""
+"Tips: When creating a user, send the content of the email, support "
+"{username} {name} {email} label"
+msgstr ""
+"提示: 創建用戶時,發送設置密碼郵件的內容, 支持 {username} {name} {email} 標籤"
+
+#: settings/serializers/msg.py:84
+msgid "Tips: Email signature (eg:jumpserver)"
+msgstr "郵件署名 (如:jumpserver)"
+
+#: settings/serializers/other.py:8
+msgid "More..."
+msgstr "更多..."
+
+#: settings/serializers/other.py:11
+msgid "Perm ungroup node"
+msgstr "顯示未分組節點"
+
+#: settings/serializers/other.py:12
+msgid "Perm single to ungroup node"
+msgstr ""
+"放置單獨授權的資產到未分組節點, 避免能看到資產所在節點,但該節點未被授權的問"
+"題"
+
+#: settings/serializers/security.py:17
+msgid "User password expiration (day)"
+msgstr "用戶密碼過期時間 (天)"
+
+#: settings/serializers/security.py:19
+msgid ""
+"If the user does not update the password during the time, the user password "
+"will expire failure;The password expiration reminder mail will be automatic "
+"sent to the user by system within 5 days (daily) before the password expires"
+msgstr ""
+"如果用戶在此期間沒有更新密碼,用戶密碼將過期失效; 密碼過期提醒郵件將在密碼過"
+"期前5天內由系統 (每天)自動發送給用戶"
+
+#: settings/serializers/security.py:26
+msgid "Number of repeated historical passwords"
+msgstr "不能設置近幾次密碼"
+
+#: settings/serializers/security.py:28
+msgid ""
+"Tip: When the user resets the password, it cannot be the previous n "
+"historical passwords of the user"
+msgstr "提示:用戶重設密碼時,不能為該用戶前幾次使用過的密碼"
+
+#: settings/serializers/security.py:34
+msgid "Password minimum length"
+msgstr "密碼最小長度"
+
+#: settings/serializers/security.py:38
+msgid "Admin user password minimum length"
+msgstr "管理員密碼最小長度"
+
+#: settings/serializers/security.py:41
+msgid "Must contain capital"
+msgstr "必須包含大寫字符"
+
+#: settings/serializers/security.py:44
+msgid "Must contain lowercase"
+msgstr "必須包含小寫字符"
+
+#: settings/serializers/security.py:47
+msgid "Must contain numeric"
+msgstr "必須包含數字"
+
+#: settings/serializers/security.py:50
+msgid "Must contain special"
+msgstr "必須包含特殊字元"
+
+#: settings/serializers/security.py:55
+msgid ""
+"If the user has failed to log in for a limited number of times, no login is "
+"allowed during this time interval."
+msgstr "當用戶登錄失敗次數達到限制後,那麼在此間隔內禁止登錄"
+
+#: settings/serializers/security.py:63
+msgid "Limit the number of user login failures"
+msgstr "限制用戶登錄失敗次數"
+
+#: settings/serializers/security.py:67
+msgid "Block user login interval (minute)"
+msgstr "禁止用戶登錄間隔 (分)"
+
+#: settings/serializers/security.py:73
+msgid "Limit the number of IP login failures"
+msgstr "限制 IP 登錄失敗次數"
+
+#: settings/serializers/security.py:77
+msgid "Block IP login interval (minute)"
+msgstr "禁止 IP 登錄間隔 (分)"
+
+#: settings/serializers/security.py:81
+msgid "Login IP White List"
+msgstr "IP 登錄白名單"
+
+#: settings/serializers/security.py:86
+msgid "Login IP Black List"
+msgstr "IP 登錄黑名單"
+
+#: settings/serializers/security.py:91
+msgid "Only single device login"
+msgstr "僅一台設備登錄"
+
+#: settings/serializers/security.py:92
+msgid ""
+"After the user logs in on the new device, other logged-in devices will "
+"automatically log out"
+msgstr "用戶在新設備登錄後,其他已登錄的設備會自動退出"
+
+#: settings/serializers/security.py:95
+msgid "Only exist user login"
+msgstr "僅已存在用戶登錄"
+
+#: settings/serializers/security.py:97
+msgid ""
+"If enabled, non-existent users will not be allowed to log in; if disabled, "
+"users of other authentication methods except local authentication methods "
+"are allowed to log in and automatically create users (if the user does not "
+"exist)"
+msgstr ""
+"如果開啟,不存在的用戶將不被允許登錄;如果關閉,除本地認證方式外,其他認證方"
+"式的用戶都允許登錄並自動創建用戶 (如果用戶不存在)"
+
+#: settings/serializers/security.py:103
+msgid "Only from source login"
+msgstr "僅從用戶來源登錄"
+
+#: settings/serializers/security.py:105
+msgid ""
+"If it is enabled, the user will only authenticate to the source when logging "
+"in; if it is disabled, the user will authenticate all the enabled "
+"authentication methods in a certain order when logging in, and as long as "
+"one of the authentication methods is successful, they can log in directly"
+msgstr ""
+"如果開啟,用戶登錄時僅會向來源端進行認證;如果關閉,用戶登錄時會按照一定的順"
+"序對所有已開啟的認證方式進行順序認證,只要有一個認證成功就可以直接登錄"
+
+#: settings/serializers/security.py:116
+msgid "Not enabled"
+msgstr "未啟用"
+
+#: settings/serializers/security.py:117
+msgid "All users"
+msgstr "所有用戶"
+
+#: settings/serializers/security.py:118
+msgid "Only admin users"
+msgstr "僅管理員"
+
+#: settings/serializers/security.py:120
+msgid "Global MFA auth"
+msgstr "全局啟用 MFA 認證"
+
+#: settings/serializers/security.py:124
+msgid "Third-party login users perform MFA authentication"
+msgstr "第三方認證開啟 MFA"
+
+#: settings/serializers/security.py:125
+msgid "The third-party login modes include OIDC, CAS, and SAML2"
+msgstr "第三方登錄方式包括: OIDC、CAS、SAML2"
+
+#: settings/serializers/security.py:128
+msgid "OTP issuer name"
+msgstr "OTP 掃描後的名稱"
+
+#: settings/serializers/security.py:132
+msgid "OTP valid window"
+msgstr "OTP 延遲有效次數"
+
+#: settings/serializers/security.py:136
+msgid "MFA verify TTL"
+msgstr "MFA 校驗有效期"
+
+#: settings/serializers/security.py:138
+msgid ""
+"Unit: second, The verification MFA takes effect only when you view the "
+"account password"
+msgstr "單位:秒,目前僅在查看帳號密碼校驗 MFA 時生效"
+
+#: settings/serializers/security.py:143
+msgid "MFA in login page"
+msgstr "MFA 在登入頁面輸入"
+
+#: settings/serializers/security.py:144
+msgid "Eu security regulations(GDPR) require MFA to be on the login page"
+msgstr "歐盟數據安全法規(GDPR) 要求 MFA 在登入頁面,來確保系統登錄安全"
+
+#: settings/serializers/security.py:148
+msgid "Verify code TTL (second)"
+msgstr "驗證碼有效時間 (分)"
+
+#: settings/serializers/security.py:149
+msgid "Reset password and send SMS code expiration time"
+msgstr "重設密碼的驗證碼及發送簡訊的驗證碼過期時間"
+
+#: settings/serializers/security.py:153
+msgid "Enable Login dynamic code"
+msgstr "啟用登錄附加碼"
+
+#: settings/serializers/security.py:154
+msgid ""
+"The password and additional code are sent to a third party authentication "
+"system for verification"
+msgstr ""
+"密碼和附加碼一併發送給第三方認證系統進行校驗, 如:有的第三方認證系統,需要 密"
+"碼+6位數字 完成認證"
+
+#: settings/serializers/security.py:158
+msgid "Enable Login captcha"
+msgstr "啟用登錄驗證碼"
+
+#: settings/serializers/security.py:159
+msgid "Enable captcha to prevent robot authentication"
+msgstr "開啟驗證碼,防止機器人登錄"
+
+#: settings/serializers/security.py:162
+msgid "Remote Login Protection"
+msgstr "異地登錄通知"
+
+#: settings/serializers/security.py:164
+msgid ""
+"The system determines whether the login IP address belongs to a common login "
+"city. If the account is logged in from a common login city, the system sends "
+"a remote login reminder"
+msgstr ""
+"根據登錄 IP 是否所屬常用登錄城市進行判斷,若帳號在非常用城市登錄,會發送異地"
+"登錄提醒"
+
+#: settings/serializers/security.py:170
+msgid "Unused user timeout (day)"
+msgstr "不活躍用戶自動禁用 (天)"
+
+#: settings/serializers/security.py:171
+msgid ""
+"Detect infrequent users daily and disable them if they exceed the "
+"predetermined time limit."
+msgstr "每天檢測一次,超過預設時間的用戶自動禁用"
+
+#: settings/serializers/security.py:191
+msgid "Enable watermark"
+msgstr "開啟浮水印"
+
+#: settings/serializers/security.py:192
+msgid "Enabled, the web session and replay contains watermark information"
+msgstr "啟用後,Web 會話和錄影將包含浮水印資訊"
+
+#: settings/serializers/security.py:196
+msgid "Connection max idle time (minute)"
+msgstr "連接最大空閒時間 (分)"
+
+#: settings/serializers/security.py:197
+msgid "If idle time more than it, disconnect connection."
+msgstr "提示:如果超過該配置沒有操作,連接會被斷開"
+
+#: settings/serializers/security.py:200
+msgid "Session expire at browser closed"
+msgstr "會話在瀏覽器關閉時過期"
+
+#: settings/serializers/security.py:201
+msgid "Whether to expire the session when the user closes their browser."
+msgstr "當用戶關閉瀏覽器時是否使會話過期。"
+
+#: settings/serializers/security.py:205
+msgid "Session max connection time (hour)"
+msgstr "會話連接最大時間 (時)"
+
+#: settings/serializers/security.py:206
+msgid "If session connection time more than it, disconnect connection."
+msgstr "提示:如果會話連接超過該配置,連接會被斷開"
+
+#: settings/serializers/security.py:209
+msgid "Remember manual auth"
+msgstr "保存手動輸入密碼"
+
+#: settings/serializers/security.py:212
+#: terminal/templates/terminal/_msg_session_sharing.html:10
+msgid "Session share"
+msgstr "會話分享"
+
+#: settings/serializers/security.py:213
+msgid "Enabled, Allows user active session to be shared with other users"
+msgstr "開啟後允許用戶分享已連接的資產會話給他人,協同工作"
+
+#: settings/serializers/security.py:219
+msgid "Insecure command alert"
+msgstr "危險命令告警"
+
+#: settings/serializers/security.py:222
+msgid "Email recipient"
+msgstr "郵件收件人"
+
+#: settings/serializers/security.py:223
+msgid "Multiple user using , split"
+msgstr "多個用戶,使用 , 分割"
+
+#: settings/serializers/settings.py:62
+#, python-format
+msgid "[%s] %s"
+msgstr "[%s] %s"
+
+#: settings/serializers/terminal.py:9 terminal/models/virtualapp/provider.py:11
+msgid "Hostname"
+msgstr "主機名"
+
+#: settings/serializers/terminal.py:15
+msgid "Auto"
+msgstr "自動"
+
+#: settings/serializers/terminal.py:22
+msgid "Enable terminal register"
+msgstr "組件註冊"
+
+#: settings/serializers/terminal.py:24
+msgid ""
+"Allow terminal register, after all terminal setup, you should disable this "
+"for security"
+msgstr "是否允許組件註冊,當所有終端啟動後,為了安全應該關閉"
+
+#: settings/serializers/terminal.py:27
+msgid "Password auth"
+msgstr "密碼認證"
+
+#: settings/serializers/terminal.py:29
+msgid "Public key auth"
+msgstr "金鑰認證"
+
+#: settings/serializers/terminal.py:30
+msgid ""
+"Tips: If use other auth method, like AD/LDAP, you should disable this to "
+"avoid being able to log in after deleting"
+msgstr ""
+"提示:如果你使用其它認證方式,如 AD/LDAP,你應該禁用此項,以避免第三方系統刪"
+"除後,還可以登錄"
+
+#: settings/serializers/terminal.py:34
+msgid "List sort by"
+msgstr "資產列表排序"
+
+#: settings/serializers/terminal.py:37
+msgid "List page size"
+msgstr "資產列表每頁數量"
+
+#: settings/serializers/terminal.py:39
+msgid "Enable database proxy"
+msgstr "啟用資料庫組件"
+
+#: settings/serializers/terminal.py:40
+msgid "Enable Razor"
+msgstr "啟用 Razor 服務"
+
+#: settings/serializers/terminal.py:41
+msgid "Enable SSH Client"
+msgstr "啟用 SSH Client"
+
+#: settings/tasks/ldap.py:28
+msgid "Periodic import ldap user"
+msgstr "週期匯入 LDAP 用戶"
+
+#: settings/tasks/ldap.py:66
+msgid "Registration periodic import ldap user task"
+msgstr "註冊週期匯入 LDAP 用戶 任務"
+
+#: settings/templates/ldap/_msg_import_ldap_user.html:2
+msgid "Sync task Finish"
+msgstr "同步任務完成"
+
+#: settings/templates/ldap/_msg_import_ldap_user.html:6
+#: terminal/models/session/session.py:46
+msgid "Date end"
+msgstr "結束日期"
+
+#: settings/templates/ldap/_msg_import_ldap_user.html:9
+msgid "Synced Organization"
+msgstr "已同步組織"
+
+#: settings/templates/ldap/_msg_import_ldap_user.html:15
+msgid "Synced User"
+msgstr "已同步用戶"
+
+#: settings/templates/ldap/_msg_import_ldap_user.html:22
+msgid "No user synchronization required"
+msgstr "沒有用戶需要同步"
+
+#: settings/utils/ldap.py:494
+msgid "ldap:// or ldaps:// protocol is used."
+msgstr "使用 ldap:// 或 ldaps:// 協議"
+
+#: settings/utils/ldap.py:505
+msgid "Host or port is disconnected: {}"
+msgstr "主機或埠不可連接: {}"
+
+#: settings/utils/ldap.py:507
+msgid "The port is not the port of the LDAP service: {}"
+msgstr "埠不是LDAP服務埠: {}"
+
+#: settings/utils/ldap.py:509
+msgid "Please add certificate: {}"
+msgstr "請添加證書"
+
+#: settings/utils/ldap.py:513 settings/utils/ldap.py:540
+#: settings/utils/ldap.py:570 settings/utils/ldap.py:598
+msgid "Unknown error: {}"
+msgstr "未知錯誤: {}"
+
+#: settings/utils/ldap.py:527
+msgid "Bind DN or Password incorrect"
+msgstr "綁定DN或密碼錯誤"
+
+#: settings/utils/ldap.py:534
+msgid "Please enter Bind DN: {}"
+msgstr "請輸入綁定DN: {}"
+
+#: settings/utils/ldap.py:536
+msgid "Please enter Password: {}"
+msgstr "請輸入密碼: {}"
+
+#: settings/utils/ldap.py:538
+msgid "Please enter correct Bind DN and Password: {}"
+msgstr "請輸入正確的綁定DN和密碼: {}"
+
+#: settings/utils/ldap.py:556
+msgid "Invalid User OU or User search filter: {}"
+msgstr "不合法的用戶OU或用戶過濾器: {}"
+
+#: settings/utils/ldap.py:587
+msgid "LDAP User attr map not include: {}"
+msgstr "LDAP屬性映射沒有包含: {}"
+
+#: settings/utils/ldap.py:594
+msgid "LDAP User attr map is not dict"
+msgstr "LDAP屬性映射不合法"
+
+#: settings/utils/ldap.py:613
+msgid "LDAP authentication is not enabled"
+msgstr "LDAP認證沒有啟用"
+
+#: settings/utils/ldap.py:631
+msgid "Error (Invalid LDAP server): {}"
+msgstr "錯誤 (不合法的LDAP伺服器地址): {}"
+
+#: settings/utils/ldap.py:633
+msgid "Error (Invalid Bind DN): {}"
+msgstr "錯誤 (不合法的綁定DN): {}"
+
+#: settings/utils/ldap.py:635
+msgid "Error (Invalid LDAP User attr map): {}"
+msgstr "錯誤 (不合法的LDAP屬性映射): {}"
+
+#: settings/utils/ldap.py:637
+msgid "Error (Invalid User OU or User search filter): {}"
+msgstr "錯誤 (不合法的用戶OU或用戶過濾器): {}"
+
+#: settings/utils/ldap.py:639
+msgid "Error (Not enabled LDAP authentication): {}"
+msgstr "錯誤 (沒有啟用LDAP認證): {}"
+
+#: settings/utils/ldap.py:641
+msgid "Error (Unknown): {}"
+msgstr "錯誤 (未知): {}"
+
+#: settings/utils/ldap.py:644
+msgid "Succeed: Match {} s user"
+msgstr "成功匹配 {} 個用戶"
+
+#: settings/utils/ldap.py:655
+msgid "Please test the connection first"
+msgstr "請先測試連接"
+
+#: settings/utils/ldap.py:677
+msgid "Authentication failed (configuration incorrect): {}"
+msgstr "認證失敗 (配置錯誤): {}"
+
+#: settings/utils/ldap.py:681
+msgid "Authentication failed (username or password incorrect): {}"
+msgstr "認證失敗 (使用者名稱或密碼不正確): {}"
+
+#: settings/utils/ldap.py:683
+msgid "Authentication failed (Unknown): {}"
+msgstr "認證失敗: (未知): {}"
+
+#: settings/utils/ldap.py:686
+msgid "Authentication success: {}"
+msgstr "認證成功: {}"
+
+#: settings/ws.py:195
+msgid "Get ldap users is None"
+msgstr "獲取 LDAP 用戶為 None"
+
+#: settings/ws.py:205
+msgid "Imported {} users successfully (Organization: {})"
+msgstr "成功匯入 {} 個用戶 ( 組織: {} )"
+
+#: templates/_csv_import_export.html:8
+msgid "Export"
+msgstr "匯出"
+
+#: templates/_csv_import_export.html:13 templates/_csv_import_modal.html:5
+msgid "Import"
+msgstr "匯入"
+
+#: templates/_csv_import_modal.html:12
+msgid "Download the imported template or use the exported CSV file format"
+msgstr "下載匯入的模板或使用匯出的csv格式"
+
+#: templates/_csv_import_modal.html:13
+msgid "Download the import template"
+msgstr "下載匯入模板"
+
+#: templates/_csv_import_modal.html:17 templates/_csv_update_modal.html:17
+msgid "Select the CSV file to import"
+msgstr "請選擇csv文件匯入"
+
+#: templates/_csv_import_modal.html:39 templates/_csv_update_modal.html:42
+msgid "Please select file"
+msgstr "選擇文件"
+
+#: templates/_csv_update_modal.html:12
+msgid "Download the update template or use the exported CSV file format"
+msgstr "下載更新的模板或使用匯出的csv格式"
+
+#: templates/_csv_update_modal.html:13
+msgid "Download the update template"
+msgstr "下載更新模板"
+
+#: templates/_header_bar.html:12
+msgid "Help"
+msgstr "幫助"
+
+#: templates/_header_bar.html:19
+msgid "Docs"
+msgstr "文件"
+
+#: templates/_header_bar.html:27
+msgid "Commercial support"
+msgstr "商業支持"
+
+#: templates/_header_bar.html:85 users/forms/profile.py:43
+msgid "Profile"
+msgstr "個人資訊"
+
+#: templates/_header_bar.html:89
+msgid "Admin page"
+msgstr "管理頁面"
+
+#: templates/_header_bar.html:92
+msgid "User page"
+msgstr "用戶頁面"
+
+#: templates/_header_bar.html:97
+msgid "Logout"
+msgstr "註銷登錄"
+
+#: templates/_message.html:6
+msgid ""
+"\n"
+" Your account has expired, please contact the administrator.\n"
+" "
+msgstr ""
+"\n"
+" 您的帳號已經過期,請聯絡管理員。 "
+
+#: templates/_message.html:13
+msgid "Your account will at"
+msgstr "您的帳號將於"
+
+#: templates/_message.html:13 templates/_message.html:30
+msgid "expired. "
+msgstr "過期。"
+
+#: templates/_message.html:23
+#, python-format
+msgid ""
+"\n"
+" Your password has expired, please click
this link update password.\n"
+" "
+msgstr ""
+"\n"
+" 您的密碼已經過期,請點擊
連結 更新密碼\n"
+" "
+
+#: templates/_message.html:30
+msgid "Your password will at"
+msgstr "您的密碼將於"
+
+#: templates/_message.html:31
+#, python-format
+msgid ""
+"\n"
+" please click
this "
+"link to update your password.\n"
+" "
+msgstr ""
+"\n"
+" 請點擊
連結 更"
+"新密碼\n"
+" "
+
+#: templates/_message.html:43
+#, python-format
+msgid ""
+"\n"
+" Your information was incomplete. Please click
this link to complete your information.\n"
+" "
+msgstr ""
+"\n"
+" 你的資訊不完整,請點擊
連結 "
+" 補充完整\n"
+" "
+
+#: templates/_message.html:56
+#, python-format
+msgid ""
+"\n"
+" Your ssh public key not set or expired. Please click
this link to update\n"
+" "
+msgstr ""
+"\n"
+" 您的SSH金鑰沒有設置或已失效,請點擊
連結 更新\n"
+" "
+
+#: templates/_mfa_login_field.html:28
+msgid "Send verification code"
+msgstr "發送驗證碼"
+
+#: templates/_mfa_login_field.html:107
+#: users/templates/users/forgot_password.html:176
+msgid "Wait: "
+msgstr "等待:"
+
+#: templates/_mfa_login_field.html:117
+#: users/templates/users/forgot_password.html:192
+msgid "The verification code has been sent"
+msgstr "驗證碼已發送"
+
+#: templates/_without_nav_base.html:26
+msgid "Home page"
+msgstr "首頁"
+
+#: templates/resource_download.html:18 templates/resource_download.html:33
+#: users/const.py:65
+msgid "Client"
+msgstr "用戶端"
+
+#: templates/resource_download.html:20
+msgid ""
+"JumpServer Client, currently used to launch the client, now only support "
+"launch RDP SSH client, The Telnet client will next"
+msgstr ""
+"JumpServer 用戶端,目前用來喚起 特定用戶端程序 連接資產, 目前僅支持 RDP SSH "
+"用戶端,Telnet 會在未來支持"
+
+#: templates/resource_download.html:33
+msgid "Microsoft"
+msgstr "微軟"
+
+#: templates/resource_download.html:33
+msgid "Official"
+msgstr "官方"
+
+#: templates/resource_download.html:35
+msgid ""
+"macOS needs to download the client to connect RDP asset, which comes with "
+"Windows"
+msgstr "macOS 需要下載用戶端來連接 RDP 資產,Windows 系統默認安裝了該程序"
+
+#: templates/resource_download.html:44
+msgid "Windows Remote application publisher tools"
+msgstr "Windows 遠程應用發布伺服器工具"
+
+#: templates/resource_download.html:45
+msgid ""
+"OpenSSH is a program used to connect remote applications in the Windows "
+"Remote Application Publisher"
+msgstr "OpenSSH 是在 windows 遠程應用發布伺服器中用來連接遠程應用的程序"
+
+#: templates/resource_download.html:53
+msgid "Offline video player"
+msgstr "離線錄影播放器"
+
+#: terminal/api/applet/applet.py:52 terminal/api/applet/applet.py:55
+#: terminal/api/virtualapp/virtualapp.py:43
+#: terminal/api/virtualapp/virtualapp.py:46
+msgid "Invalid zip file"
+msgstr "無效的 zip 文件"
+
+#: terminal/api/applet/applet.py:74
+msgid "This is enterprise edition applet"
+msgstr "企業版遠程應用,在社區版中不能使用"
+
+#: terminal/api/component/endpoint.py:32
+msgid "Not found protocol query params"
+msgstr "未發現 protocol 查詢參數"
+
+#: terminal/api/component/storage.py:31
+msgid "Deleting the default storage is not allowed"
+msgstr "不允許刪除默認儲存配置"
+
+#: terminal/api/component/storage.py:34
+msgid "Cannot delete storage that is being used"
+msgstr "不允許刪除正在使用的儲存配置"
+
+#: terminal/api/component/storage.py:75 terminal/api/component/storage.py:76
+msgid "Command storages"
+msgstr "命令儲存"
+
+#: terminal/api/component/storage.py:82
+msgid "Invalid"
+msgstr "無效"
+
+#: terminal/api/component/storage.py:130 terminal/tasks.py:149
+msgid "Test failure: {}"
+msgstr "測試失敗: {}"
+
+#: terminal/api/component/storage.py:133
+msgid "Test successful"
+msgstr "測試成功"
+
+#: terminal/api/component/storage.py:135
+msgid "Test failure: Please check configuration"
+msgstr "測試失敗:請檢查配置"
+
+#: terminal/api/component/terminal.py:55
+msgid "Have online sessions"
+msgstr "有在線會話"
+
+#: terminal/api/session/session.py:48
+#, python-format
+msgid "User %s %s session %s replay"
+msgstr "用戶 %s %s 了會話 %s 的錄影"
+
+#: terminal/api/session/session.py:317
+msgid "Session does not exist: {}"
+msgstr "會話不存在: {}"
+
+#: terminal/api/session/session.py:320
+msgid "Session is finished or the protocol not supported"
+msgstr "會話已經完成或協議不支持"
+
+#: terminal/api/session/session.py:333
+msgid "User does not have permission"
+msgstr "用戶沒有權限"
+
+#: terminal/api/session/sharing.py:29
+msgid "Secure session sharing settings is disabled"
+msgstr "未開啟會話共享"
+
+#: terminal/apps.py:9
+msgid "Terminals"
+msgstr "終端管理"
+
+#: terminal/backends/command/models.py:19
+msgid "Input"
+msgstr "輸入"
+
+#: terminal/backends/command/models.py:20 terminal/serializers/command.py:73
+msgid "Output"
+msgstr "輸出"
+
+#: terminal/backends/command/models.py:24 terminal/serializers/command.py:22
+#: terminal/templates/terminal/_msg_command_warning.html:10
+msgid "Risk level"
+msgstr "風險等級"
+
+#: terminal/connect_methods.py:29
+msgid "SSH Client"
+msgstr "SSH 用戶端"
+
+#: terminal/connect_methods.py:30
+msgid "SSH Guide"
+msgstr "SSH 嚮導"
+
+#: terminal/connect_methods.py:31
+msgid "SFTP Client"
+msgstr "SFTP 用戶端"
+
+#: terminal/connect_methods.py:33
+msgid "DB Guide"
+msgstr "DB 連接嚮導"
+
+#: terminal/connect_methods.py:34
+msgid "DB Client"
+msgstr "資料庫用戶端"
+
+#: terminal/connect_methods.py:36
+msgid "Remote Desktop"
+msgstr "遠程桌面用戶端"
+
+#: terminal/connect_methods.py:37
+msgid "RDP Guide"
+msgstr "RDP 連接嚮導"
+
+#: terminal/const.py:12
+msgid "Review & Reject"
+msgstr "審批 & 拒絕"
+
+#: terminal/const.py:13
+msgid "Review & Accept"
+msgstr "審批 & 接受"
+
+#: terminal/const.py:14
+msgid "Review & Cancel"
+msgstr "審批 & 取消"
+
+#: terminal/const.py:45
+msgid "Critical"
+msgstr "嚴重"
+
+#: terminal/const.py:46
+msgid "High"
+msgstr "較高"
+
+#: terminal/const.py:47 terminal/const.py:84
+#: users/templates/users/reset_password.html:50
+msgid "Normal"
+msgstr "正常"
+
+#: terminal/const.py:48
+msgid "Offline"
+msgstr "離線"
+
+#: terminal/const.py:80
+msgid "Mismatch"
+msgstr "未匹配"
+
+#: terminal/const.py:85
+msgid "Tunnel"
+msgstr "隧道"
+
+#: terminal/const.py:91
+msgid "Read only"
+msgstr "只讀"
+
+#: terminal/const.py:92
+msgid "Writable"
+msgstr "讀寫"
+
+#: terminal/const.py:96
+msgid "Kill session"
+msgstr "終斷會話"
+
+#: terminal/const.py:97
+msgid "Lock session"
+msgstr "鎖定會話"
+
+#: terminal/const.py:98
+msgid "Unlock session"
+msgstr "解鎖會話"
+
+#: terminal/const.py:103
+msgid "Replay create failed"
+msgstr "錄影創建失敗"
+
+#: terminal/const.py:104
+msgid "Replay upload failed"
+msgstr "錄影上傳失敗"
+
+#: terminal/const.py:105
+msgid "Replay convert failed"
+msgstr "錄影轉檔失敗"
+
+#: terminal/const.py:106
+msgid "Replay unsupported"
+msgstr "不支持錄影"
+
+#: terminal/exceptions.py:8
+msgid "Bulk create not support"
+msgstr "不支持批次創建"
+
+#: terminal/exceptions.py:13
+msgid "Storage is invalid"
+msgstr "儲存無效"
+
+#: terminal/models/applet/applet.py:30 xpack/plugins/license/models.py:88
+msgid "Community edition"
+msgstr "社區版"
+
+#: terminal/models/applet/applet.py:31
+msgid "Enterprise"
+msgstr "企業版"
+
+#: terminal/models/applet/applet.py:36
+#: terminal/models/virtualapp/virtualapp.py:22
+msgid "Author"
+msgstr "作者"
+
+#: terminal/models/applet/applet.py:38 terminal/serializers/applet.py:31
+msgid "Edition"
+msgstr "版本"
+
+#: terminal/models/applet/applet.py:43
+msgid "Can concurrent"
+msgstr "可以並發"
+
+#: terminal/models/applet/applet.py:44
+#: terminal/models/virtualapp/virtualapp.py:29
+msgid "Tags"
+msgstr "標籤"
+
+#: terminal/models/applet/applet.py:48 terminal/serializers/applet_host.py:167
+#: terminal/serializers/storage.py:197
+msgid "Hosts"
+msgstr "主機"
+
+#: terminal/models/applet/applet.py:93
+#: terminal/models/virtualapp/virtualapp.py:66
+msgid "Applet pkg not valid, Missing file {}"
+msgstr "Applet pkg 無效,缺少文件 {}"
+
+#: terminal/models/applet/applet.py:112
+msgid "Load platform.yml failed: {}"
+msgstr "載入 platform.yml 失敗: {}"
+
+#: terminal/models/applet/applet.py:115
+msgid "Only support custom platform"
+msgstr "只支持自訂平台"
+
+#: terminal/models/applet/applet.py:120
+msgid "Missing type in platform.yml"
+msgstr "在 platform.yml 中缺少類型"
+
+#: terminal/models/applet/applet.py:319 terminal/models/applet/host.py:36
+#: terminal/models/applet/host.py:138
+msgid "Hosting"
+msgstr "宿主機"
+
+#: terminal/models/applet/host.py:18 terminal/serializers/applet_host.py:69
+msgid "Deploy options"
+msgstr "部署參數"
+
+#: terminal/models/applet/host.py:19
+msgid "Auto create accounts"
+msgstr "自動創建帳號"
+
+#: terminal/models/applet/host.py:20
+msgid "Accounts create amount"
+msgstr "創建帳號數量"
+
+#: terminal/models/applet/host.py:21
+msgid "Inited"
+msgstr "已初始化"
+
+#: terminal/models/applet/host.py:22
+msgid "Date inited"
+msgstr "初始化日期"
+
+#: terminal/models/applet/host.py:23
+msgid "Date synced"
+msgstr "同步日期"
+
+#: terminal/models/applet/host.py:28
+msgid "Using same account"
+msgstr "使用同名帳號"
+
+#: terminal/models/applet/host.py:139
+msgid "Initial"
+msgstr "初始化"
+
+#: terminal/models/component/endpoint.py:15
+msgid "HTTPS port"
+msgstr "HTTPS 埠"
+
+#: terminal/models/component/endpoint.py:16
+msgid "HTTP port"
+msgstr "HTTP 埠"
+
+#: terminal/models/component/endpoint.py:17
+msgid "SSH port"
+msgstr "SSH 埠"
+
+#: terminal/models/component/endpoint.py:18
+msgid "RDP port"
+msgstr "RDP 埠"
+
+#: terminal/models/component/endpoint.py:19
+msgid "MySQL port"
+msgstr "MySQL 埠"
+
+#: terminal/models/component/endpoint.py:20
+msgid "MariaDB port"
+msgstr "MariaDB 埠"
+
+#: terminal/models/component/endpoint.py:21
+msgid "PostgreSQL port"
+msgstr "PostgreSQL 埠"
+
+#: terminal/models/component/endpoint.py:22
+msgid "Redis port"
+msgstr "Redis 埠"
+
+#: terminal/models/component/endpoint.py:23
+msgid "SQLServer port"
+msgstr "SQLServer 埠"
+
+#: terminal/models/component/endpoint.py:30
+#: terminal/models/component/endpoint.py:117
+#: terminal/serializers/endpoint.py:73 terminal/serializers/storage.py:41
+#: terminal/serializers/storage.py:53 terminal/serializers/storage.py:83
+#: terminal/serializers/storage.py:93 terminal/serializers/storage.py:101
+msgid "Endpoint"
+msgstr "端點"
+
+#: terminal/models/component/endpoint.py:123
+msgid "Endpoint rule"
+msgstr "端點規則"
+
+#: terminal/models/component/status.py:15
+msgid "Session Online"
+msgstr "在線會話"
+
+#: terminal/models/component/status.py:16
+msgid "CPU Load"
+msgstr "CPU負載"
+
+#: terminal/models/component/status.py:17
+msgid "Memory Used"
+msgstr "記憶體使用"
+
+#: terminal/models/component/status.py:18
+msgid "Disk Used"
+msgstr "磁碟使用"
+
+#: terminal/models/component/status.py:19
+msgid "Connections"
+msgstr "連接數"
+
+#: terminal/models/component/status.py:20
+msgid "Threads"
+msgstr "執行緒數"
+
+#: terminal/models/component/status.py:21
+msgid "Boot Time"
+msgstr "運行時間"
+
+#: terminal/models/component/storage.py:28
+msgid "Default storage"
+msgstr "默認儲存"
+
+#: terminal/models/component/storage.py:140
+#: terminal/models/component/terminal.py:91
+msgid "Command storage"
+msgstr "命令儲存"
+
+#: terminal/models/component/storage.py:204
+#: terminal/models/component/terminal.py:92
+msgid "Replay storage"
+msgstr "錄影儲存"
+
+#: terminal/models/component/terminal.py:88
+msgid "type"
+msgstr "類型"
+
+#: terminal/models/component/terminal.py:90 terminal/serializers/command.py:76
+msgid "Remote Address"
+msgstr "遠端地址"
+
+#: terminal/models/component/terminal.py:93
+msgid "Application User"
+msgstr "應用用戶"
+
+#: terminal/models/component/terminal.py:177
+msgid "Can view terminal config"
+msgstr "可以查看終端配置"
+
+#: terminal/models/session/command.py:76
+msgid "Command record"
+msgstr "命令記錄"
+
+#: terminal/models/session/replay.py:12
+msgid "Session replay"
+msgstr "會話錄影"
+
+#: terminal/models/session/replay.py:14
+msgid "Can upload session replay"
+msgstr "可以上傳會話錄影"
+
+#: terminal/models/session/replay.py:15
+msgid "Can download session replay"
+msgstr "可以下載會話錄影"
+
+#: terminal/models/session/session.py:35
+msgid "Account id"
+msgstr "帳號 ID"
+
+#: terminal/models/session/session.py:37 terminal/models/session/sharing.py:118
+msgid "Login from"
+msgstr "登錄來源"
+
+#: terminal/models/session/session.py:42
+msgid "Replay"
+msgstr "重播"
+
+#: terminal/models/session/session.py:48 terminal/serializers/session.py:68
+msgid "Command amount"
+msgstr "命令數量"
+
+#: terminal/models/session/session.py:49 terminal/serializers/session.py:30
+msgid "Error reason"
+msgstr "錯誤原因"
+
+#: terminal/models/session/session.py:290
+msgid "Session record"
+msgstr "會話記錄"
+
+#: terminal/models/session/session.py:292
+msgid "Can monitor session"
+msgstr "可以監控會話"
+
+#: terminal/models/session/session.py:293
+msgid "Can share session"
+msgstr "可以分享會話"
+
+#: terminal/models/session/session.py:294
+msgid "Can terminate session"
+msgstr "可以終斷會話"
+
+#: terminal/models/session/session.py:295
+msgid "Can validate session action perm"
+msgstr "可以驗證會話動作權限"
+
+#: terminal/models/session/sharing.py:32
+msgid "Expired time (min)"
+msgstr "過期時間 (分)"
+
+#: terminal/models/session/sharing.py:36 terminal/serializers/sharing.py:20
+#: terminal/serializers/sharing.py:52
+msgid "Action permission"
+msgstr "操作權限"
+
+#: terminal/models/session/sharing.py:38
+msgid "Origin"
+msgstr "來源"
+
+#: terminal/models/session/sharing.py:42 terminal/models/session/sharing.py:100
+#: terminal/notifications.py:261
+msgid "Session sharing"
+msgstr "會話分享"
+
+#: terminal/models/session/sharing.py:44
+msgid "Can add super session sharing"
+msgstr "可以創建超級會話分享"
+
+#: terminal/models/session/sharing.py:83
+msgid "Link not active"
+msgstr "連結失效"
+
+#: terminal/models/session/sharing.py:85
+msgid "Link expired"
+msgstr "連結過期"
+
+#: terminal/models/session/sharing.py:87
+msgid "User not allowed to join"
+msgstr "該用戶無權加入會話"
+
+#: terminal/models/session/sharing.py:104 terminal/serializers/sharing.py:71
+msgid "Joiner"
+msgstr "加入者"
+
+#: terminal/models/session/sharing.py:107
+msgid "Date joined"
+msgstr "加入日期"
+
+#: terminal/models/session/sharing.py:110
+msgid "Date left"
+msgstr "結束日期"
+
+#: terminal/models/session/sharing.py:133
+msgid "Session join record"
+msgstr "會話加入記錄"
+
+#: terminal/models/session/sharing.py:149
+msgid "Invalid verification code"
+msgstr "驗證碼不正確"
+
+#: terminal/models/session/sharing.py:156
+msgid "You have already joined this session"
+msgstr "您已經加入過此會話"
+
+#: terminal/models/virtualapp/virtualapp.py:32
+msgid "Providers"
+msgstr "提供商"
+
+#: terminal/models/virtualapp/virtualapp.py:94
+#: terminal/serializers/virtualapp.py:34
+msgid "App Provider"
+msgstr "應用提供商"
+
+#: terminal/models/virtualapp/virtualapp.py:102
+msgid "Virtual app publication"
+msgstr "虛擬應用發布"
+
+#: terminal/notifications.py:25
+msgid "Sessions"
+msgstr "會話管理"
+
+#: terminal/notifications.py:72
+msgid "Command warning"
+msgstr "命令告警"
+
+#: terminal/notifications.py:130
+msgid "Command reject"
+msgstr "命令拒絕"
+
+#: terminal/notifications.py:157 terminal/notifications.py:206
+msgid "Level"
+msgstr "級別"
+
+#: terminal/notifications.py:175
+msgid "Batch danger command alert"
+msgstr "批次危險命令告警"
+
+#: terminal/notifications.py:224
+msgid "Command and replay storage"
+msgstr "命令及錄影儲存"
+
+#: terminal/notifications.py:225
+msgid "Connectivity alarm"
+msgstr "可連接性告警"
+
+#: terminal/notifications.py:240 terminal/tasks.py:153
+msgid "Test failure: Account invalid"
+msgstr "測試失敗: 帳號無效"
+
+#: terminal/notifications.py:250
+#: terminal/templates/terminal/_msg_check_command_replay_storage_connectivity.html:4
+msgid "Invalid storage"
+msgstr "無效的儲存"
+
+#: terminal/serializers/applet.py:28 terminal/serializers/virtualapp.py:15
+msgid "Icon"
+msgstr "圖示"
+
+#: terminal/serializers/applet_host.py:24
+msgid "Per Session"
+msgstr "每用戶"
+
+#: terminal/serializers/applet_host.py:25
+msgid "Per Device"
+msgstr "每設備"
+
+#: terminal/serializers/applet_host.py:37
+msgid "Core API"
+msgstr "Core 服務地址"
+
+#: terminal/serializers/applet_host.py:38
+msgid ""
+" \n"
+" Tips: The application release machine communicates with the Core "
+"service. \n"
+" If the release machine and the Core service are on the same network "
+"segment, \n"
+" it is recommended to fill in the intranet address, otherwise fill in "
+"the current site URL \n"
+"
\n"
+" eg: https://172.16.10.110 or https://dev.jumpserver.com\n"
+" "
+msgstr ""
+"提示:應用發布機和 Core 服務進行通信使用,如果發布機和 Core 服務在同一網段,"
+"建議填寫內網地址,否則填寫當前站點 URL
例如:https://172.16.10.110 or "
+"https://dev.jumpserver.com"
+
+#: terminal/serializers/applet_host.py:46 terminal/serializers/storage.py:208
+msgid "Ignore Certificate Verification"
+msgstr "忽略證書認證"
+
+#: terminal/serializers/applet_host.py:47
+msgid "Existing RDS license"
+msgstr "已有 RDS 許可證"
+
+#: terminal/serializers/applet_host.py:48
+msgid "RDS License Server"
+msgstr "RDS 許可伺服器"
+
+#: terminal/serializers/applet_host.py:49
+msgid "RDS Licensing Mode"
+msgstr "RDS 授權模式"
+
+#: terminal/serializers/applet_host.py:51
+msgid "RDS Single Session Per User"
+msgstr "RDS 單用戶單會話"
+
+#: terminal/serializers/applet_host.py:53
+msgid "RDS Max Disconnection Time (ms)"
+msgstr "RDS 最大斷開時間(毫秒)"
+
+#: terminal/serializers/applet_host.py:55
+msgid ""
+"Tips: Set the maximum duration for keeping a disconnected session active on "
+"the server (log off the session after 60000 milliseconds)."
+msgstr ""
+"提示:設置某個已斷開連接的會話在伺服器上能保持活動狀態的最長時間(60000 毫秒"
+"後註銷會話)"
+
+#: terminal/serializers/applet_host.py:60
+msgid "RDS Remote App Logoff Time Limit (ms)"
+msgstr "RDS 遠程應用註銷時間限制(毫秒)"
+
+#: terminal/serializers/applet_host.py:62
+msgid ""
+"Tips: Set the logoff time for RemoteApp sessions after closing all RemoteApp "
+"programs (0 milliseconds, log off the session immediately)."
+msgstr ""
+"提示:關閉所有 RemoteApp 程序之後設置 RemoteAPP 會話的註銷時間(0 毫秒,立即"
+"註銷會話)"
+
+#: terminal/serializers/applet_host.py:71 terminal/serializers/terminal.py:47
+#: terminal/serializers/virtualapp_provider.py:13
+msgid "Load status"
+msgstr "負載狀態"
+
+#: terminal/serializers/applet_host.py:85
+msgid ""
+"These accounts are used to connect to the published application, the account "
+"is now divided into two types, one is dedicated to each account, each user "
+"has a private account, the other is public, when the application does not "
+"support multiple open and the special has been used, the public account will "
+"be used to connect"
+msgstr ""
+"這些帳號用於連接髮布的應用,帳號現在分為兩種類型:
一種是專用的,每個用"
+"戶都有一個專用帳號。 另一種是公共的,當應用不支持多開且專用的已經被使用時,會"
+"使用公共帳號連接;
注意: 如果不開啟自動創建帳號, 當前發布機僅能被指定標"
+"簽的資產調度到,默認不會放到調度池中,且需要手動維護帳號"
+
+#: terminal/serializers/applet_host.py:92
+msgid "The number of public accounts created automatically"
+msgstr "公用帳號自動創建的數量"
+
+#: terminal/serializers/applet_host.py:95
+msgid ""
+"Connect to the host using the same account first. For security reasons, "
+"please set the configuration item CACHE_LOGIN_PASSWORD_ENABLED=true and "
+"restart the service to enable it."
+msgstr ""
+"優先使用同名帳號連接髮布機。為了安全,需配置文件中開啟配置 "
+"CACHE_LOGIN_PASSWORD_ENABLED=true, 修改後重啟服務"
+
+#: terminal/serializers/applet_host.py:137
+msgid "Install applets"
+msgstr "安裝應用"
+
+#: terminal/serializers/applet_host.py:167
+msgid "Host ID"
+msgstr "主機 ID"
+
+#: terminal/serializers/applet_host.py:168
+msgid "Applet ID"
+msgstr "遠程應用 ID"
+
+#: terminal/serializers/command.py:19
+msgid "Session ID"
+msgstr "會話ID"
+
+#: terminal/serializers/command.py:41
+msgid "Command Filter ACL"
+msgstr "命令過濾器"
+
+#: terminal/serializers/command.py:44
+msgid "Command Group"
+msgstr "命令組"
+
+#: terminal/serializers/command.py:55
+msgid "Invalid command filter ACL id"
+msgstr "無效的 命令過濾器 ID"
+
+#: terminal/serializers/command.py:59
+msgid "Invalid command group id"
+msgstr "無效的 命令組 ID"
+
+#: terminal/serializers/command.py:63
+msgid "Invalid session id"
+msgstr "無效的 Session ID"
+
+#: terminal/serializers/command.py:72
+msgid "Account "
+msgstr "帳號"
+
+#: terminal/serializers/command.py:74
+msgid "Timestamp"
+msgstr "時間戳"
+
+#: terminal/serializers/endpoint.py:15
+msgid "Oracle port"
+msgstr "Oracle 埠"
+
+#: terminal/serializers/endpoint.py:18
+msgid "Oracle port range"
+msgstr "Oracle 埠範圍"
+
+#: terminal/serializers/endpoint.py:20
+msgid ""
+"Oracle proxy server listen port is dynamic, Each additional Oracle database "
+"instance adds a port listener"
+msgstr ""
+"Oracle 代理伺服器監聽埠是動態的,每增加一個 Oracle 資料庫實例,就會增加一個埠"
+"監聽"
+
+#: terminal/serializers/endpoint.py:38
+msgid ""
+"The host address accessed when connecting to assets, if it is empty, the "
+"access address of the current browser will be used (the default endpoint "
+"does not allow modification of the host)"
+msgstr ""
+"連接資產時訪問的主機地址,如果為空則使用當前瀏覽器的訪問地址 (默認端點不允許"
+"修改主機)"
+
+#: terminal/serializers/endpoint.py:64
+msgid ""
+"The assets within this IP range, the following endpoint will be used for the "
+"connection"
+msgstr "該 IP 範圍內的資產,將使用下面的端點進行連接"
+
+#: terminal/serializers/endpoint.py:65
+msgid ""
+"If asset IP addresses under different endpoints conflict, use asset labels"
+msgstr "如果不同端點下的資產 IP 有衝突,使用資產標籤實現"
+
+#: terminal/serializers/endpoint.py:69
+msgid "Asset IP"
+msgstr "資產 IP"
+
+#: terminal/serializers/session.py:25 terminal/serializers/session.py:53
+msgid "Can replay"
+msgstr "是否可重放"
+
+#: terminal/serializers/session.py:26 terminal/serializers/session.py:54
+msgid "Can join"
+msgstr "是否可加入"
+
+#: terminal/serializers/session.py:27 terminal/serializers/session.py:57
+msgid "Can terminate"
+msgstr "是否可中斷"
+
+#: terminal/serializers/session.py:47
+msgid "Duration"
+msgstr "時長"
+
+#: terminal/serializers/session.py:49
+msgid "User ID"
+msgstr "用戶 ID"
+
+#: terminal/serializers/session.py:50
+msgid "Asset ID"
+msgstr "資產 ID"
+
+#: terminal/serializers/session.py:51
+msgid "Login from display"
+msgstr "登錄來源名稱"
+
+#: terminal/serializers/session.py:58
+msgid "Terminal display"
+msgstr "終端顯示"
+
+#: terminal/serializers/storage.py:23
+msgid "Endpoint invalid: remove path `{}`"
+msgstr "端點無效: 移除路徑 `{}`"
+
+#: terminal/serializers/storage.py:29
+msgid "Bucket"
+msgstr "桶名稱"
+
+#: terminal/serializers/storage.py:33
+#: xpack/plugins/cloud/serializers/account_attrs.py:17
+msgid "Access key id"
+msgstr "Access key ID(AK)"
+
+#: terminal/serializers/storage.py:37
+#: xpack/plugins/cloud/serializers/account_attrs.py:20
+msgid "Access key secret"
+msgstr "Access key secret(SK)"
+
+#: terminal/serializers/storage.py:68 xpack/plugins/cloud/models.py:249
+msgid "Region"
+msgstr "地域"
+
+#: terminal/serializers/storage.py:112
+msgid "Container name"
+msgstr "容器名稱"
+
+#: terminal/serializers/storage.py:115
+msgid "Account key"
+msgstr "帳號金鑰"
+
+#: terminal/serializers/storage.py:118
+msgid "Endpoint suffix"
+msgstr "端點後綴"
+
+#: terminal/serializers/storage.py:129
+msgid "HOST"
+msgstr "主機"
+
+#: terminal/serializers/storage.py:146 users/models/user.py:844
+#: xpack/plugins/cloud/serializers/account_attrs.py:213
+msgid "Private key"
+msgstr "ssh私鑰"
+
+#: terminal/serializers/storage.py:173
+msgid "The address cannot contain the special character `#`"
+msgstr "地址中不能包含特殊字元 `#`"
+
+#: terminal/serializers/storage.py:175
+msgid "The address format is incorrect"
+msgstr "地址格式不正確"
+
+#: terminal/serializers/storage.py:182
+msgid "Host invalid"
+msgstr "主機無效"
+
+#: terminal/serializers/storage.py:185
+msgid "Port invalid"
+msgstr "埠無效"
+
+#: terminal/serializers/storage.py:200
+msgid "Index by date"
+msgstr "按日期建索引"
+
+#: terminal/serializers/storage.py:201
+msgid "Whether to create an index by date"
+msgstr "是否根據日期動態建立索引"
+
+#: terminal/serializers/storage.py:204
+msgid "Index"
+msgstr "索引"
+
+#: terminal/serializers/storage.py:206
+msgid "Doc type"
+msgstr "文件類型"
+
+#: terminal/serializers/task.py:9
+msgid "Session id"
+msgstr "會話 ID"
+
+#: terminal/serializers/terminal.py:83 terminal/serializers/terminal.py:91
+msgid "Not found"
+msgstr "沒有發現"
+
+#: terminal/serializers/virtualapp_provider.py:26
+msgid "Container ID"
+msgstr "容器 ID"
+
+#: terminal/serializers/virtualapp_provider.py:27
+msgid "Container Image"
+msgstr "容器鏡像"
+
+#: terminal/serializers/virtualapp_provider.py:28
+msgid "Container Name"
+msgstr "容器名稱"
+
+#: terminal/serializers/virtualapp_provider.py:29
+msgid "Container Status"
+msgstr "容器狀態"
+
+#: terminal/serializers/virtualapp_provider.py:30
+msgid "Container Ports"
+msgstr "容器埠"
+
+#: terminal/session_lifecycle.py:30
+#, python-format
+msgid "Connect to asset %s success"
+msgstr "連接資產 %s 成功"
+
+#: terminal/session_lifecycle.py:38
+#, python-format
+msgid "Connect to asset %s finished: %s"
+msgstr "連接資產 %s 結束: %s"
+
+#: terminal/session_lifecycle.py:48
+#, python-format
+msgid "User %s create share link"
+msgstr "用戶 %s 創建分享連結"
+
+#: terminal/session_lifecycle.py:57
+#, python-format
+msgid "User %s join session"
+msgstr "用戶 %s 加入會話"
+
+#: terminal/session_lifecycle.py:69
+#, python-format
+msgid "User %s leave session"
+msgstr "用戶 %s 離開會話"
+
+#: terminal/session_lifecycle.py:81
+#, python-format
+msgid "User %s join to monitor session"
+msgstr "用戶 %s 監控會話"
+
+#: terminal/session_lifecycle.py:93
+#, python-format
+msgid "User %s exit to monitor session"
+msgstr "用戶 %s 離開監控會話"
+
+#: terminal/session_lifecycle.py:105
+msgid "Replay start to convert"
+msgstr "錄影開始轉化"
+
+#: terminal/session_lifecycle.py:113
+msgid "Replay successfully converted to MP4 format"
+msgstr "錄影成功轉換成 MP4 格式"
+
+#: terminal/session_lifecycle.py:121
+#, python-format
+msgid "Replay failed to convert to MP4 format: %s"
+msgstr "錄影轉換成 MP4 格式失敗: %s"
+
+#: terminal/session_lifecycle.py:129
+msgid "Replay start to upload"
+msgstr "錄影開始上傳"
+
+#: terminal/session_lifecycle.py:137
+msgid "Replay successfully uploaded"
+msgstr "錄影成功上傳"
+
+#: terminal/session_lifecycle.py:145
+#, python-format
+msgid "Replay failed to upload: %s"
+msgstr "錄影上傳失敗:%s"
+
+#: terminal/session_lifecycle.py:152
+msgid "connect failed"
+msgstr "連接失敗"
+
+#: terminal/session_lifecycle.py:153
+msgid "connection disconnect"
+msgstr "連接斷開"
+
+#: terminal/session_lifecycle.py:154
+msgid "user closed"
+msgstr "用戶關閉"
+
+#: terminal/session_lifecycle.py:155
+msgid "idle disconnect"
+msgstr "空閒斷開"
+
+#: terminal/session_lifecycle.py:156
+msgid "admin terminated"
+msgstr "管理員終斷連接"
+
+#: terminal/session_lifecycle.py:157
+msgid "maximum session time has been reached"
+msgstr "超過會話最大連接時間"
+
+#: terminal/session_lifecycle.py:158
+msgid "permission has expired"
+msgstr "授權已過期"
+
+#: terminal/session_lifecycle.py:159
+msgid "storage is null"
+msgstr "儲存為空"
+
+#: terminal/tasks.py:31
+msgid "Periodic delete terminal status"
+msgstr "週期清理終端狀態"
+
+#: terminal/tasks.py:39
+msgid "Clean orphan session"
+msgstr "清除離線會話"
+
+#: terminal/tasks.py:87
+msgid "Run applet host deployment"
+msgstr "運行應用機部署"
+
+#: terminal/tasks.py:97
+msgid "Install applet"
+msgstr "安裝應用"
+
+#: terminal/tasks.py:108
+msgid "Uninstall applet"
+msgstr "卸載應用"
+
+#: terminal/tasks.py:119
+msgid "Generate applet host accounts"
+msgstr "收集遠程應用上的帳號"
+
+#: terminal/tasks.py:131
+msgid "Check command replay storage connectivity"
+msgstr "檢查命令及錄影儲存可連接性 "
+
+#: terminal/templates/terminal/_msg_command_alert.html:10
+msgid "view"
+msgstr "查看"
+
+#: terminal/utils/db_port_mapper.py:85
+msgid ""
+"No available port is matched. The number of databases may have exceeded the "
+"number of ports open to the database agent service, Contact the "
+"administrator to open more ports."
+msgstr ""
+"未匹配到可用埠,資料庫的數量可能已經超過資料庫代理服務開放的埠數量,請聯系管"
+"理員開放更多埠。"
+
+#: terminal/utils/db_port_mapper.py:113
+msgid ""
+"No ports can be used, check and modify the limit on the number of ports that "
+"Magnus listens on in the configuration file."
+msgstr "沒有埠可以使用,檢查並修改配置文件中 Magnus 監聽的埠數量限制。"
+
+#: terminal/utils/db_port_mapper.py:115
+msgid "All available port count: {}, Already use port count: {}"
+msgstr "所有可用埠數量:{},已使用埠數量:{}"
+
+#: tickets/api/ticket.py:88 tickets/models/ticket/general.py:286
+msgid "Applicant"
+msgstr "申請人"
+
+#: tickets/apps.py:7
+msgid "Tickets"
+msgstr "工單管理"
+
+#: tickets/const.py:10
+msgid "Apply for asset"
+msgstr "申請資產"
+
+#: tickets/const.py:17 tickets/const.py:24 tickets/const.py:42
+msgid "Open"
+msgstr "打開"
+
+#: tickets/const.py:19 tickets/const.py:31
+msgid "Approved"
+msgstr "已同意"
+
+#: tickets/const.py:20 tickets/const.py:32
+msgid "Rejected"
+msgstr "已拒絕"
+
+#: tickets/const.py:30 tickets/const.py:37
+msgid "Closed"
+msgstr "關閉的"
+
+#: tickets/const.py:49
+msgid "One level"
+msgstr "1 級"
+
+#: tickets/const.py:50
+msgid "Two level"
+msgstr "2 級"
+
+#: tickets/const.py:54
+msgid "Org admin"
+msgstr "組織管理員"
+
+#: tickets/const.py:55
+msgid "Custom user"
+msgstr "自訂用戶"
+
+#: tickets/const.py:56
+msgid "Super admin"
+msgstr "超級管理員"
+
+#: tickets/const.py:57
+msgid "Super admin and org admin"
+msgstr "組織管理員或超級管理員"
+
+#: tickets/const.py:61
+msgid "All assets"
+msgstr "所有資產"
+
+#: tickets/const.py:62
+msgid "Permed assets"
+msgstr "授權的資產"
+
+#: tickets/const.py:63
+msgid "Permed valid assets"
+msgstr "有效授權的資產"
+
+#: tickets/errors.py:9
+msgid "Ticket already closed"
+msgstr "工單已經關閉"
+
+#: tickets/handlers/apply_asset.py:36
+msgid ""
+"Created by the ticket ticket title: {} ticket applicant: {} ticket "
+"processor: {} ticket ID: {}"
+msgstr ""
+"通過工單創建, 工單標題: {}, 工單申請人: {}, 工單處理人: {}, 工單 ID: {}"
+
+#: tickets/handlers/base.py:85
+msgid "Change field"
+msgstr "變更欄位"
+
+#: tickets/handlers/base.py:85
+msgid "Before change"
+msgstr "變更前"
+
+#: tickets/handlers/base.py:85
+msgid "After change"
+msgstr "變更後"
+
+#: tickets/handlers/base.py:97
+msgid "{} {} the ticket"
+msgstr "{} {} 工單"
+
+#: tickets/models/comment.py:14
+msgid "common"
+msgstr "常見的"
+
+#: tickets/models/comment.py:23
+msgid "User display name"
+msgstr "用戶顯示名稱"
+
+#: tickets/models/comment.py:24
+msgid "Body"
+msgstr "內容"
+
+#: tickets/models/flow.py:19 tickets/models/flow.py:61
+#: tickets/models/ticket/general.py:42
+msgid "Approve level"
+msgstr "審批級別"
+
+#: tickets/models/flow.py:24 tickets/serializers/flow.py:17
+msgid "Approve strategy"
+msgstr "審批策略"
+
+#: tickets/models/flow.py:29 tickets/serializers/flow.py:19
+msgid "Assignees"
+msgstr "受理人"
+
+#: tickets/models/flow.py:33
+msgid "Ticket flow approval rule"
+msgstr "工單批准資訊"
+
+#: tickets/models/flow.py:66
+msgid "Ticket flow"
+msgstr "工單流程"
+
+#: tickets/models/relation.py:12
+msgid "Ticket session relation"
+msgstr "工單會話"
+
+#: tickets/models/ticket/apply_application.py:10
+#: tickets/models/ticket/apply_asset.py:13
+msgid "Permission name"
+msgstr "授權規則名稱"
+
+#: tickets/models/ticket/apply_application.py:19
+msgid "Apply applications"
+msgstr "申請應用"
+
+#: tickets/models/ticket/apply_application.py:22
+msgid "Apply system users"
+msgstr "申請的系統用戶"
+
+#: tickets/models/ticket/apply_asset.py:9
+#: tickets/serializers/ticket/apply_asset.py:14
+msgid "Select at least one asset or node"
+msgstr "資產或者節點至少選擇一項"
+
+#: tickets/models/ticket/apply_asset.py:17
+msgid "Apply accounts"
+msgstr "申請帳號"
+
+#: tickets/models/ticket/apply_asset.py:26
+msgid "Apply Asset Ticket"
+msgstr "申請資產"
+
+#: tickets/models/ticket/command_confirm.py:9
+msgid "Run user"
+msgstr "運行的用戶"
+
+#: tickets/models/ticket/command_confirm.py:11
+msgid "Run asset"
+msgstr "運行的資產"
+
+#: tickets/models/ticket/command_confirm.py:12
+msgid "Run command"
+msgstr "運行的命令"
+
+#: tickets/models/ticket/command_confirm.py:19
+msgid "Command filter acl"
+msgstr "命令過濾器"
+
+#: tickets/models/ticket/command_confirm.py:23
+msgid "Apply Command Ticket"
+msgstr "命令覆核工單"
+
+#: tickets/models/ticket/general.py:77
+msgid "Ticket step"
+msgstr "工單步驟"
+
+#: tickets/models/ticket/general.py:95
+msgid "Ticket assignee"
+msgstr "工單受理人"
+
+#: tickets/models/ticket/general.py:270
+msgid "Title"
+msgstr "標題"
+
+#: tickets/models/ticket/general.py:290
+msgid "TicketFlow"
+msgstr "工單流程"
+
+#: tickets/models/ticket/general.py:293
+msgid "Approval step"
+msgstr "審批步驟"
+
+#: tickets/models/ticket/general.py:296
+msgid "Relation snapshot"
+msgstr "工單快照"
+
+#: tickets/models/ticket/general.py:399
+msgid "Please try again"
+msgstr "請再次嘗試"
+
+#: tickets/models/ticket/general.py:475
+msgid "Super ticket"
+msgstr "超級工單"
+
+#: tickets/models/ticket/login_asset_confirm.py:11
+msgid "Login user"
+msgstr "登錄用戶"
+
+#: tickets/models/ticket/login_asset_confirm.py:14
+msgid "Login asset"
+msgstr "登錄資產"
+
+#: tickets/models/ticket/login_asset_confirm.py:17
+msgid "Login account"
+msgstr "登入帳號"
+
+#: tickets/models/ticket/login_asset_confirm.py:27
+msgid "Apply Login Asset Ticket"
+msgstr "資產登錄覆核工單"
+
+#: tickets/models/ticket/login_confirm.py:12
+msgid "Login datetime"
+msgstr "登錄日期"
+
+#: tickets/models/ticket/login_confirm.py:15
+msgid "Apply Login Ticket"
+msgstr "用戶登錄覆核工單"
+
+#: tickets/notifications.py:63
+msgid "Ticket basic info"
+msgstr "工單基本資訊"
+
+#: tickets/notifications.py:64
+msgid "Ticket applied info"
+msgstr "工單申請資訊"
+
+#: tickets/notifications.py:105
+msgid "Your has a new ticket, applicant - {}"
+msgstr "你有一個新的工單, 申請人 - {}"
+
+#: tickets/notifications.py:109
+msgid "{}: New Ticket - {} ({})"
+msgstr "新工單 - {} ({})"
+
+#: tickets/notifications.py:155
+msgid "Your ticket has been processed, processor - {}"
+msgstr "你的工單已被處理, 處理人 - {}"
+
+#: tickets/notifications.py:159
+msgid "Ticket has processed - {} ({})"
+msgstr "你的工單已被處理, 處理人 - {} ({})"
+
+#: tickets/serializers/flow.py:20
+msgid "Assignees display"
+msgstr "受理人名稱"
+
+#: tickets/serializers/flow.py:46
+msgid "Please select the Assignees"
+msgstr "請選擇受理人"
+
+#: tickets/serializers/flow.py:74
+msgid "The current organization type already exists"
+msgstr "當前組織已存在該類型"
+
+#: tickets/serializers/super_ticket.py:15
+msgid "Processor"
+msgstr "處理人"
+
+#: tickets/serializers/ticket/apply_asset.py:16
+msgid "Support fuzzy search, and display up to 10 items"
+msgstr "支持模糊搜索,最多顯示10項"
+
+#: tickets/serializers/ticket/apply_asset.py:22
+msgid "Apply assets"
+msgstr "申請資產"
+
+#: tickets/serializers/ticket/apply_asset.py:26
+msgid "Apply nodes"
+msgstr "申請節點"
+
+#: tickets/serializers/ticket/apply_asset.py:28
+msgid "Apply actions"
+msgstr "申請動作"
+
+#: tickets/serializers/ticket/common.py:15
+#: tickets/serializers/ticket/common.py:75
+msgid "Created by ticket ({}-{})"
+msgstr "通過工單創建 ({}-{})"
+
+#: tickets/serializers/ticket/common.py:67
+msgid "The expiration date should be greater than the start date"
+msgstr "過期時間要大於開始時間"
+
+#: tickets/serializers/ticket/common.py:82
+msgid "Permission named `{}` already exists"
+msgstr "授權名稱 `{}` 已存在"
+
+#: tickets/serializers/ticket/ticket.py:89
+msgid "The ticket flow `{}` does not exist"
+msgstr "工單流程 `{}` 不存在"
+
+#: tickets/templates/tickets/_msg_ticket.html:21
+msgid "View details"
+msgstr "查看詳情"
+
+#: tickets/templates/tickets/_msg_ticket.html:26
+msgid "Direct approval"
+msgstr "直接批准"
+
+#: tickets/templates/tickets/approve_check_password.html:11
+msgid "Ticket information"
+msgstr "工單資訊"
+
+#: tickets/templates/tickets/approve_check_password.html:28
+#: tickets/views/approve.py:43 tickets/views/approve.py:80
+msgid "Ticket approval"
+msgstr "工單審批"
+
+#: tickets/templates/tickets/approve_check_password.html:43
+msgid "Approval"
+msgstr "同意"
+
+#: tickets/views/approve.py:44
+msgid ""
+"This ticket does not exist, the process has ended, or this link has expired"
+msgstr "工單不存在,或者工單流程已經結束,或者此連結已經過期"
+
+#: tickets/views/approve.py:72
+msgid "Click the button below to approve or reject"
+msgstr "點擊下方按鈕同意或者拒絕"
+
+#: tickets/views/approve.py:81
+msgid "After successful authentication, this ticket can be approved directly"
+msgstr "認證成功後,工單可直接審批"
+
+#: tickets/views/approve.py:105
+msgid "Illegal approval action"
+msgstr "無效的審批動作"
+
+#: tickets/views/approve.py:119
+msgid "This user is not authorized to approve this ticket"
+msgstr "此用戶無權審批此工單"
+
+#: users/api/user.py:155
+msgid "Can not invite self"
+msgstr "不能邀請自己"
+
+#: users/api/user.py:208
+msgid "Could not reset self otp, use profile reset instead"
+msgstr "不能在該頁面重設 MFA 多因子認證, 請去個人資訊頁面重設"
+
+#: users/const.py:10
+msgid "System administrator"
+msgstr "系統管理員"
+
+#: users/const.py:11
+msgid "System auditor"
+msgstr "系統審計員"
+
+#: users/const.py:12
+msgid "Organization administrator"
+msgstr "組織管理員"
+
+#: users/const.py:13
+msgid "Organization auditor"
+msgstr "組織審計員"
+
+#: users/const.py:18
+msgid "Reset link will be generated and sent to the user"
+msgstr "生成重設密碼連結,透過郵件發送給用戶"
+
+#: users/const.py:19
+msgid "Set password"
+msgstr "設置密碼"
+
+#: users/const.py:23
+msgid "AUTO"
+msgstr "自動"
+
+#: users/const.py:31
+msgid "Full screen"
+msgstr "全螢幕"
+
+#: users/const.py:32
+msgid "Multi screen"
+msgstr "多屏顯示"
+
+#: users/const.py:33
+msgid "Drives redirect"
+msgstr "磁碟掛載"
+
+#: users/const.py:37
+msgid "Current window"
+msgstr "當前窗口"
+
+#: users/const.py:38
+msgid "New window"
+msgstr "新窗口"
+
+#: users/const.py:47
+msgid "High(32 bit)"
+msgstr "高(32 bit)"
+
+#: users/const.py:48
+msgid "Medium(16 bit)"
+msgstr "中(16 bit)"
+
+#: users/const.py:69
+msgid "Replace"
+msgstr "替換"
+
+#: users/const.py:70
+msgid "Suffix"
+msgstr "加後綴"
+
+#: users/exceptions.py:10
+msgid "MFA not enabled"
+msgstr "MFA 多因子認證沒有開啟"
+
+#: users/exceptions.py:20
+msgid "Unable to delete all users"
+msgstr "無法刪除全部用戶"
+
+#: users/forms/profile.py:48
+msgid ""
+"When enabled, you will enter the MFA binding process the next time you log "
+"in. you can also directly bind in \"personal information -> quick "
+"modification -> change MFA Settings\"!"
+msgstr ""
+"啟用之後您將會在下次登錄時進入多因子認證綁定流程;您也可以在 (個人資訊->快速"
+"修改->設置 MFA 多因子認證)中直接綁定!"
+
+#: users/forms/profile.py:59
+msgid "* Enable MFA to make the account more secure."
+msgstr "* 啟用 MFA 多因子認證,使帳號更加安全。"
+
+#: users/forms/profile.py:68
+msgid ""
+"In order to protect you and your company, please keep your account, password "
+"and key sensitive information properly. (for example: setting complex "
+"password, enabling MFA)"
+msgstr ""
+"為了保護您和公司的安全,請妥善保管您的帳號、密碼和金鑰等重要敏感資訊; (如:"
+"設置複雜密碼,並啟用 MFA 多因子認證)"
+
+#: users/forms/profile.py:75
+msgid "Finish"
+msgstr "完成"
+
+#: users/forms/profile.py:82
+msgid "New password"
+msgstr "新密碼"
+
+#: users/forms/profile.py:87
+msgid "Confirm password"
+msgstr "確認密碼"
+
+#: users/forms/profile.py:95
+msgid "Password does not match"
+msgstr "密碼不一致"
+
+#: users/forms/profile.py:104
+msgid "The phone number must contain an area code, for example, +86"
+msgstr "手機號碼必須包含區號,例如 +86"
+
+#: users/forms/profile.py:120
+msgid "Old password"
+msgstr "原來密碼"
+
+#: users/forms/profile.py:130
+msgid "Old password error"
+msgstr "原來密碼錯誤"
+
+#: users/forms/profile.py:140
+msgid "Automatically configure and download the SSH key"
+msgstr "自動配置並下載SSH金鑰"
+
+#: users/forms/profile.py:142
+msgid "ssh public key"
+msgstr "SSH公鑰"
+
+#: users/forms/profile.py:143
+msgid "ssh-rsa AAAA..."
+msgstr "ssh-rsa AAAA..."
+
+#: users/forms/profile.py:144
+msgid "Paste your id_rsa.pub here."
+msgstr "複製你的公鑰到這裡"
+
+#: users/forms/profile.py:157
+msgid "Public key should not be the same as your old one."
+msgstr "不能和原來的金鑰相同"
+
+#: users/forms/profile.py:161 users/serializers/profile.py:76
+#: users/serializers/profile.py:164 users/serializers/profile.py:191
+msgid "Not a valid ssh public key"
+msgstr "SSH金鑰不合法"
+
+#: users/forms/profile.py:172 users/models/user.py:847
+#: xpack/plugins/cloud/serializers/account_attrs.py:210
+msgid "Public key"
+msgstr "SSH公鑰"
+
+#: users/models/preference.py:38 users/serializers/preference/preference.py:19
+msgid "Preference"
+msgstr "用戶設置"
+
+#: users/models/user.py:656 users/serializers/profile.py:94
+msgid "Force enable"
+msgstr "強制啟用"
+
+#: users/models/user.py:762
+msgid "Lark"
+msgstr ""
+
+#: users/models/user.py:826 users/serializers/user.py:175
+msgid "Is service account"
+msgstr "服務帳號"
+
+#: users/models/user.py:828
+msgid "Avatar"
+msgstr "頭像"
+
+#: users/models/user.py:831
+msgid "Wechat"
+msgstr "微信"
+
+#: users/models/user.py:834 users/serializers/user.py:111
+msgid "Phone"
+msgstr "手機"
+
+#: users/models/user.py:840
+msgid "OTP secret key"
+msgstr "OTP 金鑰"
+
+# msgid "Private key"
+# msgstr "ssh私鑰"
+#: users/models/user.py:852 users/serializers/profile.py:128
+#: users/serializers/user.py:172
+msgid "Is first login"
+msgstr "首次登錄"
+
+#: users/models/user.py:862
+msgid "Date password last updated"
+msgstr "最後更新密碼日期"
+
+#: users/models/user.py:865
+msgid "Need update password"
+msgstr "需要更新密碼"
+
+#: users/models/user.py:867
+msgid "Date api key used"
+msgstr "Api key 最後使用日期"
+
+#: users/models/user.py:1000
+msgid "Can not delete admin user"
+msgstr "無法刪除管理員用戶"
+
+#: users/models/user.py:1028
+msgid "Can invite user"
+msgstr "可以邀請用戶"
+
+#: users/models/user.py:1029
+msgid "Can remove user"
+msgstr "可以移除用戶"
+
+#: users/models/user.py:1030
+msgid "Can match user"
+msgstr "可以匹配用戶"
+
+#: users/models/user.py:1039
+msgid "Administrator"
+msgstr "管理員"
+
+#: users/models/user.py:1042
+msgid "Administrator is the super user of system"
+msgstr "Administrator是初始的超級管理員"
+
+#: users/models/user.py:1067
+msgid "User password history"
+msgstr "用戶密碼歷史"
+
+#: users/notifications.py:55
+#: users/templates/users/_msg_password_expire_reminder.html:17
+#: users/templates/users/reset_password.html:5
+#: users/templates/users/reset_password.html:6
+msgid "Reset password"
+msgstr "重設密碼"
+
+#: users/notifications.py:85 users/views/profile/reset.py:233
+msgid "Reset password success"
+msgstr "重設密碼成功"
+
+#: users/notifications.py:117
+msgid "Reset public key success"
+msgstr "重設公鑰成功"
+
+#: users/notifications.py:143
+msgid "Password is about expire"
+msgstr "密碼即將過期"
+
+#: users/notifications.py:171
+msgid "Account is about expire"
+msgstr "帳號即將過期"
+
+#: users/notifications.py:193
+msgid "Reset SSH Key"
+msgstr "重設 SSH 金鑰"
+
+#: users/notifications.py:214
+msgid "Reset MFA"
+msgstr "重設 MFA"
+
+#: users/serializers/preference/koko.py:10
+msgid "File name conflict resolution"
+msgstr "檔案名衝突解決方案"
+
+#: users/serializers/preference/koko.py:14
+msgid "Terminal theme name"
+msgstr "終端主題名稱"
+
+#: users/serializers/preference/lina.py:13
+msgid "New file encryption password"
+msgstr "文件加密密碼"
+
+#: users/serializers/preference/lina.py:18
+msgid "Confirm file encryption password"
+msgstr "確認文件加密密碼"
+
+#: users/serializers/preference/lina.py:31 users/serializers/profile.py:48
+msgid "The newly set password is inconsistent"
+msgstr "兩次密碼不一致"
+
+#: users/serializers/preference/luna.py:26
+msgid "Async loading of asset tree"
+msgstr "非同步載入資產樹"
+
+#: users/serializers/preference/luna.py:30
+msgid "Connect default open method"
+msgstr "連接默認打開方式"
+
+#: users/serializers/preference/luna.py:37
+msgid "RDP resolution"
+msgstr "RDP 解析度"
+
+#: users/serializers/preference/luna.py:41
+msgid "Keyboard layout"
+msgstr "鍵盤布局"
+
+#: users/serializers/preference/luna.py:45
+msgid "RDP client option"
+msgstr "RDP 用戶端選項"
+
+#: users/serializers/preference/luna.py:49
+msgid "RDP color quality"
+msgstr "RDP 顏色質量"
+
+#: users/serializers/preference/luna.py:53
+msgid "RDP smart size"
+msgstr "RDP 智慧尺寸"
+
+# msgid "Rdp smart size"
+# msgstr "RDP 智慧大小"
+#: users/serializers/preference/luna.py:54
+msgid ""
+"Determines whether the client computer should scale the content on the "
+"remote computer to fit the window size of the client computer when the "
+"window is resized."
+msgstr ""
+"確定調整窗口大小時用戶端計算機是否應縮放遠程計算機上的內容以適應用戶端計算機"
+"的窗口大小"
+
+#: users/serializers/preference/luna.py:59
+msgid "Remote application connection method"
+msgstr "遠程應用連接方式"
+
+#: users/serializers/preference/luna.py:66
+msgid "Character terminal font size"
+msgstr "字元終端字體大小"
+
+#: users/serializers/preference/luna.py:69
+msgid "Backspace as Ctrl+H"
+msgstr "字元終端Backspace As Ctrl+H"
+
+#: users/serializers/preference/luna.py:72
+msgid "Right click quickly paste"
+msgstr "右鍵快速黏貼"
+
+#: users/serializers/preference/luna.py:78
+msgid "Graphics"
+msgstr "圖形化"
+
+#: users/serializers/preference/luna.py:79
+msgid "Command line"
+msgstr "命令行"
+
+#: users/serializers/profile.py:29
+msgid "The old password is incorrect"
+msgstr "舊密碼錯誤"
+
+#: users/serializers/profile.py:36 users/serializers/profile.py:178
+msgid "Password does not match security rules"
+msgstr "密碼不滿足安全規則"
+
+#: users/serializers/profile.py:40
+msgid "The new password cannot be the last {} passwords"
+msgstr "新密碼不能是最近 {} 次的密碼"
+
+#: users/serializers/user.py:44
+msgid "System roles"
+msgstr "系統角色"
+
+#: users/serializers/user.py:48
+msgid "Org roles"
+msgstr "組織角色"
+
+#: users/serializers/user.py:51
+msgid "Organizations and roles"
+msgstr "組織和角色"
+
+#: users/serializers/user.py:93
+msgid "Password strategy"
+msgstr "密碼策略"
+
+#: users/serializers/user.py:95
+msgid "MFA enabled"
+msgstr "MFA 已啟用"
+
+#: users/serializers/user.py:97
+msgid "MFA force enabled"
+msgstr "強制 MFA"
+
+#: users/serializers/user.py:99
+msgid "Login blocked"
+msgstr "登錄被鎖定"
+
+#: users/serializers/user.py:102 users/serializers/user.py:181
+msgid "Is OTP bound"
+msgstr "是否綁定了虛擬 MFA"
+
+#: users/serializers/user.py:103
+msgid "Super Administrator"
+msgstr "超級管理員"
+
+#: users/serializers/user.py:104
+msgid "Organization Administrator"
+msgstr "組織管理員"
+
+#: users/serializers/user.py:106
+msgid "Can public key authentication"
+msgstr "可以使用公鑰認證"
+
+#: users/serializers/user.py:176
+msgid "Is org admin"
+msgstr "組織管理員"
+
+#: users/serializers/user.py:178
+msgid "Avatar url"
+msgstr "頭像路徑"
+
+#: users/serializers/user.py:182
+msgid "MFA level"
+msgstr "MFA 級別"
+
+#: users/serializers/user.py:304
+msgid "Select users"
+msgstr "選擇用戶"
+
+#: users/serializers/user.py:305
+msgid "For security, only list several users"
+msgstr "為了安全,僅列出幾個用戶"
+
+#: users/serializers/user.py:338
+msgid "name not unique"
+msgstr "名稱重複"
+
+#: users/signal_handlers.py:33
+msgid ""
+"The administrator has enabled \"Only allow existing users to log in\", \n"
+" and the current user is not in the user list. Please contact the "
+"administrator."
+msgstr ""
+"管理員已開啟'僅允許已存在用戶登錄',當前用戶不在用戶列表中,請聯絡管理員。"
+
+#: users/signal_handlers.py:167
+msgid "Clean up expired user sessions"
+msgstr "清除過期的用戶會話"
+
+#: users/tasks.py:25
+msgid "Check password expired"
+msgstr "校驗密碼已過期"
+
+#: users/tasks.py:39
+msgid "Periodic check password expired"
+msgstr "週期校驗密碼過期"
+
+#: users/tasks.py:53
+msgid "Check user expired"
+msgstr "校驗用戶已過期"
+
+#: users/tasks.py:70
+msgid "Periodic check user expired"
+msgstr "週期檢測用戶過期"
+
+#: users/tasks.py:84
+msgid "Check unused users"
+msgstr "檢查未使用的用戶"
+
+#: users/tasks.py:123
+msgid "The user has not logged in recently and has been disabled."
+msgstr "該用戶最近未登錄,已被禁用。"
+
+#: users/templates/users/_msg_account_expire_reminder.html:7
+msgid "Your account will expire in"
+msgstr "您的帳號即將過期"
+
+#: users/templates/users/_msg_account_expire_reminder.html:8
+msgid ""
+"In order not to affect your normal work, please contact the administrator "
+"for confirmation."
+msgstr ""
+"為了不影響您正常工作,請聯絡管理員確認。\n"
+" "
+
+#: users/templates/users/_msg_password_expire_reminder.html:7
+msgid "Your password will expire in"
+msgstr "您的密碼將過期"
+
+#: users/templates/users/_msg_password_expire_reminder.html:8
+msgid ""
+"For your account security, please click on the link below to update your "
+"password in time"
+msgstr "為了您的帳號安全,請點擊下面的連結及時更新密碼"
+
+#: users/templates/users/_msg_password_expire_reminder.html:11
+msgid "Click here update password"
+msgstr "點擊這裡更新密碼"
+
+#: users/templates/users/_msg_password_expire_reminder.html:16
+msgid "If your password has expired, please click the link below to"
+msgstr "如果你的密碼已過期,請點擊"
+
+#: users/templates/users/_msg_reset_mfa.html:7
+msgid "Your MFA has been reset by site administrator"
+msgstr "你的 MFA 已經被管理員重設"
+
+#: users/templates/users/_msg_reset_mfa.html:8
+#: users/templates/users/_msg_reset_ssh_key.html:8
+msgid "Please click the link below to set"
+msgstr "請點擊下面連結設置"
+
+#: users/templates/users/_msg_reset_mfa.html:11
+#: users/templates/users/_msg_reset_ssh_key.html:11
+msgid "Click here set"
+msgstr "點擊這裡設置"
+
+#: users/templates/users/_msg_reset_ssh_key.html:7
+msgid "Your ssh public key has been reset by site administrator"
+msgstr "你的 SSH 金鑰已經被管理員重設"
+
+#: users/templates/users/_msg_user_created.html:15
+msgid "click here to set your password"
+msgstr "點擊這裡設置密碼"
+
+#: users/templates/users/forgot_password.html:46
+msgid "Input your email account, that will send a email to your"
+msgstr "輸入您的信箱, 將會發一封重設郵件到您的信箱中"
+
+#: users/templates/users/forgot_password.html:49
+msgid ""
+"Enter your mobile number and a verification code will be sent to your phone"
+msgstr "輸入您的手機號碼,驗證碼將發送到您的手機"
+
+#: users/templates/users/forgot_password.html:71
+msgid "Email account"
+msgstr "信箱帳號"
+
+#: users/templates/users/forgot_password.html:93
+msgid "Mobile number"
+msgstr "手機號碼"
+
+#: users/templates/users/forgot_password.html:101
+msgid "Send"
+msgstr "發送"
+
+#: users/templates/users/forgot_password.html:105
+#: users/templates/users/forgot_password_previewing.html:30
+msgid "Submit"
+msgstr "提交"
+
+#: users/templates/users/forgot_password_previewing.html:21
+msgid "Please enter the username for which you want to retrieve the password"
+msgstr "請輸入您需要找回密碼的使用者名稱"
+
+#: users/templates/users/mfa_setting.html:24
+msgid "Enable MFA"
+msgstr "啟用 MFA 多因子認證"
+
+#: users/templates/users/mfa_setting.html:30
+msgid "MFA force enable, cannot disable"
+msgstr "MFA 已強制啟用,無法禁用"
+
+#: users/templates/users/mfa_setting.html:48
+msgid "MFA setting"
+msgstr "設置 MFA 多因子認證"
+
+#: users/templates/users/mfa_setting.html:61
+msgid "Reset"
+msgstr "重設"
+
+#: users/templates/users/reset_password.html:23
+msgid "Your password must satisfy"
+msgstr "您的密碼必須滿足:"
+
+#: users/templates/users/reset_password.html:24
+msgid "Password strength"
+msgstr "密碼強度:"
+
+#: users/templates/users/reset_password.html:48
+msgid "Very weak"
+msgstr "很弱"
+
+#: users/templates/users/reset_password.html:49
+msgid "Weak"
+msgstr "弱"
+
+#: users/templates/users/reset_password.html:51
+msgid "Medium"
+msgstr "一般"
+
+#: users/templates/users/reset_password.html:52
+msgid "Strong"
+msgstr "強"
+
+#: users/templates/users/reset_password.html:53
+msgid "Very strong"
+msgstr "很強"
+
+#: users/templates/users/user_otp_check_password.html:6
+msgid "Enable OTP"
+msgstr "啟用 MFA(OTP)"
+
+#: users/templates/users/user_otp_enable_bind.html:6
+msgid "Bind one-time password authenticator"
+msgstr "綁定MFA驗證器"
+
+#: users/templates/users/user_otp_enable_bind.html:13
+msgid ""
+"Use the MFA Authenticator application to scan the following qr code for a 6-"
+"bit verification code"
+msgstr "使用 MFA 驗證器應用掃描以下二維碼,獲取6位驗證碼"
+
+#: users/templates/users/user_otp_enable_bind.html:22
+#: users/templates/users/user_verify_mfa.html:27
+msgid "Six figures"
+msgstr "6 位數字"
+
+#: users/templates/users/user_otp_enable_install_app.html:6
+msgid "Install app"
+msgstr "安裝應用"
+
+#: users/templates/users/user_otp_enable_install_app.html:13
+msgid ""
+"Download and install the MFA Authenticator application on your phone or "
+"applet of WeChat"
+msgstr "請在手機端或微信小程序下載並安裝 MFA 驗證器應用"
+
+#: users/templates/users/user_otp_enable_install_app.html:18
+msgid "Android downloads"
+msgstr "Android手機下載"
+
+#: users/templates/users/user_otp_enable_install_app.html:23
+msgid "iPhone downloads"
+msgstr "iPhone手機下載"
+
+#: users/templates/users/user_otp_enable_install_app.html:27
+msgid ""
+"After installation, click the next step to enter the binding page (if "
+"installed, go to the next step directly)."
+msgstr "安裝完成後點擊下一步進入綁定頁面 (如已安裝,直接進入下一步)"
+
+#: users/templates/users/user_password_verify.html:8
+#: users/templates/users/user_password_verify.html:9
+msgid "Verify password"
+msgstr "校驗密碼"
+
+#: users/templates/users/user_verify_mfa.html:9
+msgid "Authenticate"
+msgstr "驗證身份"
+
+#: users/templates/users/user_verify_mfa.html:15
+msgid ""
+"The account protection has been opened, please complete the following "
+"operations according to the prompts"
+msgstr "帳號保護已開啟,請根據提示完成以下操作"
+
+#: users/templates/users/user_verify_mfa.html:17
+msgid "Open MFA Authenticator and enter the 6-bit dynamic code"
+msgstr "請打開 MFA 驗證器,輸入 6 位動態碼"
+
+#: users/views/profile/otp.py:106
+msgid "Already bound"
+msgstr "已經綁定"
+
+#: users/views/profile/otp.py:107
+msgid "MFA already bound, disable first, then bound"
+msgstr "MFA(OTP) 已經綁定,請先禁用,再綁定"
+
+#: users/views/profile/otp.py:134
+msgid "OTP enable success"
+msgstr "MFA(OTP) 啟用成功"
+
+#: users/views/profile/otp.py:135
+msgid "OTP enable success, return login page"
+msgstr "MFA(OTP) 啟用成功,返回到登入頁面"
+
+#: users/views/profile/otp.py:177
+msgid "Disable OTP"
+msgstr "禁用虛擬 MFA(OTP)"
+
+#: users/views/profile/otp.py:183
+msgid "OTP disable success"
+msgstr "MFA(OTP) 禁用成功"
+
+#: users/views/profile/otp.py:184
+msgid "OTP disable success, return login page"
+msgstr "MFA(OTP) 禁用成功,返回登入頁面"
+
+#: users/views/profile/password.py:33 users/views/profile/password.py:38
+msgid "Password invalid"
+msgstr "使用者名稱或密碼無效"
+
+#: users/views/profile/reset.py:66
+msgid ""
+"Non-local users can log in only from third-party platforms and cannot change "
+"their passwords: {}"
+msgstr "非本地用戶僅允許從第三方平台登錄,不支持修改密碼: {}"
+
+#: users/views/profile/reset.py:188 users/views/profile/reset.py:199
+msgid "Token invalid or expired"
+msgstr "令牌錯誤或失效"
+
+#: users/views/profile/reset.py:204
+msgid "User auth from {}, go there change password"
+msgstr "用戶認證源來自 {}, 請去相應系統修改密碼"
+
+#: users/views/profile/reset.py:211
+msgid "* Your password does not meet the requirements"
+msgstr "* 您的密碼不符合要求"
+
+#: users/views/profile/reset.py:217
+msgid "* The new password cannot be the last {} passwords"
+msgstr "* 新密碼不能是最近 {} 次的密碼"
+
+#: users/views/profile/reset.py:234
+msgid "Reset password success, return to login page"
+msgstr "重設密碼成功,返回到登入頁面"
+
+#: xpack/apps.py:8
+msgid "XPACK"
+msgstr "XPack"
+
+#: xpack/exceptions.py:7
+msgid ""
+"The current task is not synchronized with unmatched policy assets, skipping"
+msgstr ""
+
+#: xpack/plugins/cloud/api.py:60
+msgid "Test connection successful"
+msgstr "測試成功"
+
+#: xpack/plugins/cloud/api.py:62
+msgid "Test connection failed: {}"
+msgstr "測試連接失敗:{}"
+
+#: xpack/plugins/cloud/const.py:8
+msgid "Alibaba Cloud"
+msgstr "阿里雲"
+
+#: xpack/plugins/cloud/const.py:9
+msgid "AWS (International)"
+msgstr "AWS (國際)"
+
+#: xpack/plugins/cloud/const.py:10
+msgid "AWS (China)"
+msgstr "AWS (中國)"
+
+#: xpack/plugins/cloud/const.py:11
+msgid "Azure (China)"
+msgstr "Azure (中國)"
+
+#: xpack/plugins/cloud/const.py:12
+msgid "Azure (International)"
+msgstr "Azure (國際)"
+
+#: xpack/plugins/cloud/const.py:14
+msgid "Baidu Cloud"
+msgstr "百度雲"
+
+#: xpack/plugins/cloud/const.py:15
+msgid "JD Cloud"
+msgstr "京東雲"
+
+#: xpack/plugins/cloud/const.py:16
+msgid "KingSoft Cloud"
+msgstr "金山雲"
+
+#: xpack/plugins/cloud/const.py:17
+msgid "Tencent Cloud"
+msgstr "騰訊雲"
+
+#: xpack/plugins/cloud/const.py:18
+msgid "Tencent Cloud (Lighthouse)"
+msgstr "騰訊雲(輕量伺服器應用)"
+
+#: xpack/plugins/cloud/const.py:19
+msgid "Google Cloud Platform"
+msgstr "Google雲"
+
+#: xpack/plugins/cloud/const.py:20
+msgid "UCloud"
+msgstr "ucloud"
+
+#: xpack/plugins/cloud/const.py:21
+msgid "Volcengine"
+msgstr "火山引擎"
+
+#: xpack/plugins/cloud/const.py:23
+msgid "VMware"
+msgstr "VMware"
+
+#: xpack/plugins/cloud/const.py:24 xpack/plugins/cloud/providers/nutanix.py:15
+msgid "Nutanix"
+msgstr "Nutanix"
+
+#: xpack/plugins/cloud/const.py:25
+msgid "Huawei Private Cloud"
+msgstr "華為私有雲"
+
+#: xpack/plugins/cloud/const.py:26
+msgid "Qingyun Private Cloud"
+msgstr "青雲私有雲"
+
+#: xpack/plugins/cloud/const.py:27
+msgid "CTYun Private Cloud"
+msgstr "天翼私有雲"
+
+#: xpack/plugins/cloud/const.py:28
+msgid "OpenStack"
+msgstr "OpenStack"
+
+#: xpack/plugins/cloud/const.py:29 xpack/plugins/cloud/providers/zstack.py:21
+msgid "ZStack"
+msgstr "ZStack"
+
+#: xpack/plugins/cloud/const.py:30
+msgid "Fusion Compute"
+msgstr "融合計算"
+
+#: xpack/plugins/cloud/const.py:31
+msgid "SCP"
+msgstr "深信服SCP"
+
+#: xpack/plugins/cloud/const.py:32
+msgid "Apsara Stack"
+msgstr "阿里雲專有雲"
+
+#: xpack/plugins/cloud/const.py:37
+msgid "Private IP"
+msgstr "私有IP"
+
+#: xpack/plugins/cloud/const.py:38
+msgid "Public IP"
+msgstr "公網IP"
+
+#: xpack/plugins/cloud/const.py:42 xpack/plugins/cloud/models.py:299
+msgid "Instance name"
+msgstr "實例名稱"
+
+#: xpack/plugins/cloud/const.py:43
+msgid "Instance name and Partial IP"
+msgstr "實例名稱和部分IP"
+
+#: xpack/plugins/cloud/const.py:48
+msgid "Succeed"
+msgstr "成功"
+
+#: xpack/plugins/cloud/const.py:52
+msgid "Unsync"
+msgstr "未同步"
+
+#: xpack/plugins/cloud/const.py:53
+msgid "New Sync"
+msgstr "新同步"
+
+#: xpack/plugins/cloud/const.py:54
+msgid "Synced"
+msgstr "已同步"
+
+#: xpack/plugins/cloud/const.py:55
+msgid "Released"
+msgstr "已釋放"
+
+#: xpack/plugins/cloud/const.py:59
+msgid "And"
+msgstr "與"
+
+#: xpack/plugins/cloud/const.py:60
+msgid "Or"
+msgstr "或"
+
+#: xpack/plugins/cloud/manager.py:55 xpack/plugins/cloud/providers/gcp.py:64
+#: xpack/plugins/cloud/providers/huaweicloud.py:34
+msgid "Account unavailable"
+msgstr "帳號無效"
+
+#: xpack/plugins/cloud/meta.py:9
+msgid "Cloud center"
+msgstr "雲管中心"
+
+#: xpack/plugins/cloud/models.py:34
+msgid "Provider"
+msgstr "雲服務商"
+
+#: xpack/plugins/cloud/models.py:38
+msgid "Validity"
+msgstr "有效"
+
+#: xpack/plugins/cloud/models.py:43
+msgid "Cloud account"
+msgstr "雲帳號"
+
+#: xpack/plugins/cloud/models.py:45
+msgid "Test cloud account"
+msgstr "測試雲帳號"
+
+#: xpack/plugins/cloud/models.py:88 xpack/plugins/cloud/serializers/task.py:159
+msgid "Regions"
+msgstr "地域"
+
+#: xpack/plugins/cloud/models.py:91
+msgid "Hostname strategy"
+msgstr "主機名策略"
+
+#: xpack/plugins/cloud/models.py:96 xpack/plugins/cloud/serializers/task.py:162
+msgid "IP network segment group"
+msgstr "IP網段組"
+
+#: xpack/plugins/cloud/models.py:99 xpack/plugins/cloud/serializers/task.py:167
+msgid "Sync IP type"
+msgstr "同步IP類型"
+
+#: xpack/plugins/cloud/models.py:102
+#: xpack/plugins/cloud/serializers/task.py:185
+msgid "Always update"
+msgstr "總是更新"
+
+#: xpack/plugins/cloud/models.py:104
+msgid "Fully synchronous"
+msgstr "完全同步"
+
+#: xpack/plugins/cloud/models.py:109
+msgid "Date last sync"
+msgstr "最後同步日期"
+
+#: xpack/plugins/cloud/models.py:112 xpack/plugins/cloud/models.py:317
+#: xpack/plugins/cloud/models.py:341
+msgid "Strategy"
+msgstr "策略"
+
+#: xpack/plugins/cloud/models.py:117 xpack/plugins/cloud/models.py:196
+msgid "Sync instance task"
+msgstr "同步實例任務"
+
+#: xpack/plugins/cloud/models.py:207 xpack/plugins/cloud/models.py:259
+msgid "Date sync"
+msgstr "同步日期"
+
+#: xpack/plugins/cloud/models.py:211
+msgid "Sync instance snapshot"
+msgstr "同步實例快照"
+
+#: xpack/plugins/cloud/models.py:215
+msgid "Sync instance task execution"
+msgstr "同步實例任務執行"
+
+#: xpack/plugins/cloud/models.py:239
+msgid "Sync task"
+msgstr "同步任務"
+
+#: xpack/plugins/cloud/models.py:243
+msgid "Sync instance task history"
+msgstr "同步實例任務歷史"
+
+#: xpack/plugins/cloud/models.py:246
+msgid "Instance"
+msgstr "實例"
+
+#: xpack/plugins/cloud/models.py:263
+msgid "Sync instance detail"
+msgstr "同步實例詳情"
+
+#: xpack/plugins/cloud/models.py:275 xpack/plugins/cloud/serializers/task.py:72
+msgid "Rule relation"
+msgstr "條件關係"
+
+#: xpack/plugins/cloud/models.py:284
+msgid "Task strategy"
+msgstr "任務策略"
+
+#: xpack/plugins/cloud/models.py:288
+msgid "Equal"
+msgstr "等於"
+
+#: xpack/plugins/cloud/models.py:289
+msgid "Not Equal"
+msgstr "不等於"
+
+#: xpack/plugins/cloud/models.py:290
+msgid "In"
+msgstr "在...中"
+
+#: xpack/plugins/cloud/models.py:291
+msgid "Contains"
+msgstr "包含"
+
+#: xpack/plugins/cloud/models.py:292
+msgid "Exclude"
+msgstr "排除"
+
+#: xpack/plugins/cloud/models.py:293
+msgid "Startswith"
+msgstr "以...開頭"
+
+#: xpack/plugins/cloud/models.py:294
+msgid "Endswith"
+msgstr "以...結尾"
+
+#: xpack/plugins/cloud/models.py:300
+msgid "Instance platform"
+msgstr "實例平台"
+
+#: xpack/plugins/cloud/models.py:301
+msgid "Instance address"
+msgstr "實例地址"
+
+#: xpack/plugins/cloud/models.py:308
+msgid "Rule attr"
+msgstr "規則屬性"
+
+#: xpack/plugins/cloud/models.py:312
+msgid "Rule match"
+msgstr "規則匹配"
+
+#: xpack/plugins/cloud/models.py:314
+msgid "Rule value"
+msgstr "規則值"
+
+#: xpack/plugins/cloud/models.py:321 xpack/plugins/cloud/serializers/task.py:75
+msgid "Strategy rule"
+msgstr "條件"
+
+#: xpack/plugins/cloud/models.py:336
+msgid "Action attr"
+msgstr "動作屬性"
+
+#: xpack/plugins/cloud/models.py:338
+msgid "Action value"
+msgstr "動作值"
+
+#: xpack/plugins/cloud/models.py:345 xpack/plugins/cloud/serializers/task.py:78
+msgid "Strategy action"
+msgstr "動作"
+
+#: xpack/plugins/cloud/providers/aws_international.py:18
+msgid "China (Beijing)"
+msgstr "中國 (北京)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:19
+msgid "China (Ningxia)"
+msgstr "中國 (寧夏)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:22
+msgid "US East (Ohio)"
+msgstr "美國東部 (俄亥俄州)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:23
+msgid "US East (N. Virginia)"
+msgstr "美國東部 (維吉尼亞北部)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:24
+msgid "US West (N. California)"
+msgstr "美國西部 (加利福尼亞北部)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:25
+msgid "US West (Oregon)"
+msgstr "美國西部 (奧勒岡)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:26
+msgid "Africa (Cape Town)"
+msgstr "非洲 (開普敦)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:27
+msgid "Asia Pacific (Hong Kong)"
+msgstr "亞太地區 (香港)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:28
+msgid "Asia Pacific (Mumbai)"
+msgstr "亞太地區 (孟買)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:29
+msgid "Asia Pacific (Osaka-Local)"
+msgstr "亞太區域 (大阪當地)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:30
+msgid "Asia Pacific (Seoul)"
+msgstr "亞太區域 (首爾)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:31
+msgid "Asia Pacific (Singapore)"
+msgstr "亞太區域 (新加坡)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:32
+msgid "Asia Pacific (Sydney)"
+msgstr "亞太區域 (雪梨)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:33
+msgid "Asia Pacific (Tokyo)"
+msgstr "亞太區域 (東京)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:34
+msgid "Canada (Central)"
+msgstr "加拿大 (中部)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:35
+msgid "Europe (Frankfurt)"
+msgstr "歐洲 (法蘭克福)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:36
+msgid "Europe (Ireland)"
+msgstr "歐洲 (愛爾蘭)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:37
+msgid "Europe (London)"
+msgstr "歐洲 (倫敦)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:38
+msgid "Europe (Milan)"
+msgstr "歐洲 (米蘭)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:39
+msgid "Europe (Paris)"
+msgstr "歐洲 (巴黎)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:40
+msgid "Europe (Stockholm)"
+msgstr "歐洲 (斯德哥爾摩)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:41
+msgid "Middle East (Bahrain)"
+msgstr "中東 (巴林)"
+
+#: xpack/plugins/cloud/providers/aws_international.py:42
+msgid "South America (São Paulo)"
+msgstr "南美洲 (聖保羅)"
+
+#: xpack/plugins/cloud/providers/baiducloud.py:56
+#: xpack/plugins/cloud/providers/jdcloud.py:125
+msgid "CN North-Beijing"
+msgstr "華北-北京"
+
+#: xpack/plugins/cloud/providers/baiducloud.py:57
+#: xpack/plugins/cloud/providers/huaweicloud.py:47
+#: xpack/plugins/cloud/providers/jdcloud.py:128
+msgid "CN South-Guangzhou"
+msgstr "華南-廣州"
+
+#: xpack/plugins/cloud/providers/baiducloud.py:58
+msgid "CN East-Suzhou"
+msgstr "華東-蘇州"
+
+#: xpack/plugins/cloud/providers/baiducloud.py:59
+#: xpack/plugins/cloud/providers/huaweicloud.py:54
+msgid "CN-Hong Kong"
+msgstr "中國-香港"
+
+#: xpack/plugins/cloud/providers/baiducloud.py:60
+msgid "CN Center-Wuhan"
+msgstr "華中-武漢"
+
+#: xpack/plugins/cloud/providers/baiducloud.py:61
+msgid "CN North-Baoding"
+msgstr "華北-保定"
+
+#: xpack/plugins/cloud/providers/baiducloud.py:62
+#: xpack/plugins/cloud/providers/jdcloud.py:127
+msgid "CN East-Shanghai"
+msgstr "華東-上海"
+
+#: xpack/plugins/cloud/providers/baiducloud.py:63
+#: xpack/plugins/cloud/providers/huaweicloud.py:56
+msgid "AP-Singapore"
+msgstr "亞太-新加坡"
+
+#: xpack/plugins/cloud/providers/huaweicloud.py:44
+msgid "CN North-Beijing1"
+msgstr "華北-北京1"
+
+#: xpack/plugins/cloud/providers/huaweicloud.py:45
+msgid "CN North-Beijing4"
+msgstr "華北-北京4"
+
+#: xpack/plugins/cloud/providers/huaweicloud.py:46
+msgid "CN North-Ulanqab1"
+msgstr "華北-烏蘭察布一"
+
+#: xpack/plugins/cloud/providers/huaweicloud.py:48
+msgid "CN South-Shenzhen"
+msgstr "華南-廣州"
+
+#: xpack/plugins/cloud/providers/huaweicloud.py:49
+msgid "CN South-Guangzhou-InvitationOnly"
+msgstr "華南-廣州-友好用戶環境"
+
+#: xpack/plugins/cloud/providers/huaweicloud.py:50
+msgid "CN East-Shanghai2"
+msgstr "華東-上海2"
+
+#: xpack/plugins/cloud/providers/huaweicloud.py:51
+msgid "CN East-Shanghai1"
+msgstr "華東-上海1"
+
+#: xpack/plugins/cloud/providers/huaweicloud.py:53
+msgid "CN Southwest-Guiyang1"
+msgstr "西南-貴陽1"
+
+#: xpack/plugins/cloud/providers/huaweicloud.py:55
+msgid "AP-Bangkok"
+msgstr "亞太-曼谷"
+
+#: xpack/plugins/cloud/providers/huaweicloud.py:58
+msgid "AF-Johannesburg"
+msgstr "非洲-約翰尼斯堡"
+
+#: xpack/plugins/cloud/providers/huaweicloud.py:59
+msgid "LA-Mexico City1"
+msgstr "拉美-墨西哥城一"
+
+#: xpack/plugins/cloud/providers/huaweicloud.py:60
+msgid "LA-Santiago"
+msgstr "拉美-聖地牙哥"
+
+#: xpack/plugins/cloud/providers/huaweicloud.py:61
+msgid "LA-Sao Paulo1"
+msgstr "拉美-聖保羅一"
+
+#: xpack/plugins/cloud/providers/huaweicloud.py:63
+msgid "TR-Istanbul"
+msgstr "TR-Istanbul"
+
+#: xpack/plugins/cloud/providers/jdcloud.py:126
+msgid "CN East-Suqian"
+msgstr "華東-宿遷"
+
+#: xpack/plugins/cloud/serializers/account.py:69
+msgid "Validity display"
+msgstr "有效性顯示"
+
+#: xpack/plugins/cloud/serializers/account.py:70
+msgid "Provider display"
+msgstr "服務商顯示"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:35
+msgid "Client ID"
+msgstr "用戶端 ID"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:41
+msgid "Tenant ID"
+msgstr "租戶 ID"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:44
+msgid "Subscription ID"
+msgstr "訂閱 ID"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:98
+#: xpack/plugins/cloud/serializers/account_attrs.py:102
+#: xpack/plugins/cloud/serializers/account_attrs.py:126
+#: xpack/plugins/cloud/serializers/account_attrs.py:156
+#: xpack/plugins/cloud/serializers/account_attrs.py:206
+msgid "API Endpoint"
+msgstr "API 端點"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:108
+msgid "Auth url"
+msgstr "認證地址"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:109
+msgid "eg: http://openstack.example.com:5000/v3"
+msgstr "如: http://openstack.example.com:5000/v3"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:112
+msgid "User domain"
+msgstr "用戶域"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:127
+msgid "Cert File"
+msgstr "證書文件"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:128
+msgid "Key File"
+msgstr "金鑰文件"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:144
+msgid "Service account key"
+msgstr "服務帳號金鑰"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:145
+msgid "The file is in JSON format"
+msgstr "JSON 格式的文件"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:163
+msgid "IP address invalid `{}`, {}"
+msgstr "IP 地址無效: `{}`, {}"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:179
+msgid "Such as: 192.168.1.0/24, 10.0.0.0-10.0.0.255"
+msgstr "例: 192.168.1.0/24,10.0.0.0-10.0.0.255"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:182
+msgid ""
+"The port is used to detect the validity of the IP address. When the "
+"synchronization task is executed, only the valid IP address will be "
+"synchronized.
If the port is 0, all IP addresses are valid."
+msgstr ""
+"埠用來檢測 IP 地址的有效性,在同步任務執行時,只會同步有效的 IP 地址。
如"
+"果埠為 0,則表示所有 IP 地址均有效。"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:190
+msgid "Hostname prefix"
+msgstr "主機名前綴"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:193
+msgid "IP segment"
+msgstr "IP 網段"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:197
+msgid "Test port"
+msgstr "測試埠"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:200
+msgid "Test timeout"
+msgstr "測試超時時間"
+
+#: xpack/plugins/cloud/serializers/account_attrs.py:216
+msgid "Project"
+msgstr "project"
+
+#: xpack/plugins/cloud/serializers/task.py:151
+msgid ""
+"Only instances matching the IP range will be synced.
If the instance "
+"contains multiple IP addresses, the first IP address that matches will be "
+"used as the IP for the created asset.
The default value of * means sync "
+"all instances and randomly match IP addresses.
Such as: 192.168.1.0/24, "
+"10.1.1.1-10.1.1.20"
+msgstr ""
+"只有匹配到 IP 段的實例會被同步。
如果實例包含多個 IP 地址,那麼第一個匹配"
+"到的 IP 地址將被用作創建的資產的 IP。
預設值 * 表示同步所有實例和隨機匹配 "
+"IP 地址。
例如: 192.168.1.0/24,10.1.1.1-10.1.1.20。"
+
+#: xpack/plugins/cloud/serializers/task.py:157
+msgid "History count"
+msgstr "執行次數"
+
+#: xpack/plugins/cloud/serializers/task.py:158
+msgid "Instance count"
+msgstr "實例個數"
+
+#: xpack/plugins/cloud/tasks.py:27
+msgid "Run sync instance task"
+msgstr "執行同步實例任務"
+
+#: xpack/plugins/cloud/tasks.py:41
+msgid "Period clean sync instance task execution"
+msgstr "定期清除同步實例任務執行記錄"
+
+#: xpack/plugins/interface/api.py:52
+msgid "Restore default successfully."
+msgstr "恢復默認成功!"
+
+#: xpack/plugins/interface/meta.py:9
+msgid "Interface settings"
+msgstr "界面設置"
+
+#: xpack/plugins/interface/models.py:23
+msgid "Title of login page"
+msgstr "登入頁面標題"
+
+#: xpack/plugins/interface/models.py:27
+msgid "Image of login page"
+msgstr "登入頁面圖片"
+
+#: xpack/plugins/interface/models.py:31
+msgid "Website icon"
+msgstr "網站圖示"
+
+#: xpack/plugins/interface/models.py:35
+msgid "Logo of management page"
+msgstr "管理頁面logo"
+
+#: xpack/plugins/interface/models.py:39
+msgid "Logo of logout page"
+msgstr "退出頁面logo"
+
+#: xpack/plugins/interface/models.py:41
+msgid "Theme"
+msgstr "主題"
+
+#: xpack/plugins/interface/models.py:42
+msgid "Footer content"
+msgstr "頁尾內容"
+
+#: xpack/plugins/interface/models.py:45 xpack/plugins/interface/models.py:86
+msgid "Interface setting"
+msgstr "界面設置"
+
+#: xpack/plugins/license/api.py:52
+msgid "License import successfully"
+msgstr "許可證匯入成功"
+
+#: xpack/plugins/license/api.py:53
+msgid "License is invalid"
+msgstr "無效的許可證"
+
+#: xpack/plugins/license/meta.py:10 xpack/plugins/license/models.py:144
+msgid "License"
+msgstr "許可證"
+
+#: xpack/plugins/license/models.py:80
+msgid "Basic edition"
+msgstr "企業基礎版"
+
+#: xpack/plugins/license/models.py:82
+msgid "Standard edition"
+msgstr "企業標準版"
+
+#: xpack/plugins/license/models.py:84
+msgid "Professional edition"
+msgstr "企業專業版"
+
+#: xpack/plugins/license/models.py:86
+msgid "Ultimate edition"
+msgstr "企業旗艦版"
diff --git a/apps/locale/zh_Hant/LC_MESSAGES/djangojs.mo b/apps/locale/zh_Hant/LC_MESSAGES/djangojs.mo
new file mode 100644
index 000000000..a7a02ede1
--- /dev/null
+++ b/apps/locale/zh_Hant/LC_MESSAGES/djangojs.mo
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cc91c07e525f289e4277ec70a51438c10b694a4111f922bd4ee4b20f6e0f0cd0
+size 2832
diff --git a/apps/locale/zh_Hant/LC_MESSAGES/djangojs.po b/apps/locale/zh_Hant/LC_MESSAGES/djangojs.po
new file mode 100644
index 000000000..934646143
--- /dev/null
+++ b/apps/locale/zh_Hant/LC_MESSAGES/djangojs.po
@@ -0,0 +1,159 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR
, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-24 11:05+0800\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-ZhConverter: 繁化姬 dict-74c8d060-r1048 @ 2024/04/07 18:35:03 | https://zhconvert.org\n"
+
+#: static/js/jumpserver.js:259
+msgid "Update is successful!"
+msgstr "更新成功"
+
+#: static/js/jumpserver.js:261
+msgid "An unknown error occurred while updating.."
+msgstr "更新時發生未知錯誤"
+
+#: static/js/jumpserver.js:324 static/js/jumpserver.js:362
+#: static/js/jumpserver.js:364
+msgid "Error"
+msgstr "錯誤"
+
+#: static/js/jumpserver.js:324
+msgid "Being used by the asset, please unbind the asset first."
+msgstr "正在被資產使用中,請先解除資產綁定"
+
+#: static/js/jumpserver.js:330 static/js/jumpserver.js:371
+msgid "Delete the success"
+msgstr "刪除成功"
+
+#: static/js/jumpserver.js:337
+msgid "Are you sure about deleting it?"
+msgstr "你確定刪除嗎 ?"
+
+#: static/js/jumpserver.js:341 static/js/jumpserver.js:382
+msgid "Cancel"
+msgstr "取消"
+
+#: static/js/jumpserver.js:343 static/js/jumpserver.js:384
+msgid "Confirm"
+msgstr "確認"
+
+#: static/js/jumpserver.js:362
+msgid ""
+"The organization contains undeleted information. Please try again after "
+"deleting"
+msgstr "組織中包含未刪除資訊,請刪除後重試"
+
+#: static/js/jumpserver.js:364
+msgid ""
+"Do not perform this operation under this organization. Try again after "
+"switching to another organization"
+msgstr "請勿在此組織下執行此操作,切換到其他組織後重試"
+
+#: static/js/jumpserver.js:378
+msgid ""
+"Please ensure that the following information in the organization has been "
+"deleted"
+msgstr "請確保組織內的以下資訊已刪除"
+
+#: static/js/jumpserver.js:379
+msgid ""
+"User list、User group、Asset list、Domain list、Admin user、System user、"
+"Labels、Asset permission"
+msgstr ""
+"用戶列表、用戶組、資產列表、網域列表、特權用戶、系統用戶、標籤管理、資產授權"
+"規則"
+
+#: static/js/jumpserver.js:416
+msgid "Loading"
+msgstr "載入中"
+
+#: static/js/jumpserver.js:417
+msgid "Search"
+msgstr "搜索"
+
+#: static/js/jumpserver.js:420
+#, javascript-format
+msgid "Selected item %d"
+msgstr "選中 %d 項"
+
+#: static/js/jumpserver.js:424
+msgid "Per page _MENU_"
+msgstr "每頁 _MENU_"
+
+#: static/js/jumpserver.js:425
+msgid ""
+"Displays the results of items _START_ to _END_; A total of _TOTAL_ entries"
+msgstr "顯示第 _START_ 至 _END_ 項結果; 總共 _TOTAL_ 項"
+
+#: static/js/jumpserver.js:428
+msgid "No match"
+msgstr "沒有匹配項"
+
+#: static/js/jumpserver.js:429
+msgid "No record"
+msgstr "沒有記錄"
+
+#: static/js/jumpserver.js:582
+msgid "Unknown error occur"
+msgstr "出現未知錯誤"
+
+#: static/js/jumpserver.js:838
+msgid "Password minimum length {N} bits"
+msgstr "密碼最小長度 {N} 位"
+
+#: static/js/jumpserver.js:839
+msgid "Must contain capital letters"
+msgstr "必須包含大寫字母"
+
+#: static/js/jumpserver.js:840
+msgid "Must contain lowercase letters"
+msgstr "必須包含小寫字母"
+
+#: static/js/jumpserver.js:841
+msgid "Must contain numeric characters"
+msgstr "必須包含數字字元"
+
+#: static/js/jumpserver.js:842
+msgid "Must contain special characters"
+msgstr "必須包含特殊字元"
+
+#: static/js/jumpserver.js:984
+msgid "Export failed"
+msgstr "匯出失敗"
+
+#: static/js/jumpserver.js:1001
+msgid "Import Success"
+msgstr "匯入成功"
+
+#: static/js/jumpserver.js:1006
+msgid "Update Success"
+msgstr "更新成功"
+
+#: static/js/jumpserver.js:1007
+msgid "Count"
+msgstr "數量"
+
+#: static/js/jumpserver.js:1035
+msgid "Import failed"
+msgstr "匯入失敗"
+
+#: static/js/jumpserver.js:1040
+msgid "Update failed"
+msgstr "更新失敗"
+
+#: static/js/plugins/moment/moment.min.js:6
+msgid "\n"
+msgstr ""
diff --git a/apps/notifications/backends/__init__.py b/apps/notifications/backends/__init__.py
index 047123e57..5baecf35a 100644
--- a/apps/notifications/backends/__init__.py
+++ b/apps/notifications/backends/__init__.py
@@ -1,7 +1,7 @@
import importlib
-from django.utils.translation import gettext_lazy as _
from django.db import models
+from django.utils.translation import gettext_lazy as _
client_name_mapper = {}
@@ -12,7 +12,9 @@ class BACKEND(models.TextChoices):
DINGTALK = 'dingtalk', _('DingTalk')
SITE_MSG = 'site_msg', _('Site message')
FEISHU = 'feishu', _('FeiShu')
+ LARK = 'lark', 'Lark'
SLACK = 'slack', _('Slack')
+
# SMS = 'sms', _('SMS')
@property
diff --git a/apps/notifications/backends/lark.py b/apps/notifications/backends/lark.py
new file mode 100644
index 000000000..e6e37218a
--- /dev/null
+++ b/apps/notifications/backends/lark.py
@@ -0,0 +1,23 @@
+from django.conf import settings
+
+from common.sdk.im.lark import Lark as Client
+from .base import BackendBase
+
+
+class Lark(BackendBase):
+ account_field = 'lark_id'
+ is_enable_field_in_settings = 'AUTH_LARK'
+
+ def __init__(self):
+ self.client = Client(
+ app_id=settings.LARK_APP_ID,
+ app_secret=settings.LARK_APP_SECRET
+ )
+
+ def send_msg(self, users, message, subject=None):
+ accounts, __, __ = self.get_accounts(users)
+ print('lark', message)
+ return self.client.send_text(accounts, message)
+
+
+backend = Lark
diff --git a/apps/notifications/notifications.py b/apps/notifications/notifications.py
index 5ceb629c3..ba2b69343 100644
--- a/apps/notifications/notifications.py
+++ b/apps/notifications/notifications.py
@@ -196,6 +196,9 @@ class Message(metaclass=MessageType):
def get_feishu_msg(self) -> dict:
return self.markdown_msg
+ def get_lark_msg(self) -> dict:
+ return self.markdown_msg
+
def get_email_msg(self) -> dict:
return self.html_msg_with_sign
diff --git a/apps/notifications/ws.py b/apps/notifications/ws.py
index 653b068d4..5616694de 100644
--- a/apps/notifications/ws.py
+++ b/apps/notifications/ws.py
@@ -1,12 +1,8 @@
import json
-import time
-from threading import Thread
from channels.generic.websocket import JsonWebsocketConsumer
-from django.conf import settings
from common.db.utils import safe_db_connection
-from common.sessions.cache import user_session_manager
from common.utils import get_logger
from .signal_handlers import new_site_msg_chan
from .site_msg import SiteMessageUtil
@@ -26,7 +22,6 @@ class SiteMsgWebsocket(JsonWebsocketConsumer):
user = self.scope["user"]
if user.is_authenticated:
self.accept()
- user_session_manager.add_or_increment(self.session.session_key)
self.sub = self.watch_recv_new_site_msg()
else:
self.close()
@@ -70,32 +65,3 @@ class SiteMsgWebsocket(JsonWebsocketConsumer):
if not self.sub:
return
self.sub.unsubscribe()
-
- user_session_manager.decrement_or_remove(self.session.session_key)
- if self.should_delete_session():
- thread = Thread(target=self.delay_delete_session)
- thread.start()
-
- def should_delete_session(self):
- return (self.session.modified or settings.SESSION_SAVE_EVERY_REQUEST) and \
- not self.session.is_empty() and \
- self.session.get_expire_at_browser_close() and \
- not user_session_manager.check_active(self.session.session_key)
-
- def delay_delete_session(self):
- timeout = 6
- check_interval = 0.5
-
- start_time = time.time()
- while time.time() - start_time < timeout:
- time.sleep(check_interval)
- if user_session_manager.check_active(self.session.session_key):
- return
-
- self.delete_session()
-
- def delete_session(self):
- try:
- self.session.delete()
- except Exception as e:
- logger.info(f'delete session error: {e}')
diff --git a/apps/ops/ansible/callback.py b/apps/ops/ansible/callback.py
index d05158725..80094332e 100644
--- a/apps/ops/ansible/callback.py
+++ b/apps/ops/ansible/callback.py
@@ -48,9 +48,11 @@ class DefaultCallback:
event = data.get('event', None)
if not event:
return
+
pid = data.get('pid', None)
if pid:
self.write_pid(pid)
+
event_data = data.get('event_data', {})
host = event_data.get('remote_addr', '')
task = event_data.get('task', '')
@@ -158,11 +160,9 @@ class DefaultCallback:
def status_handler(self, data, **kwargs):
status = data.get('status', '')
self.status = self.STATUS_MAPPER.get(status, 'unknown')
-
- rc = kwargs.get('runner_config', None)
- self.private_data_dir = rc.private_data_dir if rc else '/tmp/'
+ self.private_data_dir = data.get("private_data_dir", None)
def write_pid(self, pid):
pid_filepath = os.path.join(self.private_data_dir, 'local.pid')
with open(pid_filepath, 'w') as f:
- f.write(str(pid))
+ f.write(str(pid))
\ No newline at end of file
diff --git a/apps/ops/ansible/cleaner.py b/apps/ops/ansible/cleaner.py
new file mode 100644
index 000000000..5a2f29d2f
--- /dev/null
+++ b/apps/ops/ansible/cleaner.py
@@ -0,0 +1,36 @@
+import os
+import shutil
+from functools import wraps
+
+from settings.api import settings
+
+
+def cleanup_post_run(func):
+ def get_instance(*args):
+ if not len(args) > 0:
+ return
+ return args[0]
+
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ instance = get_instance(*args)
+ if not instance or not issubclass(type(instance), WorkPostRunCleaner):
+ raise NotImplementedError("you should extend 'WorkPostRunCleaner'")
+ try:
+ return func(*args, **kwargs)
+ finally:
+ instance.clean_post_run()
+
+ return wrapper
+
+
+class WorkPostRunCleaner:
+ @property
+ def clean_dir(self):
+ raise NotImplemented
+
+ def clean_post_run(self):
+ if settings.DEBUG_DEV:
+ return
+ if self.clean_dir and os.path.exists(self.clean_dir):
+ shutil.rmtree(self.clean_dir)
diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py
index 0dfa0b575..963406e6a 100644
--- a/apps/ops/ansible/inventory.py
+++ b/apps/ops/ansible/inventory.py
@@ -45,7 +45,7 @@ class JMSInventory:
return groups
@staticmethod
- def make_proxy_command(gateway):
+ def make_proxy_command(gateway, path_dir):
proxy_command_list = [
"ssh", "-o", "Port={}".format(gateway.port),
"-o", "StrictHostKeyChecking=no",
@@ -58,7 +58,7 @@ class JMSInventory:
0, "sshpass -p {}".format(gateway.password)
)
if gateway.private_key:
- proxy_command_list.append("-i {}".format(gateway.private_key_path))
+ proxy_command_list.append("-i {}".format(gateway.get_private_key_path(path_dir)))
proxy_command = "-o ProxyCommand='{}'".format(
" ".join(proxy_command_list)
@@ -66,7 +66,7 @@ class JMSInventory:
return {"ansible_ssh_common_args": proxy_command}
@staticmethod
- def make_account_ansible_vars(account):
+ def make_account_ansible_vars(account, path_dir):
var = {
'ansible_user': account.username,
}
@@ -76,22 +76,31 @@ class JMSInventory:
if account.secret_type == 'password':
var['ansible_password'] = account.escape_jinja2_syntax(account.secret)
elif account.secret_type == 'ssh_key':
- var['ansible_ssh_private_key_file'] = account.private_key_path
+ var['ansible_ssh_private_key_file'] = account.get_private_key_path(path_dir)
return var
@staticmethod
- def make_custom_become_ansible_vars(account, su_from_auth):
+ def make_custom_become_ansible_vars(account, su_from_auth, path_dir):
su_method = su_from_auth['ansible_become_method']
var = {
'custom_become': True,
'custom_become_method': su_method,
'custom_become_user': account.su_from.username,
'custom_become_password': account.escape_jinja2_syntax(account.su_from.secret),
- 'custom_become_private_key_path': account.su_from.private_key_path
+ 'custom_become_private_key_path': account.su_from.get_private_key_path(path_dir)
}
return var
- def make_account_vars(self, host, asset, account, automation, protocol, platform, gateway):
+ @staticmethod
+ def make_protocol_setting_vars(host, protocols):
+ # 针对 ssh 协议的特殊处理
+ for p in protocols:
+ if p.name == 'ssh':
+ if hasattr(p, 'setting'):
+ setting = getattr(p, 'setting')
+ host['old_ssh_version'] = setting.get('old_ssh_version', False)
+
+ def make_account_vars(self, host, asset, account, automation, protocol, platform, gateway, path_dir):
from accounts.const import AutomationTypes
if not account:
host['error'] = _("No account available")
@@ -105,15 +114,19 @@ class JMSInventory:
if platform.su_enabled and su_from:
su_from_auth = account.get_ansible_become_auth()
host.update(su_from_auth)
- host.update(self.make_custom_become_ansible_vars(account, su_from_auth))
+ host.update(self.make_custom_become_ansible_vars(account, su_from_auth, path_dir))
elif platform.su_enabled and not su_from and \
self.task_type in (AutomationTypes.change_secret, AutomationTypes.push_account):
- host.update(self.make_account_ansible_vars(account))
+ host.update(self.make_account_ansible_vars(account, path_dir))
host['ansible_become'] = True
host['ansible_become_user'] = 'root'
host['ansible_become_password'] = account.escape_jinja2_syntax(account.secret)
else:
- host.update(self.make_account_ansible_vars(account))
+ host.update(self.make_account_ansible_vars(account, path_dir))
+
+ if platform.name == 'Huawei':
+ host['ansible_connection'] = 'network_cli'
+ host['ansible_network_os'] = 'asa'
if gateway:
ansible_connection = host.get('ansible_connection', 'ssh')
@@ -121,11 +134,11 @@ class JMSInventory:
host['gateway'] = {
'address': gateway.address, 'port': gateway.port,
'username': gateway.username, 'secret': gateway.password,
- 'private_key_path': gateway.private_key_path
+ 'private_key_path': gateway.get_private_key_path(path_dir)
}
host['jms_asset']['port'] = port
else:
- ansible_ssh_common_args = self.make_proxy_command(gateway)
+ ansible_ssh_common_args = self.make_proxy_command(gateway, path_dir)
host['jms_asset'].update(ansible_ssh_common_args)
host.update(ansible_ssh_common_args)
@@ -152,9 +165,10 @@ class JMSInventory:
else:
ansible_config['ansible_winrm_scheme'] = 'http'
ansible_config['ansible_winrm_transport'] = 'ntlm'
+ ansible_config['ansible_winrm_connection_timeout'] = 120
return ansible_config
- def asset_to_host(self, asset, account, automation, protocols, platform):
+ def asset_to_host(self, asset, account, automation, protocols, platform, path_dir):
try:
ansible_config = dict(automation.ansible_config)
except (AttributeError, TypeError):
@@ -177,10 +191,12 @@ class JMSInventory:
'jms_account': {
'id': str(account.id), 'username': account.username,
'secret': account.escape_jinja2_syntax(account.secret),
- 'secret_type': account.secret_type, 'private_key_path': account.private_key_path
+ 'secret_type': account.secret_type, 'private_key_path': account.get_private_key_path(path_dir)
} if account else None
}
+ self.make_protocol_setting_vars(host, protocols)
+
protocols = host['jms_asset']['protocols']
host['jms_asset'].update({f"{p['name']}_port": p['port'] for p in protocols})
if host['jms_account'] and tp == 'oracle':
@@ -194,17 +210,22 @@ class JMSInventory:
gateway = asset.domain.select_gateway()
self.make_account_vars(
- host, asset, account, automation, protocol, platform, gateway
+ host, asset, account, automation, protocol, platform, gateway, path_dir
)
return host
- def get_asset_sorted_accounts(self, asset):
- accounts = list(asset.accounts.filter(is_active=True))
+ @staticmethod
+ def sorted_accounts(accounts):
connectivity_score = {'ok': 2, '-': 1, 'err': 0}
sort_key = lambda x: (x.privileged, connectivity_score.get(x.connectivity, 0), x.date_updated)
accounts_sorted = sorted(accounts, key=sort_key, reverse=True)
return accounts_sorted
+ def get_asset_sorted_accounts(self, asset):
+ accounts = list(asset.accounts.filter(is_active=True))
+ accounts_sorted = self.sorted_accounts(accounts)
+ return accounts_sorted
+
@staticmethod
def get_account_prefer(account_prefer):
account_usernames = []
@@ -258,7 +279,7 @@ class JMSInventory:
for asset in assets:
protocols = self.set_platform_protocol_setting_to_asset(asset, platform_protocols)
account = self.select_account(asset)
- host = self.asset_to_host(asset, account, automation, protocols, platform)
+ host = self.asset_to_host(asset, account, automation, protocols, platform, path_dir)
if not automation.ansible_enabled:
host['error'] = _('Ansible disabled')
diff --git a/apps/ops/ansible/receptor/__init__.py b/apps/ops/ansible/receptor/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/ops/ansible/receptor/receptor_runner.py b/apps/ops/ansible/receptor/receptor_runner.py
new file mode 100644
index 000000000..de07859ae
--- /dev/null
+++ b/apps/ops/ansible/receptor/receptor_runner.py
@@ -0,0 +1,147 @@
+import concurrent.futures
+import os
+import queue
+import socket
+
+from django.conf import settings
+import ansible_runner
+from receptorctl import ReceptorControl
+
+from ops.ansible.cleaner import WorkPostRunCleaner, cleanup_post_run
+
+
+class ReceptorCtl:
+ @property
+ def ctl(self):
+ return ReceptorControl(settings.ANSIBLE_RECEPTOR_SOCK_PATH)
+
+ def cancel(self, unit_id):
+ return self.ctl.simple_command("work cancel {}".format(unit_id))
+
+ def nodes(self):
+ return self.ctl.simple_command("status").get("Advertisements", None)
+
+ def submit_work(self,
+ worktype,
+ payload,
+ node=None,
+ tlsclient=None,
+ ttl=None,
+ signwork=False,
+ params=None, ):
+ return self.ctl.submit_work(worktype, payload, node, tlsclient, ttl, signwork, params)
+
+ def get_work_results(self, unit_id, startpos=0, return_socket=False, return_sockfile=True):
+ return self.ctl.get_work_results(unit_id, startpos, return_socket, return_sockfile)
+
+ def kill_process(self, pid):
+ submit_result = self.submit_work(worktype="kill", node="primary", payload=str(pid))
+ unit_id = submit_result["unitid"]
+ result_socket, result_file = self.get_work_results(unit_id=unit_id, return_sockfile=True,
+ return_socket=True)
+ while not result_socket.close():
+ buf = result_file.read()
+ if not buf:
+ break
+ print(buf.decode('utf8'))
+
+
+receptor_ctl = ReceptorCtl()
+
+
+def run(**kwargs):
+ receptor_runner = AnsibleReceptorRunner(**kwargs)
+ return receptor_runner.run()
+
+
+class AnsibleReceptorRunner(WorkPostRunCleaner):
+ def __init__(self, **kwargs):
+ self.runner_params = kwargs
+ self.unit_id = None
+ self.clean_workspace = kwargs.pop("clean_workspace", True)
+
+ def write_unit_id(self):
+ if not self.unit_id:
+ return
+ private_dir = self.runner_params.get("private_data_dir", "")
+ with open(os.path.join(private_dir, "local.unitid"), "w") as f:
+ f.write(self.unit_id)
+ f.flush()
+
+ @property
+ def clean_dir(self):
+ if not self.clean_workspace:
+ return None
+ return self.runner_params.get("private_data_dir", None)
+
+ @cleanup_post_run
+ def run(self):
+ input, output = socket.socketpair()
+
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
+ transmitter_future = executor.submit(self.transmit, input)
+ result = receptor_ctl.submit_work(payload=output.makefile('rb'),
+ node='primary', worktype='ansible-runner')
+ input.close()
+ output.close()
+
+ self.unit_id = result['unitid']
+ self.write_unit_id()
+
+ transmitter_future.result()
+
+ result_file = receptor_ctl.get_work_results(self.unit_id, return_sockfile=True)
+
+ stdout_queue = queue.Queue()
+
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
+ processor_future = executor.submit(self.processor, result_file, stdout_queue)
+
+ while not processor_future.done() or \
+ not stdout_queue.empty():
+ msg = stdout_queue.get()
+ if msg is None:
+ break
+ print(msg)
+
+ return processor_future.result()
+
+ def transmit(self, _socket):
+ try:
+ ansible_runner.run(
+ streamer='transmit',
+ _output=_socket.makefile('wb'),
+ **self.runner_params
+ )
+ finally:
+ _socket.shutdown(socket.SHUT_WR)
+
+ def processor(self, _result_file, stdout_queue):
+ try:
+ original_event_handler = self.runner_params.pop("event_handler", None)
+ original_status_handler = self.runner_params.pop("status_handler", None)
+
+ def event_handler(data, **kwargs):
+ stdout = data.get('stdout', '')
+ if stdout:
+ stdout_queue.put(stdout)
+ if original_event_handler:
+ original_event_handler(data, **kwargs)
+
+ def status_handler(data, **kwargs):
+ private_data_dir = self.runner_params.get("private_data_dir", None)
+ if private_data_dir:
+ data["private_data_dir"] = private_data_dir
+ if original_status_handler:
+ original_status_handler(data, **kwargs)
+
+ return ansible_runner.interface.run(
+ quite=True,
+ streamer='process',
+ _input=_result_file,
+ event_handler=event_handler,
+ status_handler=status_handler,
+ **self.runner_params,
+ )
+ finally:
+ stdout_queue.put(None)
diff --git a/apps/ops/ansible/runner.py b/apps/ops/ansible/runner.py
index c5fee5fd9..1d808f196 100644
--- a/apps/ops/ansible/runner.py
+++ b/apps/ops/ansible/runner.py
@@ -1,18 +1,38 @@
+import logging
import os
-import uuid
import shutil
+import uuid
+
import ansible_runner
from django.conf import settings
from django.utils._os import safe_join
+from django.utils.functional import LazyObject
from .callback import DefaultCallback
+from .receptor import receptor_runner
from ..utils import get_ansible_log_verbosity
+logger = logging.getLogger(__file__)
+
class CommandInBlackListException(Exception):
pass
+class AnsibleWrappedRunner(LazyObject):
+ def _setup(self):
+ self._wrapped = self.get_runner()
+
+ @staticmethod
+ def get_runner():
+ if settings.ANSIBLE_RECEPTOR_ENABLE and settings.ANSIBLE_RECEPTOR_SOCK_PATH:
+ return receptor_runner
+ return ansible_runner
+
+
+runner = AnsibleWrappedRunner()
+
+
class AdHocRunner:
cmd_modules_choices = ('shell', 'raw', 'command', 'script', 'win_shell')
@@ -29,6 +49,8 @@ class AdHocRunner:
self.extra_vars = extra_vars
self.dry_run = dry_run
self.timeout = timeout
+ # enable local connection
+ self.extra_vars.update({"LOCAL_CONNECTION_ENABLED": "1"})
def check_module(self):
if self.module not in self.cmd_modules_choices:
@@ -43,8 +65,11 @@ class AdHocRunner:
if not os.path.exists(self.project_dir):
os.mkdir(self.project_dir, 0o755)
+ private_env = safe_join(self.project_dir, 'env')
+ if os.path.exists(private_env):
+ shutil.rmtree(private_env)
- ansible_runner.run(
+ runner.run(
timeout=self.timeout if self.timeout > 0 else None,
extravars=self.extra_vars,
host_pattern=self.pattern,
@@ -62,6 +87,7 @@ class AdHocRunner:
class PlaybookRunner:
def __init__(self, inventory, playbook, project_dir='/tmp/', callback=None):
+
self.id = uuid.uuid4()
self.inventory = inventory
self.playbook = playbook
@@ -69,11 +95,24 @@ class PlaybookRunner:
if not callback:
callback = DefaultCallback()
self.cb = callback
+ self.envs = {}
+
+ def copy_playbook(self):
+ entry = os.path.basename(self.playbook)
+ playbook_dir = os.path.dirname(self.playbook)
+ project_playbook_dir = os.path.join(self.project_dir, "project")
+ shutil.copytree(playbook_dir, project_playbook_dir, dirs_exist_ok=True)
+ self.playbook = entry
def run(self, verbosity=0, **kwargs):
- verbosity = get_ansible_log_verbosity(verbosity)
+ self.copy_playbook()
- ansible_runner.run(
+ verbosity = get_ansible_log_verbosity(verbosity)
+ private_env = safe_join(self.project_dir, 'env')
+ if os.path.exists(private_env):
+ shutil.rmtree(private_env)
+
+ runner.run(
private_data_dir=self.project_dir,
inventory=self.inventory,
playbook=self.playbook,
@@ -81,23 +120,32 @@ class PlaybookRunner:
event_handler=self.cb.event_handler,
status_handler=self.cb.status_handler,
host_cwd=self.project_dir,
+ envvars=self.envs,
**kwargs
)
return self.cb
+class SuperPlaybookRunner(PlaybookRunner):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.envs = {"LOCAL_CONNECTION_ENABLED": "1"}
+
+
class UploadFileRunner:
- def __init__(self, inventory, job_id, dest_path, callback=None):
+ def __init__(self, inventory, project_dir, job_id, dest_path, callback=None):
self.id = uuid.uuid4()
self.inventory = inventory
+ self.project_dir = project_dir
self.cb = DefaultCallback()
- upload_file_dir = safe_join(settings.DATA_DIR, 'job_upload_file')
+ upload_file_dir = safe_join(settings.SHARE_DIR, 'job_upload_file')
self.src_paths = safe_join(upload_file_dir, str(job_id))
self.dest_path = safe_join("/tmp", dest_path)
def run(self, verbosity=0, **kwargs):
verbosity = get_ansible_log_verbosity(verbosity)
- ansible_runner.run(
+ runner.run(
+ private_data_dir=self.project_dir,
host_pattern="*",
inventory=self.inventory,
module='copy',
diff --git a/apps/ops/api/adhoc.py b/apps/ops/api/adhoc.py
index 9be055101..70b74bed7 100644
--- a/apps/ops/api/adhoc.py
+++ b/apps/ops/api/adhoc.py
@@ -17,9 +17,6 @@ class AdHocViewSet(OrgBulkModelViewSet):
search_fields = ('name', 'comment')
model = AdHoc
- def allow_bulk_destroy(self, qs, filtered):
- return True
-
def get_queryset(self):
queryset = super().get_queryset()
return queryset.filter(creator=self.request.user)
diff --git a/apps/ops/api/celery.py b/apps/ops/api/celery.py
index dd66a886e..ae4e9d8b6 100644
--- a/apps/ops/api/celery.py
+++ b/apps/ops/api/celery.py
@@ -94,11 +94,13 @@ class CeleryPeriodTaskViewSet(CommonApiMixin, viewsets.ModelViewSet):
queryset = PeriodicTask.objects.all()
serializer_class = CeleryPeriodTaskSerializer
http_method_names = ('get', 'head', 'options', 'patch')
+ lookup_field = 'name'
+ lookup_value_regex = '[\w.@]+'
- def get_queryset(self):
- queryset = super().get_queryset()
- queryset = queryset.exclude(description='')
- return queryset
+ def get_object(self):
+ name = self.kwargs.get('name')
+ obj = get_object_or_404(PeriodicTask, name=name)
+ return obj
class CelerySummaryAPIView(generics.RetrieveAPIView):
@@ -150,21 +152,18 @@ class CeleryTaskViewSet(
return input_string
def generate_execute_time(self, queryset):
- names = [i.name for i in queryset]
- periodic_tasks = PeriodicTask.objects.filter(name__in=names)
- periodic_task_dict = {task.name: task for task in periodic_tasks}
now = local_now()
for i in queryset:
- task = periodic_task_dict.get(i.name)
+ task = getattr(i, 'periodic_obj', None)
if not task:
continue
i.exec_cycle = self.extract_schedule(str(task.scheduler))
-
last_run_at = task.last_run_at or now
next_run_at = task.schedule.remaining_estimate(last_run_at)
if next_run_at.total_seconds() < 0:
next_run_at = task.schedule.remaining_estimate(now)
i.next_exec_time = now + next_run_at
+ i.enabled = task.enabled
return queryset
def generate_summary_state(self, execution_qs):
@@ -209,6 +208,7 @@ class CeleryTaskViewSet(
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
+ queryset = self.mark_periodic_and_sorted(queryset)
page = self.paginate_queryset(queryset)
if page is not None:
@@ -222,6 +222,20 @@ class CeleryTaskViewSet(
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
+ @staticmethod
+ def mark_periodic_and_sorted(queryset):
+ names = queryset.values_list('name', flat=True)
+ periodic_tasks = PeriodicTask.objects.filter(name__in=names)
+ periodic_task_dict = {task.task: task for task in periodic_tasks}
+ for q in queryset:
+ if q.name in periodic_task_dict:
+ q.periodic_obj = periodic_task_dict[q.name]
+ q.is_periodic = True
+ else:
+ q.is_periodic = False
+ queryset = sorted(queryset, key=lambda x: x.is_periodic, reverse=True)
+ return queryset
+
class CeleryTaskExecutionViewSet(CommonApiMixin, viewsets.ModelViewSet):
serializer_class = CeleryTaskExecutionSerializer
@@ -234,6 +248,8 @@ class CeleryTaskExecutionViewSet(CommonApiMixin, viewsets.ModelViewSet):
if task_id:
task = get_object_or_404(CeleryTask, id=task_id)
self.queryset = self.queryset.filter(name=task.name)
+ if not self.request.user.is_superuser:
+ self.queryset = self.queryset.filter(creator=self.request.user)
return self.queryset
def create(self, request, *args, **kwargs):
diff --git a/apps/ops/api/job.py b/apps/ops/api/job.py
index a2a0c00ee..98fded770 100644
--- a/apps/ops/api/job.py
+++ b/apps/ops/api/job.py
@@ -19,11 +19,12 @@ from common.permissions import IsValidUser
from ops.celery import app
from ops.const import Types
from ops.models import Job, JobExecution
-from ops.serializers.job import JobSerializer, JobExecutionSerializer, FileSerializer, JobTaskStopSerializer
+from ops.serializers.job import (
+ JobSerializer, JobExecutionSerializer, FileSerializer, JobTaskStopSerializer
+)
__all__ = [
- 'JobViewSet', 'JobExecutionViewSet', 'JobRunVariableHelpAPIView',
- 'JobAssetDetail', 'JobExecutionTaskDetail', 'UsernameHintsAPI'
+ 'JobViewSet', 'JobExecutionViewSet', 'JobRunVariableHelpAPIView', 'JobExecutionTaskDetail', 'UsernameHintsAPI'
]
from ops.tasks import run_ops_job_execution
@@ -31,6 +32,9 @@ from ops.variables import JMS_JOB_VARIABLE_HELP
from orgs.mixins.api import OrgBulkModelViewSet
from orgs.utils import tmp_to_org, get_current_org
from accounts.models import Account
+from assets.const import Protocol
+from perms.const import ActionChoices
+from perms.utils.asset_perm import PermAssetDetailUtil
from perms.models import PermNode
from perms.utils import UserPermAssetUtil
from jumpserver.settings import get_file_md5
@@ -43,16 +47,17 @@ def set_task_to_serializer_data(serializer, task_id):
def merge_nodes_and_assets(nodes, assets, user):
- if nodes:
- perm_util = UserPermAssetUtil(user=user)
- for node_id in nodes:
- if node_id == PermNode.FAVORITE_NODE_KEY:
- node_assets = perm_util.get_favorite_assets()
- elif node_id == PermNode.UNGROUPED_NODE_KEY:
- node_assets = perm_util.get_ungroup_assets()
- else:
- node, node_assets = perm_util.get_node_all_assets(node_id)
- assets.extend(node_assets.exclude(id__in=[asset.id for asset in assets]))
+ if not nodes:
+ return assets
+ perm_util = UserPermAssetUtil(user=user)
+ for node_id in nodes:
+ if node_id == PermNode.FAVORITE_NODE_KEY:
+ node_assets = perm_util.get_favorite_assets()
+ elif node_id == PermNode.UNGROUPED_NODE_KEY:
+ node_assets = perm_util.get_ungroup_assets()
+ else:
+ _, node_assets = perm_util.get_node_all_assets(node_id)
+ assets.extend(node_assets.exclude(id__in=[asset.id for asset in assets]))
return assets
@@ -62,16 +67,37 @@ class JobViewSet(OrgBulkModelViewSet):
model = Job
def check_permissions(self, request):
+ # job: upload_file
+ if self.action == 'upload' or request.data.get('type') == Types.upload_file:
+ return super().check_permissions(request)
+ # job: adhoc, playbook
if not settings.SECURITY_COMMAND_EXECUTION:
return self.permission_denied(request, "Command execution disabled")
return super().check_permissions(request)
- def allow_bulk_destroy(self, qs, filtered):
- return True
+ def check_upload_permission(self, assets, account_name):
+ protocols_required = {Protocol.ssh, Protocol.sftp, Protocol.winrm}
+ error_msg_missing_protocol = _(
+ "Asset ({asset}) must have at least one of the following protocols added: SSH, SFTP, or WinRM")
+ error_msg_auth_missing_protocol = _("Asset ({asset}) authorization is missing SSH, SFTP, or WinRM protocol")
+ error_msg_auth_missing_upload = _("Asset ({asset}) authorization lacks upload permissions")
+ for asset in assets:
+ protocols = asset.protocols.values_list("name", flat=True)
+ if not set(protocols).intersection(protocols_required):
+ self.permission_denied(self.request, error_msg_missing_protocol.format(asset=asset.name))
+ util = PermAssetDetailUtil(self.request.user, asset)
+ if not util.check_perm_protocols(protocols_required):
+ self.permission_denied(self.request, error_msg_auth_missing_protocol.format(asset=asset.name))
+ if not util.check_perm_actions(account_name, [ActionChoices.upload.value]):
+ self.permission_denied(self.request, error_msg_auth_missing_upload.format(asset=asset.name))
def get_queryset(self):
queryset = super().get_queryset()
- queryset = queryset.filter(creator=self.request.user).exclude(type=Types.upload_file)
+ queryset = queryset \
+ .filter(creator=self.request.user) \
+ .exclude(type=Types.upload_file)
+
+ # Job 列表不显示 adhoc, retrieve 要取状态
if self.action != 'retrieve':
return queryset.filter(instant=False)
return queryset
@@ -79,10 +105,14 @@ class JobViewSet(OrgBulkModelViewSet):
def perform_create(self, serializer):
run_after_save = serializer.validated_data.pop('run_after_save', False)
node_ids = serializer.validated_data.pop('nodes', [])
- assets = serializer.validated_data.__getitem__('assets')
+ assets = serializer.validated_data.get('assets')
assets = merge_nodes_and_assets(node_ids, assets, self.request.user)
- serializer.validated_data.__setitem__('assets', assets)
+ serializer.validated_data['assets'] = assets
+ if serializer.validated_data.get('type') == Types.upload_file:
+ account_name = serializer.validated_data.get('runas')
+ self.check_upload_permission(assets, account_name)
instance = serializer.save()
+
if instance.instant or run_after_save:
self.run_job(instance, serializer)
@@ -99,7 +129,10 @@ class JobViewSet(OrgBulkModelViewSet):
set_task_to_serializer_data(serializer, execution.id)
transaction.on_commit(
- lambda: run_ops_job_execution.apply_async((str(execution.id),), task_id=str(execution.id)))
+ lambda: run_ops_job_execution.apply_async(
+ (str(execution.id),), task_id=str(execution.id)
+ )
+ )
@staticmethod
def get_duplicates_files(files):
@@ -120,8 +153,8 @@ class JobViewSet(OrgBulkModelViewSet):
exceeds_limit_files.append(file)
return exceeds_limit_files
- @action(methods=[POST], detail=False, serializer_class=FileSerializer, permission_classes=[IsValidUser, ],
- url_path='upload')
+ @action(methods=[POST], detail=False, serializer_class=FileSerializer,
+ permission_classes=[IsValidUser, ], url_path='upload')
def upload(self, request, *args, **kwargs):
uploaded_files = request.FILES.getlist('files')
serializer = self.get_serializer(data=request.data)
@@ -142,10 +175,10 @@ class JobViewSet(OrgBulkModelViewSet):
status=400)
job_id = request.data.get('job_id', '')
- job = get_object_or_404(Job, pk=job_id)
+ job = get_object_or_404(Job, pk=job_id, creator=request.user)
job_args = json.loads(job.args)
src_path_info = []
- upload_file_dir = safe_join(settings.DATA_DIR, 'job_upload_file', job_id)
+ upload_file_dir = safe_join(settings.SHARE_DIR, 'job_upload_file', job_id)
for uploaded_file in uploaded_files:
filename = uploaded_file.name
saved_path = safe_join(upload_file_dir, f'{filename}')
@@ -198,37 +231,32 @@ class JobExecutionViewSet(OrgBulkModelViewSet):
return Response({'error': serializer.errors}, status=400)
task_id = serializer.validated_data['task_id']
try:
- instance = get_object_or_404(JobExecution, task_id=task_id, creator=request.user)
+ instance = get_object_or_404(JobExecution, pk=task_id, creator=request.user)
except Http404:
return Response(
{'error': _('The task is being created and cannot be interrupted. Please try again later.')},
status=400
)
+ try:
+ task = AsyncResult(task_id, app=app)
+ inspect = app.control.inspect()
- task = AsyncResult(task_id, app=app)
- inspect = app.control.inspect()
- for worker in inspect.registered().keys():
- if task_id not in [at['id'] for at in inspect.active().get(worker, [])]:
- # 在队列中未执行使用revoke执行
- task.revoke(terminate=True)
- instance.set_error('Job stop by "revoke task {}"'.format(task_id))
- return Response({'task_id': task_id}, status=200)
+ for worker in inspect.registered().keys():
+ if not worker.startswith('ansible'):
+ continue
+ if task_id not in [at['id'] for at in inspect.active().get(worker, [])]:
+ # 在队列中未执行使用revoke执行
+ task.revoke(terminate=True)
+ instance.set_error('Job stop by "revoke task {}"'.format(task_id))
+ return Response({'task_id': task_id}, status=200)
+ except Exception as e:
+ instance.set_error(str(e))
+ return Response({'error': f'Error while stopping the task {task_id}: {e}'}, status=400)
instance.stop()
return Response({'task_id': task_id}, status=200)
-class JobAssetDetail(APIView):
- rbac_perms = {
- 'get': ['ops.view_jobexecution'],
- }
-
- def get(self, request, **kwargs):
- execution_id = request.query_params.get('execution_id', '')
- execution = get_object_or_404(JobExecution, id=execution_id)
- return Response(data=execution.assent_result_detail)
-
-
class JobExecutionTaskDetail(APIView):
rbac_perms = {
'GET': ['ops.view_jobexecution'],
@@ -239,7 +267,7 @@ class JobExecutionTaskDetail(APIView):
task_id = str(kwargs.get('task_id'))
with tmp_to_org(org):
- execution = get_object_or_404(JobExecution, task_id=task_id)
+ execution = get_object_or_404(JobExecution, pk=task_id, creator=request.user)
return Response(data={
'status': execution.status,
diff --git a/apps/ops/api/playbook.py b/apps/ops/api/playbook.py
index 2bf52132c..38e18bdfb 100644
--- a/apps/ops/api/playbook.py
+++ b/apps/ops/api/playbook.py
@@ -52,36 +52,39 @@ class PlaybookViewSet(OrgBulkModelViewSet):
if 'multipart/form-data' in self.request.headers['Content-Type']:
src_path = safe_join(settings.MEDIA_ROOT, instance.path.name)
dest_path = safe_join(settings.DATA_DIR, "ops", "playbook", instance.id.__str__())
+
try:
unzip_playbook(src_path, dest_path)
- except RuntimeError as e:
+ except RuntimeError:
raise JMSException(code='invalid_playbook_file', detail={"msg": "Unzip failed"})
if 'main.yml' not in os.listdir(dest_path):
raise PlaybookNoValidEntry
- else:
- if instance.create_method == 'blank':
- dest_path = safe_join(settings.DATA_DIR, "ops", "playbook", instance.id.__str__())
- os.makedirs(dest_path)
- with open(safe_join(dest_path, 'main.yml'), 'w') as f:
- f.write('## write your playbook here')
+ elif instance.create_method == 'blank':
+ dest_path = safe_join(settings.DATA_DIR, "ops", "playbook", instance.id.__str__())
+ os.makedirs(dest_path)
+ with open(safe_join(dest_path, 'main.yml'), 'w') as f:
+ f.write('## write your playbook here')
class PlaybookFileBrowserAPIView(APIView):
- rbac_perms = ()
permission_classes = (RBACPermission,)
rbac_perms = {
- 'GET': 'ops.change_playbook',
+ 'GET': 'ops.view_playbook',
'POST': 'ops.change_playbook',
'DELETE': 'ops.change_playbook',
'PATCH': 'ops.change_playbook',
}
protected_files = ['root', 'main.yml']
+ def get_playbook(self, playbook_id):
+ playbook = get_object_or_404(Playbook, id=playbook_id, creator=self.request.user)
+ return playbook
+
def get(self, request, **kwargs):
playbook_id = kwargs.get('pk')
- playbook = get_object_or_404(Playbook, id=playbook_id)
+ playbook = self.get_playbook(playbook_id)
work_path = playbook.work_dir
file_key = request.query_params.get('key', '')
if file_key:
@@ -101,7 +104,7 @@ class PlaybookFileBrowserAPIView(APIView):
def post(self, request, **kwargs):
playbook_id = kwargs.get('pk')
- playbook = get_object_or_404(Playbook, id=playbook_id)
+ playbook = self.get_playbook(playbook_id)
work_path = playbook.work_dir
parent_key = request.data.get('key', '')
@@ -157,7 +160,7 @@ class PlaybookFileBrowserAPIView(APIView):
def patch(self, request, **kwargs):
playbook_id = kwargs.get('pk')
- playbook = get_object_or_404(Playbook, id=playbook_id)
+ playbook = self.get_playbook(playbook_id)
work_path = playbook.work_dir
file_key = request.data.get('key', '')
@@ -197,7 +200,7 @@ class PlaybookFileBrowserAPIView(APIView):
def delete(self, request, **kwargs):
playbook_id = kwargs.get('pk')
- playbook = get_object_or_404(Playbook, id=playbook_id)
+ playbook = self.get_playbook(playbook_id)
work_path = playbook.work_dir
file_key = request.query_params.get('key', '')
if not file_key:
diff --git a/apps/ops/celery/beat/schedulers.py b/apps/ops/celery/beat/schedulers.py
deleted file mode 100644
index 17bdcea3c..000000000
--- a/apps/ops/celery/beat/schedulers.py
+++ /dev/null
@@ -1,80 +0,0 @@
-import logging
-from celery.utils.log import get_logger
-from django.db import close_old_connections
-from django.core.exceptions import ObjectDoesNotExist
-from django.db.utils import DatabaseError, InterfaceError
-
-from django_celery_beat.schedulers import DatabaseScheduler as DJDatabaseScheduler
-
-logger = get_logger(__name__)
-debug, info, warning = logger.debug, logger.info, logger.warning
-
-
-__all__ = ['DatabaseScheduler']
-
-
-class DatabaseScheduler(DJDatabaseScheduler):
-
- def sync(self):
- if logger.isEnabledFor(logging.DEBUG):
- debug('Writing entries...')
- _tried = set()
- _failed = set()
- try:
- close_old_connections()
-
- while self._dirty:
- name = self._dirty.pop()
- try:
- # 源码
- # self.schedule[name].save()
- # _tried.add(name)
-
- """
- ::Debug Description (2023.07.10)::
-
- 如果调用 self.schedule 可能会导致 self.save() 方法之前重新获取数据库中的数据, 而不是临时设置的 last_run_at 数据
-
- 如果这里调用 self.schedule
- 那么可能会导致调用 save 的 self.schedule[name] 的 last_run_at 是从数据库中获取回来的老数据
- 而不是任务执行后临时设置的 last_run_at (在 __next__() 方法中设置的)
- 当 `max_interval` 间隔之后, 下一个任务检测周期还是会再次执行任务
-
- ::Demo::
- 任务信息:
- beat config: max_interval = 60s
-
- 任务名称: cap
- 任务执行周期: 每 3 分钟执行一次
- 任务最后执行时间: 18:00
-
- 任务第一次执行: 18:03 (执行时设置 last_run_at = 18:03, 此时在内存中)
-
- 任务执行完成后,
- 检测到需要 sync, sync 中调用了 self.schedule,
- self.schedule 中发现 schedule_changed() 为 True, 需要调用 all_as_schedule()
- 此时,sync 中调用的 self.schedule[name] 的 last_run_at 是 18:00
- 这时候在 self.sync() 进行 self.save()
-
-
- beat: Waking up 60s ...
-
- 任务第二次执行: 18:04 (因为获取回来的 last_run_at 是 18:00, entry.is_due() == True)
-
- ::解决方法::
- 所以这里为了避免从数据库中获取,直接使用 _schedule #
- """
- self._schedule[name].save()
- _tried.add(name)
- except (KeyError, TypeError, ObjectDoesNotExist):
- _failed.add(name)
- except DatabaseError as exc:
- logger.exception('Database error while sync: %r', exc)
- except InterfaceError:
- warning(
- 'DatabaseScheduler: InterfaceError in sync(), '
- 'waiting to retry in next call...'
- )
- finally:
- # retry later, only for the failed ones
- self._dirty |= _failed
diff --git a/apps/ops/celery/signal_handler.py b/apps/ops/celery/signal_handler.py
index bfd68508a..4ce8d0f38 100644
--- a/apps/ops/celery/signal_handler.py
+++ b/apps/ops/celery/signal_handler.py
@@ -26,6 +26,10 @@ def on_app_ready(sender=None, headers=None, **kwargs):
logger.debug("Work ready signal recv")
logger.debug("Start need start task: [{}]".format(", ".join(tasks)))
for task in tasks:
+ periodic_task = PeriodicTask.objects.filter(task=task).first()
+ if periodic_task and not periodic_task.enabled:
+ logger.debug("Periodic task [{}] is disabled!".format(task))
+ continue
subtask(task).delay()
diff --git a/apps/ops/celery/utils.py b/apps/ops/celery/utils.py
index 3c38ee287..580a256fe 100644
--- a/apps/ops/celery/utils.py
+++ b/apps/ops/celery/utils.py
@@ -75,7 +75,6 @@ def create_or_update_celery_periodic_tasks(tasks):
crontab=crontab,
name=name,
task=detail['task'],
- enabled=detail.get('enabled', True),
args=json.dumps(detail.get('args', [])),
kwargs=json.dumps(detail.get('kwargs', {})),
description=detail.get('description') or '',
diff --git a/apps/ops/const.py b/apps/ops/const.py
index 578697c48..61f0b5e32 100644
--- a/apps/ops/const.py
+++ b/apps/ops/const.py
@@ -54,6 +54,7 @@ class JobModules(models.TextChoices):
postgresql = 'postgresql', _('PostgreSQL')
sqlserver = 'sqlserver', _('SQLServer')
raw = 'raw', _('Raw')
+ huawei = 'huawei', _('HUAWEI')
class AdHocModules(models.TextChoices):
diff --git a/apps/ops/migrations/0026_auto_20230810_1039.py b/apps/ops/migrations/0026_auto_20230810_1039.py
index 99ffdf7d5..944a61e6e 100644
--- a/apps/ops/migrations/0026_auto_20230810_1039.py
+++ b/apps/ops/migrations/0026_auto_20230810_1039.py
@@ -1,8 +1,9 @@
# Generated by Django 4.1.10 on 2023-08-10 02:36
-import common.db.encoder
from django.db import migrations, models
+import common.db.encoder
+
class Migration(migrations.Migration):
@@ -19,12 +20,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='historicaljob',
name='module',
- field=models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell'), ('python', 'Python'), ('mysql', 'MySQL'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('raw', 'Raw')], default='shell', max_length=128, null=True, verbose_name='Module'),
+ field=models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell'), ('python', 'Python'), ('mysql', 'MySQL'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('raw', 'Raw'), ('huawei', 'HUAWEI')], default='shell', max_length=128, null=True, verbose_name='Module'),
),
migrations.AlterField(
model_name='job',
name='module',
- field=models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell'), ('python', 'Python'), ('mysql', 'MySQL'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('raw', 'Raw')], default='shell', max_length=128, null=True, verbose_name='Module'),
+ field=models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell'), ('python', 'Python'), ('mysql', 'MySQL'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('raw', 'Raw'), ('huawei', 'HUAWEI')], default='shell', max_length=128, null=True, verbose_name='Module'),
),
migrations.AlterField(
model_name='jobexecution',
diff --git a/apps/ops/migrations/0028_celerytaskexecution_creator.py b/apps/ops/migrations/0028_celerytaskexecution_creator.py
new file mode 100644
index 000000000..37d44077b
--- /dev/null
+++ b/apps/ops/migrations/0028_celerytaskexecution_creator.py
@@ -0,0 +1,21 @@
+# Generated by Django 4.1.13 on 2024-03-18 06:47
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('ops', '0027_alter_celerytaskexecution_options'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='celerytaskexecution',
+ name='creator',
+ field=models.ForeignKey(db_constraint=False, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Creator'),
+ ),
+ ]
diff --git a/apps/ops/models/celery.py b/apps/ops/models/celery.py
index ef1fab463..9e40cd921 100644
--- a/apps/ops/models/celery.py
+++ b/apps/ops/models/celery.py
@@ -82,6 +82,8 @@ class CeleryTaskExecution(models.Model):
kwargs = models.JSONField(verbose_name=_("Kwargs"))
state = models.CharField(max_length=16, verbose_name=_("State"))
is_finished = models.BooleanField(default=False, verbose_name=_("Finished"))
+ creator = models.ForeignKey('users.User', on_delete=models.SET_NULL, default=None, null=True,
+ verbose_name=_('Creator'), db_constraint=False)
date_published = models.DateTimeField(auto_now_add=True, verbose_name=_('Date published'))
date_start = models.DateTimeField(null=True, verbose_name=_('Date start'))
date_finished = models.DateTimeField(null=True, verbose_name=_('Date finished'))
diff --git a/apps/ops/models/job.py b/apps/ops/models/job.py
index f7831251b..e7245568b 100644
--- a/apps/ops/models/job.py
+++ b/apps/ops/models/job.py
@@ -23,6 +23,7 @@ from assets.models import Asset
from assets.automations.base.manager import SSHTunnelManager
from common.db.encoder import ModelJSONFieldEncoder
from ops.ansible import JMSInventory, AdHocRunner, PlaybookRunner, CommandInBlackListException, UploadFileRunner
+from ops.ansible.receptor import receptor_runner
from ops.mixin import PeriodTaskModelMixin
from ops.variables import *
from ops.const import Types, RunasPolicies, JobStatus, JobModules
@@ -57,7 +58,7 @@ class JMSPermedInventory(JMSInventory):
self.module = module
self.assets_accounts_mapper = self.get_assets_accounts_mapper()
- def make_account_vars(self, host, asset, account, automation, protocol, platform, gateway):
+ def make_account_vars(self, host, asset, account, automation, protocol, platform, gateway, path_dir):
if not account:
host['error'] = _("No account available")
return host
@@ -66,7 +67,7 @@ class JMSPermedInventory(JMSInventory):
'mysql': ['mysql'],
'postgresql': ['postgresql'],
'sqlserver': ['sqlserver'],
- 'ssh': ['shell', 'python', 'win_shell', 'raw'],
+ 'ssh': ['shell', 'python', 'win_shell', 'raw', 'huawei'],
'winrm': ['win_shell', 'shell'],
}
@@ -89,11 +90,12 @@ class JMSPermedInventory(JMSInventory):
}
host['jms_asset']['port'] = protocol.port
return host
- return super().make_account_vars(host, asset, account, automation, protocol, platform, gateway)
+ return super().make_account_vars(host, asset, account, automation, protocol, platform, gateway, path_dir)
def get_asset_sorted_accounts(self, asset):
accounts = self.assets_accounts_mapper.get(asset.id, [])
- return list(accounts)
+ accounts_sorted = self.sorted_accounts(accounts)
+ return list(accounts_sorted)
def get_assets_accounts_mapper(self):
mapper = defaultdict(set)
@@ -254,45 +256,6 @@ class JobExecution(JMSOrgBaseModel):
return self.job.get_history(self.job_version)
return self.job
- @property
- def assent_result_detail(self):
- if not self.is_finished or self.summary.get('error'):
- return None
- result = {
- "summary": self.summary,
- "detail": [],
- }
- for asset in self.current_job.assets.all():
- asset_detail = {
- "name": asset.name,
- "status": "ok",
- "tasks": [],
- }
- if self.summary.get("excludes", None) and self.summary["excludes"].get(asset.name, None):
- asset_detail.update({"status": "excludes"})
- result["detail"].append(asset_detail)
- break
- if self.result["dark"].get(asset.name, None):
- asset_detail.update({"status": "failed"})
- for key, task in self.result["dark"][asset.name].items():
- task_detail = {"name": key,
- "output": "{}{}".format(task.get("stdout", ""), task.get("stderr", ""))}
- asset_detail["tasks"].append(task_detail)
- if self.result["failures"].get(asset.name, None):
- asset_detail.update({"status": "failed"})
- for key, task in self.result["failures"][asset.name].items():
- task_detail = {"name": key,
- "output": "{}{}".format(task.get("stdout", ""), task.get("stderr", ""))}
- asset_detail["tasks"].append(task_detail)
-
- if self.result["ok"].get(asset.name, None):
- for key, task in self.result["ok"][asset.name].items():
- task_detail = {"name": key,
- "output": "{}{}".format(task.get("stdout", ""), task.get("stderr", ""))}
- asset_detail["tasks"].append(task_detail)
- result["detail"].append(asset_detail)
- return result
-
def compile_shell(self):
if self.current_job.type != 'adhoc':
return
@@ -340,6 +303,11 @@ class JobExecution(JMSOrgBaseModel):
shell += " chdir={}".format(self.current_job.chdir)
if self.current_job.module in ['python']:
shell += " executable={}".format(self.current_job.module)
+
+ if module == JobModules.huawei.value:
+ module = 'ce_command'
+ shell = "commands=\"{}\" ".format(self.current_job.args)
+
return module, shell
def get_runner(self):
@@ -372,13 +340,15 @@ class JobExecution(JMSOrgBaseModel):
)
elif self.current_job.type == Types.playbook:
runner = PlaybookRunner(
- self.inventory_path, self.current_job.playbook.entry
+ self.inventory_path,
+ self.current_job.playbook.entry,
+ self.private_dir
)
elif self.current_job.type == Types.upload_file:
job_id = self.current_job.id
args = json.loads(self.current_job.args)
dst_path = args.get('dst_path', '/')
- runner = UploadFileRunner(self.inventory_path, job_id, dst_path)
+ runner = UploadFileRunner(self.inventory_path, self.private_dir, job_id, dst_path)
else:
raise Exception("unsupported job type")
return runner
@@ -555,13 +525,16 @@ class JobExecution(JMSOrgBaseModel):
ssh_tunnel.local_gateway_clean(runner)
def stop(self):
- with open(os.path.join(self.private_dir, 'local.pid')) as f:
- try:
- pid = f.read()
- os.kill(int(pid), 9)
- except Exception as e:
- print(e)
- self.set_error('Job stop by "kill -9 {}"'.format(pid))
+ from ops.signal_handlers import job_execution_stop_pub_sub
+ pid_path = os.path.join(self.private_dir, "local.pid")
+ if os.path.exists(pid_path):
+ with open(pid_path) as f:
+ try:
+ pid = f.read()
+ job_execution_stop_pub_sub.publish(int(pid))
+ except Exception as e:
+ print(e)
+ self.set_error('Job stop by "user cancel"')
class Meta:
verbose_name = _("Job Execution")
diff --git a/apps/ops/notifications.py b/apps/ops/notifications.py
index b0c4906e4..b7b8a0164 100644
--- a/apps/ops/notifications.py
+++ b/apps/ops/notifications.py
@@ -1,13 +1,14 @@
-from django.utils.translation import gettext_lazy as _
-from django.utils.translation import gettext
from django.template.loader import render_to_string
+from django.utils.translation import gettext
+from django.utils.translation import gettext_lazy as _
-from notifications.notifications import SystemMessage
-from notifications.models import SystemMsgSubscription
-from users.models import User
from notifications.backends import BACKEND
-from terminal.models.component.status import Status
+from notifications.models import SystemMsgSubscription
+from notifications.notifications import SystemMessage
+from terminal.const import TerminalType
from terminal.models import Terminal
+from terminal.models.component.status import Status
+from users.models import User
__all__ = ('ServerPerformanceMessage', 'ServerPerformanceCheckUtil')
@@ -133,7 +134,9 @@ class ServerPerformanceCheckUtil(object):
def initial_terminals(self):
terminals = []
- for terminal in Terminal.objects.filter(is_deleted=False):
+ for terminal in Terminal.objects.filter(is_deleted=False).exclude(
+ type__in=[TerminalType.core, TerminalType.celery]
+ ):
if not terminal.is_active:
continue
terminal.stat = Status.get_terminal_latest_stat(terminal)
diff --git a/apps/ops/serializers/celery.py b/apps/ops/serializers/celery.py
index a4e9c7e8d..aeaabd783 100644
--- a/apps/ops/serializers/celery.py
+++ b/apps/ops/serializers/celery.py
@@ -23,21 +23,21 @@ class CeleryResultSerializer(serializers.Serializer):
class CeleryPeriodTaskSerializer(serializers.ModelSerializer):
class Meta:
model = PeriodicTask
- fields = [
- 'name', 'task', 'enabled', 'description',
- 'last_run_at', 'total_run_count'
- ]
+ read_only_fields = ['name', 'task', 'description',
+ 'last_run_at', 'total_run_count']
+ fields = ['enabled'] + read_only_fields
class CeleryTaskSerializer(serializers.ModelSerializer):
exec_cycle = serializers.CharField(read_only=True)
next_exec_time = serializers.DateTimeField(format="%Y/%m/%d %H:%M:%S", read_only=True)
+ enabled = serializers.BooleanField(required=False)
class Meta:
model = CeleryTask
read_only_fields = [
'id', 'name', 'meta', 'summary', 'state',
- 'date_last_publish', 'exec_cycle', 'next_exec_time'
+ 'date_last_publish', 'exec_cycle', 'next_exec_time', 'enabled'
]
fields = read_only_fields
diff --git a/apps/ops/serializers/job.py b/apps/ops/serializers/job.py
index ce4faee55..915e19575 100644
--- a/apps/ops/serializers/job.py
+++ b/apps/ops/serializers/job.py
@@ -81,3 +81,8 @@ class JobExecutionSerializer(BulkOrgResourceModelSerializer):
fields = read_only_fields + [
"job", "parameters", "creator"
]
+
+ def validate_job(self, job_obj):
+ if job_obj.creator != self.context['request'].user:
+ raise serializers.ValidationError(_("You do not have permission for the current job."))
+ return job_obj
diff --git a/apps/ops/signal_handlers.py b/apps/ops/signal_handlers.py
index 417561111..6fb2ced2d 100644
--- a/apps/ops/signal_handlers.py
+++ b/apps/ops/signal_handlers.py
@@ -1,4 +1,5 @@
import ast
+import json
import time
from celery import signals
@@ -8,10 +9,15 @@ from django.db.models.signals import pre_save
from django.db.utils import ProgrammingError
from django.dispatch import receiver
from django.utils import translation, timezone
+from django.utils.functional import LazyObject
+from rest_framework.utils.encoders import JSONEncoder
from common.db.utils import close_old_connections, get_logger
from common.signals import django_ready
+from common.utils.connection import RedisPubSub
+from jumpserver.utils import get_current_request
from orgs.utils import get_current_org_id, set_current_org
+from .ansible.receptor.receptor_runner import receptor_ctl
from .celery import app
from .models import CeleryTaskExecution, CeleryTask, Job
@@ -125,12 +131,12 @@ def task_sent_handler(headers=None, body=None, **kwargs):
logger.error("Not found task id or name: {}".format(info))
return
- args = info.get('argsrepr', '()')
- kwargs = info.get('kwargsrepr', '{}')
+ args, kwargs, __ = body
+
try:
- args = list(ast.literal_eval(args))
- kwargs = ast.literal_eval(kwargs)
- except (ValueError, SyntaxError):
+ args = list(args)
+ kwargs = json.loads(json.dumps(kwargs, cls=JSONEncoder))
+ except Exception as e:
args = []
kwargs = {}
@@ -142,5 +148,30 @@ def task_sent_handler(headers=None, body=None, **kwargs):
'args': args,
'kwargs': kwargs
}
- CeleryTaskExecution.objects.create(**data)
+ request = get_current_request()
+ if request and request.user.is_authenticated:
+ data['creator'] = request.user
+ try:
+ CeleryTaskExecution.objects.create(**data)
+ except Exception as e:
+ logger.error(e)
CeleryTask.objects.filter(name=task).update(date_last_publish=timezone.now())
+
+
+@receiver(django_ready)
+def subscribe_stop_job_execution(sender, **kwargs):
+ logger.info("Start subscribe for stop job execution")
+
+ def on_stop(pid):
+ logger.info(f"Stop job execution {pid} start")
+ receptor_ctl.kill_process(pid)
+
+ job_execution_stop_pub_sub.subscribe(on_stop)
+
+
+class JobExecutionPubSub(LazyObject):
+ def _setup(self):
+ self._wrapped = RedisPubSub('fm.job_execution_stop')
+
+
+job_execution_stop_pub_sub = JobExecutionPubSub()
diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py
index 5242d2812..3da3b726e 100644
--- a/apps/ops/tasks.py
+++ b/apps/ops/tasks.py
@@ -1,10 +1,11 @@
# coding: utf-8
import datetime
+import time
from celery import shared_task
from celery.exceptions import SoftTimeLimitExceeded
-from django.utils.translation import gettext_lazy as _
from django.utils import timezone
+from django.utils.translation import gettext_lazy as _
from django_celery_beat.models import PeriodicTask
from common.const.crontab import CRONTAB_AT_AM_TWO
@@ -134,7 +135,6 @@ def clean_up_unexpected_jobs():
@shared_task(verbose_name=_('Clean job_execution db record'))
@register_as_period_task(crontab=CRONTAB_AT_AM_TWO)
-@after_app_shutdown_clean_periodic
def clean_job_execution_period():
logger.info("Start clean job_execution db record")
now = timezone.now()
@@ -143,3 +143,11 @@ def clean_job_execution_period():
with tmp_to_root_org():
del_res = JobExecution.objects.filter(date_created__lt=expired_day).delete()
logger.info(f"clean job_execution db record success! delete {days} days {del_res[0]} records")
+
+
+@shared_task
+def longtime_add(x, y):
+ print('long time task begins')
+ time.sleep(50)
+ print('long time task finished')
+ return x + y
diff --git a/apps/ops/urls/api_urls.py b/apps/ops/urls/api_urls.py
index 905f0ed0a..a02f582fa 100644
--- a/apps/ops/urls/api_urls.py
+++ b/apps/ops/urls/api_urls.py
@@ -25,7 +25,6 @@ router.register(r'task-executions', api.CeleryTaskExecutionViewSet, 'task-execut
urlpatterns = [
path('playbook//file/', api.PlaybookFileBrowserAPIView.as_view(), name='playbook-file'),
path('variables/help/', api.JobRunVariableHelpAPIView.as_view(), name='variable-help'),
- path('job-execution/asset-detail/', api.JobAssetDetail.as_view(), name='asset-detail'),
path('job-execution/task-detail//', api.JobExecutionTaskDetail.as_view(), name='task-detail'),
path('username-hints/', api.UsernameHintsAPI.as_view(), name='username-hints'),
path('ansible/job-execution//log/', api.AnsibleTaskLogApi.as_view(), name='job-execution-log'),
diff --git a/apps/ops/ws.py b/apps/ops/ws.py
index c2386c77a..9605835a0 100644
--- a/apps/ops/ws.py
+++ b/apps/ops/ws.py
@@ -2,6 +2,7 @@ import asyncio
import os
import aiofiles
+from asgiref.sync import sync_to_async
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from common.db.utils import close_old_connections
@@ -9,12 +10,17 @@ from common.utils import get_logger
from .ansible.utils import get_ansible_task_log_path
from .celery.utils import get_celery_task_log_path
from .const import CELERY_LOG_MAGIC_MARK
+from .models import CeleryTaskExecution
logger = get_logger(__name__)
class TaskLogWebsocket(AsyncJsonWebsocketConsumer):
disconnected = False
+ user_tasks = (
+ 'ops.tasks.run_ops_job',
+ 'ops.tasks.run_ops_job_execution',
+ )
log_types = {
'celery': get_celery_task_log_path,
@@ -33,17 +39,41 @@ class TaskLogWebsocket(AsyncJsonWebsocketConsumer):
if func:
return func(task_id)
+ @sync_to_async
+ def get_task(self, task_id):
+ task = CeleryTaskExecution.objects.filter(id=task_id).first()
+ # task.creator 是 foreign key, 会异步去查询的,在下面的 if task.creator 会报错, 所以这里先取出来
+ if task and task.creator != ' ':
+ return task
+ else:
+ return None
+
async def receive_json(self, content, **kwargs):
task_id = content.get('task')
- task_typ = content.get('type', 'celery')
- log_path = self.get_log_path(task_id, task_typ)
+ task = await self.get_task(task_id)
+ if not task:
+ await self.send_json({'message': 'Task not found', 'task': task_id})
+ return
+ if task.name in self.user_tasks and task.creator != self.scope['user']:
+ await self.send_json({'message': 'No permission', 'task': task_id})
+ return
+
+ task_type = content.get('type', 'celery')
+ log_path = self.get_log_path(task_id, task_type)
await self.async_handle_task(task_id, log_path)
async def async_handle_task(self, task_id, log_path):
logger.info("Task id: {}".format(task_id))
+ timeout = 0
while not self.disconnected:
+ if timeout >= 60:
+ await self.send_json({'message': '\r\n', 'task': task_id})
+ await self.send_json({'message': 'Task log was not found, the directory may not be shared.',
+ 'task': task_id})
+ break
if not os.path.exists(log_path):
await self.send_json({'message': '.', 'task': task_id})
+ timeout += 0.5
await asyncio.sleep(0.5)
else:
await self.send_task_log(task_id, log_path)
diff --git a/apps/orgs/api.py b/apps/orgs/api.py
index b10c3454e..eb1a980d6 100644
--- a/apps/orgs/api.py
+++ b/apps/orgs/api.py
@@ -34,7 +34,6 @@ class OrgViewSet(JMSBulkModelViewSet):
search_fields = ('name', 'comment')
queryset = Organization.objects.all()
serializer_class = OrgSerializer
- ordering = ('name',)
def get_serializer_class(self):
mapper = {
diff --git a/apps/perms/api/asset_permission.py b/apps/perms/api/asset_permission.py
index c72254d0d..b55fdf570 100644
--- a/apps/perms/api/asset_permission.py
+++ b/apps/perms/api/asset_permission.py
@@ -20,4 +20,3 @@ class AssetPermissionViewSet(OrgBulkModelViewSet):
}
filterset_class = AssetPermissionFilter
search_fields = ('name',)
- ordering = ('name',)
diff --git a/apps/perms/api/user_permission/assets.py b/apps/perms/api/user_permission/assets.py
index 676a21e5e..22c187e3e 100644
--- a/apps/perms/api/user_permission/assets.py
+++ b/apps/perms/api/user_permission/assets.py
@@ -42,7 +42,7 @@ class UserPermedAssetRetrieveApi(SelfOrPKUserMixin, RetrieveAPIView):
class BaseUserPermedAssetsApi(SelfOrPKUserMixin, ExtraFilterFieldsMixin, ListAPIView):
ordering = []
search_fields = ('name', 'address', 'comment')
- ordering_fields = ("name", "address")
+ ordering_fields = ("name", "address", "connectivity")
filterset_class = AssetFilterSet
serializer_class = serializers.AssetPermedSerializer
diff --git a/apps/perms/api/user_permission/tree/asset.py b/apps/perms/api/user_permission/tree/asset.py
index e5e2ab4d3..ee0cb72ff 100644
--- a/apps/perms/api/user_permission/tree/asset.py
+++ b/apps/perms/api/user_permission/tree/asset.py
@@ -20,7 +20,6 @@ class AssetTreeMixin(RebuildTreeMixin, SerializeToTreeNodeMixin):
filter_queryset: callable
get_queryset: callable
- ordering = ('name',)
filterset_fields = ('id', 'name', 'address', 'comment')
search_fields = ('name', 'address', 'comment')
diff --git a/apps/perms/const.py b/apps/perms/const.py
index 1e2851b3d..603824e19 100644
--- a/apps/perms/const.py
+++ b/apps/perms/const.py
@@ -42,6 +42,10 @@ class ActionChoices(BitChoices):
def contains(cls, total, action_value):
return action_value & total == action_value
+ @classmethod
+ def contains_all(cls, total, action_values):
+ return all(cls.contains(total, action) for action in action_values)
+
@classmethod
def display(cls, value):
return ', '.join([str(c.label) for c in cls if c.value & value == c.value])
diff --git a/apps/perms/serializers/permission.py b/apps/perms/serializers/permission.py
index 35d6ecacd..74a7fac26 100644
--- a/apps/perms/serializers/permission.py
+++ b/apps/perms/serializers/permission.py
@@ -190,8 +190,8 @@ class AssetPermissionListSerializer(AssetPermissionSerializer):
class Meta(AssetPermissionSerializer.Meta):
amount_fields = ["users_amount", "user_groups_amount", "assets_amount", "nodes_amount"]
- remove_fields = {"users", "assets", "nodes", "user_groups"}
- fields = list(set(AssetPermissionSerializer.Meta.fields + amount_fields) - remove_fields)
+ fields = [item for item in (AssetPermissionSerializer.Meta.fields + amount_fields) if
+ item not in ["users", "assets", "nodes", "user_groups"]]
@classmethod
def setup_eager_loading(cls, queryset):
diff --git a/apps/perms/signal_handlers/refresh_perms.py b/apps/perms/signal_handlers/refresh_perms.py
index 5387bc33f..7220bceba 100644
--- a/apps/perms/signal_handlers/refresh_perms.py
+++ b/apps/perms/signal_handlers/refresh_perms.py
@@ -113,7 +113,7 @@ def on_asset_permission_user_groups_changed(sender, instance, action, pk_set, re
def on_node_asset_change(action, instance, reverse, pk_set, **kwargs):
if not need_rebuild_mapping_node(action):
return
- print("Asset node changed: ", action)
+
if reverse:
asset_ids = pk_set
node_ids = [instance.id]
diff --git a/apps/perms/utils/asset_perm.py b/apps/perms/utils/asset_perm.py
index 178a5ab21..6de84121f 100644
--- a/apps/perms/utils/asset_perm.py
+++ b/apps/perms/utils/asset_perm.py
@@ -6,6 +6,7 @@ from assets.models import Asset
from common.utils import lazyproperty
from orgs.utils import tmp_to_org, tmp_to_root_org
from .permission import AssetPermissionUtil
+from perms.const import ActionChoices
__all__ = ['PermAssetDetailUtil']
@@ -137,3 +138,23 @@ class PermAssetDetailUtil:
account.date_expired = max(cleaned_accounts_expired[account])
accounts.append(account)
return accounts
+
+ def check_perm_protocols(self, protocols):
+ """
+ 检查用户是否有某些协议权限
+ :param protocols: set
+ """
+ perms_protocols = self.get_permed_protocols_for_user(only_name=True)
+ if "all" in perms_protocols:
+ return True
+ return protocols.intersection(perms_protocols)
+
+ def check_perm_actions(self, account_name, actions):
+ """
+ 检查用户是否有某个账号的某个资产操作权限
+ :param account_name: str
+ :param actions: list
+ """
+ perms = self.user_asset_perms
+ action_bit_mapper, __ = self.parse_alias_action_date_expire(perms, self.asset)
+ return ActionChoices.contains_all(action_bit_mapper.get(account_name, 0), actions)
diff --git a/apps/perms/utils/user_perm.py b/apps/perms/utils/user_perm.py
index 1fc4e86bf..add9d8baf 100644
--- a/apps/perms/utils/user_perm.py
+++ b/apps/perms/utils/user_perm.py
@@ -9,7 +9,7 @@ from rest_framework.utils.encoders import JSONEncoder
from assets.const import AllTypes
from assets.models import FavoriteAsset, Asset, Node
from common.utils.common import timeit, get_logger
-from orgs.utils import current_org, tmp_to_root_org
+from orgs.utils import current_org
from perms.models import PermNode, UserAssetGrantedTreeNodeRelation, AssetPermission
from .permission import AssetPermissionUtil
@@ -112,11 +112,10 @@ class UserPermAssetUtil(AssetPermissionPermAssetUtil):
favor_ids = FavoriteAsset.objects.filter(user=self.user).values_list('asset_id', flat=True)
favor_ids = set(favor_ids)
- with tmp_to_root_org():
- valid_ids = self.get_all_assets() \
- .filter(id__in=favor_ids) \
- .values_list('id', flat=True)
- valid_ids = set(valid_ids)
+ valid_ids = self.get_all_assets() \
+ .filter(id__in=favor_ids) \
+ .values_list('id', flat=True)
+ valid_ids = set(valid_ids)
invalid_ids = favor_ids - valid_ids
FavoriteAsset.objects.filter(user=self.user, asset_id__in=invalid_ids).delete()
diff --git a/apps/perms/utils/user_perm_tree.py b/apps/perms/utils/user_perm_tree.py
index 01090176e..391b205a5 100644
--- a/apps/perms/utils/user_perm_tree.py
+++ b/apps/perms/utils/user_perm_tree.py
@@ -84,7 +84,8 @@ class UserPermTreeRefreshUtil(_UserPermTreeCacheMixin):
logger.info("Delay refresh user orgs: {} {}".format(self.user, [o.name for o in to_refresh_orgs]))
sync = True if settings.ASSET_SIZE == 'small' else False
refresh_user_orgs_perm_tree.apply(sync=sync, user_orgs=((self.user, tuple(to_refresh_orgs)),))
- refresh_user_favorite_assets.apply(sync=sync, users=(self.user,))
+ with tmp_to_root_org():
+ refresh_user_favorite_assets.apply(sync=sync, users=(self.user,))
@timeit
def refresh_tree_manual(self):
diff --git a/apps/rbac/api/role.py b/apps/rbac/api/role.py
index 022a2ac05..2b61b59ec 100644
--- a/apps/rbac/api/role.py
+++ b/apps/rbac/api/role.py
@@ -79,8 +79,11 @@ class RoleViewSet(JMSModelViewSet):
ids = [role.id for role in queryset]
queryset = Role.objects.filter(id__in=ids).order_by(*self.ordering)
org_id = current_org.id
- q = Q(role__scope=Role.Scope.system) | Q(role__scope=Role.Scope.org, org_id=org_id)
- role_bindings = RoleBinding.objects.filter(q).values_list('role_id').annotate(
+ if current_org.is_root():
+ q = Q(role__scope=Role.Scope.system) | Q(role__scope=Role.Scope.org)
+ else:
+ q = Q(role__scope=Role.Scope.system) | Q(role__scope=Role.Scope.org, org_id=org_id)
+ role_bindings = RoleBinding.objects_raw.filter(q).values_list('role_id').annotate(
user_count=Count('user_id', distinct=True)
)
role_user_amount_mapper = {role_id: user_count for role_id, user_count in role_bindings}
diff --git a/apps/rbac/models/rolebinding.py b/apps/rbac/models/rolebinding.py
index e9d636354..ee019f34b 100644
--- a/apps/rbac/models/rolebinding.py
+++ b/apps/rbac/models/rolebinding.py
@@ -56,6 +56,7 @@ class RoleBinding(JMSBaseModel):
on_delete=models.CASCADE, verbose_name=_('Organization')
)
objects = RoleBindingManager()
+ objects_raw = models.Manager()
class Meta:
verbose_name = _('Role binding')
@@ -152,13 +153,15 @@ class RoleBinding(JMSBaseModel):
orgs = cls.orgs_order_by_name(orgs)
workbench_perm = 'rbac.view_workbench'
# 全局组织
+ has_root_org = False
+ root_org = Organization.root()
if orgs and perm != workbench_perm and user.has_perm('orgs.view_rootorg'):
- root_org = Organization.root()
- orgs = [root_org, *list(orgs)]
+ has_root_org = True
elif orgs and perm == workbench_perm and user.has_perm('orgs.view_alljoinedorg'):
- # Todo: 先复用组织
- root_org = Organization.root()
- root_org.name = _("All organizations")
+ root_org.name = _('All organizations')
+ has_root_org = True
+
+ if has_root_org and system_bindings:
orgs = [root_org, *list(orgs)]
return orgs
diff --git a/apps/settings/api/__init__.py b/apps/settings/api/__init__.py
index 686a52f3b..b30c60c59 100644
--- a/apps/settings/api/__init__.py
+++ b/apps/settings/api/__init__.py
@@ -2,6 +2,7 @@ from .chat import *
from .dingtalk import *
from .email import *
from .feishu import *
+from .lark import *
from .ldap import *
from .public import *
from .security import *
diff --git a/apps/settings/api/feishu.py b/apps/settings/api/feishu.py
index ed3b51f9e..1f639f66c 100644
--- a/apps/settings/api/feishu.py
+++ b/apps/settings/api/feishu.py
@@ -1,16 +1,16 @@
-from rest_framework.views import Response
-from rest_framework.generics import GenericAPIView
-from rest_framework.exceptions import APIException
-from rest_framework import status
from django.utils.translation import gettext_lazy as _
+from rest_framework import status
+from rest_framework.exceptions import APIException
+from rest_framework.generics import GenericAPIView
+from rest_framework.views import Response
-from settings.models import Setting
from common.sdk.im.feishu import FeiShu
-
+from settings.models import Setting
from .. import serializers
class FeiShuTestingAPI(GenericAPIView):
+ category = 'FEISHU'
serializer_class = serializers.FeiShuSettingSerializer
rbac_perms = {
'POST': 'settings.change_auth'
@@ -20,11 +20,11 @@ class FeiShuTestingAPI(GenericAPIView):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
- app_id = serializer.validated_data['FEISHU_APP_ID']
- app_secret = serializer.validated_data.get('FEISHU_APP_SECRET')
+ app_id = serializer.validated_data[f'{self.category}_APP_ID']
+ app_secret = serializer.validated_data.get(f'{self.category}_APP_SECRET')
if not app_secret:
- secret = Setting.objects.filter(name='FEISHU_APP_SECRET').first()
+ secret = Setting.objects.filter(name=f'{self.category}_APP_SECRET').first()
if secret:
app_secret = secret.cleaned_value
@@ -40,3 +40,8 @@ class FeiShuTestingAPI(GenericAPIView):
except:
error = e.detail
return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error})
+
+
+class LarkTestingAPI(FeiShuTestingAPI):
+ category = 'LARK'
+ serializer_class = serializers.LarkSettingSerializer
diff --git a/apps/settings/api/lark.py b/apps/settings/api/lark.py
new file mode 100644
index 000000000..6a7fb2cc7
--- /dev/null
+++ b/apps/settings/api/lark.py
@@ -0,0 +1,7 @@
+from .feishu import FeiShuTestingAPI
+from .. import serializers
+
+
+class LarkTestingAPI(FeiShuTestingAPI):
+ category = 'LARK'
+ serializer_class = serializers.LarkSettingSerializer
diff --git a/apps/settings/api/settings.py b/apps/settings/api/settings.py
index c03e99b6c..a801ab2f8 100644
--- a/apps/settings/api/settings.py
+++ b/apps/settings/api/settings.py
@@ -39,6 +39,7 @@ class SettingsApi(generics.RetrieveUpdateAPIView):
'wecom': serializers.WeComSettingSerializer,
'dingtalk': serializers.DingTalkSettingSerializer,
'feishu': serializers.FeiShuSettingSerializer,
+ 'lark': serializers.LarkSettingSerializer,
'slack': serializers.SlackSettingSerializer,
'auth': serializers.AuthSettingSerializer,
'oidc': serializers.OIDCSettingSerializer,
diff --git a/apps/settings/migrations/0013_auto_20240326_1531.py b/apps/settings/migrations/0013_auto_20240326_1531.py
new file mode 100644
index 000000000..59e6a7f34
--- /dev/null
+++ b/apps/settings/migrations/0013_auto_20240326_1531.py
@@ -0,0 +1,42 @@
+# Generated by Django 4.1.13 on 2024-03-26 07:31
+import json
+
+from django.db import migrations
+from django.db.models import F
+
+
+def migrate_feishu_to_lark(apps, schema_editor):
+ setting_model = apps.get_model("settings", "Setting")
+ user_model = apps.get_model("users", "User")
+ db_alias = schema_editor.connection.alias
+
+ feishu_version_instance = setting_model.objects.using(db_alias).filter(name='FEISHU_VERSION').first()
+ if not feishu_version_instance or json.loads(feishu_version_instance.value) == 'feishu':
+ return
+ feishu_version_instance.delete()
+
+ user_model.objects.using(db_alias).filter(feishu_id__isnull=False).update(lark_id=F('feishu_id'))
+ user_model.objects.filter(feishu_id__isnull=False).update(lark_id=F('feishu_id'))
+ user_model.objects.filter(feishu_id__isnull=False).update(feishu_id=None)
+
+ settings_to_update = [
+ ('AUTH_FEISHU', 'AUTH_LARK'),
+ ('FEISHU_APP_ID', 'LARK_APP_ID'),
+ ('FEISHU_APP_SECRET', 'LARK_APP_SECRET'),
+ ]
+
+ for old_name, new_name in settings_to_update:
+ setting_model.objects.using(db_alias).filter(
+ name=old_name
+ ).update(name=new_name)
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('settings', '0012_alter_setting_options'),
+ ('users', '0050_user_lark_id_alter_user_source'),
+ ]
+
+ operations = [
+ migrations.RunPython(migrate_feishu_to_lark),
+ ]
diff --git a/apps/settings/serializers/auth/__init__.py b/apps/settings/serializers/auth/__init__.py
index 2b641114c..b5f567618 100644
--- a/apps/settings/serializers/auth/__init__.py
+++ b/apps/settings/serializers/auth/__init__.py
@@ -2,13 +2,14 @@ from .base import *
from .cas import *
from .dingtalk import *
from .feishu import *
+from .lark import *
from .ldap import *
from .oauth2 import *
from .oidc import *
from .passkey import *
from .radius import *
from .saml2 import *
+from .slack import *
from .sms import *
from .sso import *
from .wecom import *
-from .slack import *
diff --git a/apps/settings/serializers/auth/base.py b/apps/settings/serializers/auth/base.py
index fbc833124..bde6e781d 100644
--- a/apps/settings/serializers/auth/base.py
+++ b/apps/settings/serializers/auth/base.py
@@ -17,6 +17,7 @@ class AuthSettingSerializer(serializers.Serializer):
AUTH_RADIUS = serializers.BooleanField(required=False, label=_('RADIUS Auth'))
AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('DingTalk Auth'))
AUTH_FEISHU = serializers.BooleanField(default=False, label=_('FeiShu Auth'))
+ AUTH_LARK = serializers.BooleanField(default=False, label=_('Lark Auth'))
AUTH_WECOM = serializers.BooleanField(default=False, label=_('Slack Auth'))
AUTH_SLACK = serializers.BooleanField(default=False, label=_('WeCom Auth'))
AUTH_SSO = serializers.BooleanField(default=False, label=_("SSO Auth"))
diff --git a/apps/settings/serializers/auth/feishu.py b/apps/settings/serializers/auth/feishu.py
index d3c1edcad..187f2642f 100644
--- a/apps/settings/serializers/auth/feishu.py
+++ b/apps/settings/serializers/auth/feishu.py
@@ -9,13 +9,6 @@ __all__ = ['FeiShuSettingSerializer']
class FeiShuSettingSerializer(serializers.Serializer):
PREFIX_TITLE = _('FeiShu')
- VERSION_CHOICES = (
- ('feishu', _('FeiShu')),
- ('lark', 'Lark')
- )
AUTH_FEISHU = serializers.BooleanField(default=False, label=_('Enable FeiShu Auth'))
FEISHU_APP_ID = serializers.CharField(max_length=256, required=True, label='App ID')
FEISHU_APP_SECRET = EncryptedField(max_length=256, required=False, label='App Secret')
- FEISHU_VERSION = serializers.ChoiceField(
- choices=VERSION_CHOICES, default='feishu', label=_('Version')
- )
diff --git a/apps/settings/serializers/auth/lark.py b/apps/settings/serializers/auth/lark.py
new file mode 100644
index 000000000..4b2143fe1
--- /dev/null
+++ b/apps/settings/serializers/auth/lark.py
@@ -0,0 +1,14 @@
+from django.utils.translation import gettext_lazy as _
+from rest_framework import serializers
+
+from common.serializers.fields import EncryptedField
+
+__all__ = ['LarkSettingSerializer']
+
+
+class LarkSettingSerializer(serializers.Serializer):
+ PREFIX_TITLE = 'Lark'
+
+ AUTH_LARK = serializers.BooleanField(default=False, label=_('Enable Lark Auth'))
+ LARK_APP_ID = serializers.CharField(max_length=256, required=True, label='App ID')
+ LARK_APP_SECRET = EncryptedField(max_length=256, required=False, label='App Secret')
diff --git a/apps/settings/serializers/auth/ldap.py b/apps/settings/serializers/auth/ldap.py
index a79c0081f..4229e1251 100644
--- a/apps/settings/serializers/auth/ldap.py
+++ b/apps/settings/serializers/auth/ldap.py
@@ -76,6 +76,15 @@ class LDAPSettingSerializer(serializers.Serializer):
min_value=1, max_value=300,
required=False, label=_('Connect timeout (s)'),
)
+ AUTH_LDAP_CACHE_TIMEOUT = serializers.IntegerField(
+ min_value=0, max_value=3600 * 24 * 30 * 12,
+ default=3600 * 24 * 30,
+ required=False, label=_('User DN cache timeout (s)'),
+ help_text=_(
+ 'Caching the User DN obtained during user login authentication can effectively'
+ 'improve the speed of user authentication., 0 means no cache'
+ )
+ )
AUTH_LDAP_SEARCH_PAGED_SIZE = serializers.IntegerField(required=False, label=_('Search paged size (piece)'))
AUTH_LDAP_SYNC_RECEIVERS = serializers.ListField(
required=False, label=_('Recipient'), max_length=36
diff --git a/apps/settings/serializers/feature.py b/apps/settings/serializers/feature.py
index ca3987029..708e8179c 100644
--- a/apps/settings/serializers/feature.py
+++ b/apps/settings/serializers/feature.py
@@ -75,13 +75,13 @@ class ChatAISettingSerializer(serializers.Serializer):
required=False, label=_('Enable Chat AI')
)
GPT_BASE_URL = serializers.CharField(
- max_length=256, allow_blank=True, required=False, label=_('Base Url')
+ allow_blank=True, required=False, label=_('Base Url')
)
GPT_API_KEY = EncryptedField(
- max_length=256, allow_blank=True, required=False, label=_('API Key'),
+ allow_blank=True, required=False, label=_('API Key'),
)
GPT_PROXY = serializers.CharField(
- max_length=256, allow_blank=True, required=False, label=_('Proxy')
+ allow_blank=True, required=False, label=_('Proxy')
)
GPT_MODEL = serializers.ChoiceField(
default='', choices=GPT_MODEL_CHOICES, label=_("GPT Model"), required=False,
diff --git a/apps/settings/serializers/public.py b/apps/settings/serializers/public.py
index b4d66671e..ebefcb717 100644
--- a/apps/settings/serializers/public.py
+++ b/apps/settings/serializers/public.py
@@ -40,6 +40,7 @@ class PrivateSettingSerializer(PublicSettingSerializer):
AUTH_WECOM = serializers.BooleanField()
AUTH_DINGTALK = serializers.BooleanField()
AUTH_FEISHU = serializers.BooleanField()
+ AUTH_LARK = serializers.BooleanField()
AUTH_TEMP_TOKEN = serializers.BooleanField()
TERMINAL_RAZOR_ENABLED = serializers.BooleanField()
diff --git a/apps/settings/serializers/security.py b/apps/settings/serializers/security.py
index 1fca3be3d..d1c252a28 100644
--- a/apps/settings/serializers/security.py
+++ b/apps/settings/serializers/security.py
@@ -196,6 +196,10 @@ class SecuritySessionSerializer(serializers.Serializer):
label=_('Connection max idle time (minute)'),
help_text=_('If idle time more than it, disconnect connection.')
)
+ SESSION_EXPIRE_AT_BROWSER_CLOSE = serializers.BooleanField(
+ required=False, default=False, label=_('Session expire at browser closed'),
+ help_text=_('Whether to expire the session when the user closes their browser.')
+ )
SECURITY_MAX_SESSION_TIME = serializers.IntegerField(
min_value=1, max_value=99999, required=False,
label=_('Session max connection time (hour)'),
diff --git a/apps/settings/serializers/settings.py b/apps/settings/serializers/settings.py
index 743f7cc60..4706f9740 100644
--- a/apps/settings/serializers/settings.py
+++ b/apps/settings/serializers/settings.py
@@ -1,15 +1,15 @@
-# coding: utf-8
from django.core.cache import cache
from django.utils import translation
from django.utils.translation import gettext_noop, gettext_lazy as _
+from rest_framework import serializers
from common.utils import i18n_fmt
from .auth import (
LDAPSettingSerializer, OIDCSettingSerializer, KeycloakSettingSerializer,
CASSettingSerializer, RadiusSettingSerializer, FeiShuSettingSerializer,
- WeComSettingSerializer, DingTalkSettingSerializer, AlibabaSMSSettingSerializer,
- TencentSMSSettingSerializer, CMPP2SMSSettingSerializer, AuthSettingSerializer,
- SAML2SettingSerializer, OAuth2SettingSerializer, PasskeySettingSerializer,
+ LarkSettingSerializer, WeComSettingSerializer, DingTalkSettingSerializer,
+ AlibabaSMSSettingSerializer, TencentSMSSettingSerializer, CMPP2SMSSettingSerializer,
+ AuthSettingSerializer, SAML2SettingSerializer, OAuth2SettingSerializer, PasskeySettingSerializer,
CustomSMSSettingSerializer,
)
from .basic import BasicSettingSerializer
@@ -20,11 +20,53 @@ from .security import SecuritySettingSerializer
from .terminal import TerminalSettingSerializer
__all__ = [
+ 'BaseSerializerWithFieldLabel',
'SettingsSerializer',
]
+class BaseSerializerWithFieldLabel:
+ CACHE_KEY: str
+ ignore_iter_fields = True
+
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
+ self.fields_label_mapping = None
+
+ def extract_fields(self, serializer):
+ fields = {}
+ for field_name, field in serializer.get_fields().items():
+ if isinstance(field, serializers.Serializer):
+ fields.update(self.extract_fields(field))
+ else:
+ fields.update({field_name: field})
+ return fields
+
+ def get_field_label(self, field_name):
+ self.fields_label_mapping = cache.get(self.CACHE_KEY, None)
+ if self.fields_label_mapping is None:
+ self.fields_label_mapping = {}
+ with translation.override('en'):
+ cls = self.__class__
+ base_name = getattr(cls, 'PREFIX_TITLE', cls.__name__)
+
+ for subclass in cls.__bases__:
+ ignore_iter_fields = getattr(subclass, 'ignore_iter_fields', False)
+ if ignore_iter_fields:
+ continue
+
+ prefix = getattr(subclass, 'PREFIX_TITLE', base_name)
+ fields = self.extract_fields(subclass())
+ for name, item in fields.items():
+ label = getattr(item, 'label', '')
+ detail = i18n_fmt(gettext_noop('[%s] %s'), prefix, label)
+ self.fields_label_mapping[name] = detail
+ cache.set(self.CACHE_KEY, self.fields_label_mapping, 3600 * 24)
+ return self.fields_label_mapping.get(field_name, field_name)
+
+
class SettingsSerializer(
+ BaseSerializerWithFieldLabel,
BasicSettingSerializer,
LDAPSettingSerializer,
AuthSettingSerializer,
@@ -33,6 +75,7 @@ class SettingsSerializer(
WeComSettingSerializer,
DingTalkSettingSerializer,
FeiShuSettingSerializer,
+ LarkSettingSerializer,
EmailSettingSerializer,
EmailContentSettingSerializer,
OtherSettingSerializer,
@@ -49,25 +92,5 @@ class SettingsSerializer(
CustomSMSSettingSerializer,
PasskeySettingSerializer
):
+ PREFIX_TITLE = _('Setting')
CACHE_KEY = 'SETTING_FIELDS_MAPPING'
-
- # encrypt_fields 现在使用 write_only 来判断了
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- self.fields_label_mapping = None
-
- # 单次计算量不大,搞个缓存,以防操作日志大量写入时,这里影响性能
- def get_field_label(self, field_name):
- self.fields_label_mapping = cache.get(self.CACHE_KEY, None)
- if self.fields_label_mapping is None:
- self.fields_label_mapping = {}
- with translation.override('en'):
- for subclass in SettingsSerializer.__bases__:
- prefix = getattr(subclass, 'PREFIX_TITLE', _('Setting'))
- fields = subclass().get_fields()
- for name, item in fields.items():
- label = getattr(item, 'label', '')
- detail = i18n_fmt(gettext_noop('[%s] %s'), prefix, label)
- self.fields_label_mapping[name] = detail
- cache.set(self.CACHE_KEY, self.fields_label_mapping, 3600 * 24)
- return self.fields_label_mapping.get(field_name)
diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py
index 15b97c82c..03f3a4390 100644
--- a/apps/settings/urls/api_urls.py
+++ b/apps/settings/urls/api_urls.py
@@ -15,6 +15,7 @@ urlpatterns = [
path('wecom/testing/', api.WeComTestingAPI.as_view(), name='wecom-testing'),
path('dingtalk/testing/', api.DingTalkTestingAPI.as_view(), name='dingtalk-testing'),
path('feishu/testing/', api.FeiShuTestingAPI.as_view(), name='feishu-testing'),
+ path('lark/testing/', api.LarkTestingAPI.as_view(), name='lark-testing'),
path('slack/testing/', api.SlackTestingAPI.as_view(), name='slack-testing'),
path('sms//testing/', api.SMSTestingAPI.as_view(), name='sms-testing'),
path('sms/backend/', api.SMSBackendAPI.as_view(), name='sms-backend'),
diff --git a/apps/settings/utils/common.py b/apps/settings/utils/common.py
index 826b19cd8..a7c250ed1 100644
--- a/apps/settings/utils/common.py
+++ b/apps/settings/utils/common.py
@@ -20,7 +20,8 @@ def get_login_title():
def generate_ips(address_string):
def transform(_ip):
real_ip, err_msg = lookup_domain(_ip)
- return _ip if err_msg else real_ip
+ return _ip if err_msg or real_ip == '0.0.0.0' else real_ip
+
# 支持的格式
# 192.168.1.1,192.168.1.2
# 192.168.1.1-12 | 192.168.1.1-192.168.1.12 | 192.168.1.0/30 | 192.168.1.1
diff --git a/apps/settings/utils/ldap.py b/apps/settings/utils/ldap.py
index 6333f0350..afcd2dca1 100644
--- a/apps/settings/utils/ldap.py
+++ b/apps/settings/utils/ldap.py
@@ -652,7 +652,7 @@ class LDAPTestUtil(object):
def _test_before_login_check(self, username, password):
from settings.ws import CACHE_KEY_LDAP_TEST_CONFIG_TASK_STATUS, TASK_STATUS_IS_OVER
if not cache.get(CACHE_KEY_LDAP_TEST_CONFIG_TASK_STATUS):
- raise self.LDAPBeforeLoginCheckError(_('Please test the connection first'))
+ self.test_config()
backend = LDAPAuthorizationBackend()
ok, msg = backend.pre_check(username, password)
diff --git a/apps/settings/ws.py b/apps/settings/ws.py
index 7e4f8853b..dd0d16af4 100644
--- a/apps/settings/ws.py
+++ b/apps/settings/ws.py
@@ -28,15 +28,7 @@ from .tools import (
logger = get_logger(__name__)
-CACHE_KEY_LDAP_TEST_CONFIG_MSG = 'CACHE_KEY_LDAP_TEST_CONFIG_MSG'
-CACHE_KEY_LDAP_TEST_LOGIN_MSG = 'CACHE_KEY_LDAP_TEST_LOGIN_MSG'
-CACHE_KEY_LDAP_SYNC_USER_MSG = 'CACHE_KEY_LDAP_SYNC_USER_MSG'
-CACHE_KEY_LDAP_IMPORT_USER_MSG = 'CACHE_KEY_LDAP_IMPORT_USER_MSG'
CACHE_KEY_LDAP_TEST_CONFIG_TASK_STATUS = 'CACHE_KEY_LDAP_TEST_CONFIG_TASK_STATUS'
-CACHE_KEY_LDAP_TEST_LOGIN_TASK_STATUS = 'CACHE_KEY_LDAP_TEST_LOGIN_TASK_STATUS'
-CACHE_KEY_LDAP_SYNC_USER_TASK_STATUS = 'CACHE_KEY_LDAP_SYNC_USER_TASK_STATUS'
-CACHE_KEY_LDAP_IMPORT_USER_TASK_STATUS = 'CACHE_KEY_LDAP_IMPORT_USER_TASK_STATUS'
-TASK_STATUS_IS_RUNNING = 'RUNNING'
TASK_STATUS_IS_OVER = 'OVER'
@@ -118,15 +110,7 @@ class LdapWebsocket(AsyncJsonWebsocketConsumer):
msg_type = data.pop('msg_type', 'testing_config')
try:
tool_func = getattr(self, f'run_{msg_type.lower()}')
- await asyncio.to_thread(tool_func, data)
- if msg_type == 'testing_config':
- ok, msg = cache.get(CACHE_KEY_LDAP_TEST_CONFIG_MSG)
- elif msg_type == 'sync_user':
- ok, msg = cache.get(CACHE_KEY_LDAP_SYNC_USER_MSG)
- elif msg_type == 'import_user':
- ok, msg = cache.get(CACHE_KEY_LDAP_IMPORT_USER_MSG)
- else:
- ok, msg = cache.get(CACHE_KEY_LDAP_TEST_LOGIN_MSG)
+ ok, msg = await asyncio.to_thread(tool_func, data)
await self.send_msg(ok, msg)
except Exception as error:
await self.send_msg(msg='Exception: %s' % error)
@@ -172,60 +156,35 @@ class LdapWebsocket(AsyncJsonWebsocketConsumer):
def set_task_status_over(task_key, ttl=120):
cache.set(task_key, TASK_STATUS_IS_OVER, ttl)
- @staticmethod
- def set_task_msg(task_key, ok, msg, ttl=120):
- cache.set(task_key, (ok, msg), ttl)
-
def run_testing_config(self, data):
- while True:
- if self.task_is_over(CACHE_KEY_LDAP_TEST_CONFIG_TASK_STATUS):
- break
- else:
- serializer = LDAPTestConfigSerializer(data=data)
- if not serializer.is_valid():
- self.send_msg(msg=f'error: {str(serializer.errors)}')
- config = self.get_ldap_config(serializer)
- ok, msg = LDAPTestUtil(config).test_config()
- self.set_task_status_over(CACHE_KEY_LDAP_TEST_CONFIG_TASK_STATUS)
- self.set_task_msg(CACHE_KEY_LDAP_TEST_CONFIG_MSG, ok, msg)
+ serializer = LDAPTestConfigSerializer(data=data)
+ if not serializer.is_valid():
+ self.send_msg(msg=f'error: {str(serializer.errors)}')
+ config = self.get_ldap_config(serializer)
+ ok, msg = LDAPTestUtil(config).test_config()
+ if ok:
+ self.set_task_status_over(CACHE_KEY_LDAP_TEST_CONFIG_TASK_STATUS)
+ return ok, msg
def run_testing_login(self, data):
- while True:
- if self.task_is_over(CACHE_KEY_LDAP_TEST_LOGIN_TASK_STATUS):
- break
- else:
- serializer = LDAPTestLoginSerializer(data=data)
- if not serializer.is_valid():
- self.send_msg(msg=f'error: {str(serializer.errors)}')
- username = serializer.validated_data['username']
- password = serializer.validated_data['password']
- ok, msg = LDAPTestUtil().test_login(username, password)
- self.set_task_status_over(CACHE_KEY_LDAP_TEST_LOGIN_TASK_STATUS, 3)
- self.set_task_msg(CACHE_KEY_LDAP_TEST_LOGIN_MSG, ok, msg)
+ serializer = LDAPTestLoginSerializer(data=data)
+ if not serializer.is_valid():
+ self.send_msg(msg=f'error: {str(serializer.errors)}')
+ username = serializer.validated_data['username']
+ password = serializer.validated_data['password']
+ ok, msg = LDAPTestUtil().test_login(username, password)
+ return ok, msg
- def run_sync_user(self, data):
- while True:
- if self.task_is_over(CACHE_KEY_LDAP_SYNC_USER_TASK_STATUS):
- break
- else:
- sync_util = LDAPSyncUtil()
- sync_util.clear_cache()
- sync_ldap_user()
- msg = sync_util.get_task_error_msg()
- ok = False if msg else True
- self.set_task_status_over(CACHE_KEY_LDAP_SYNC_USER_TASK_STATUS)
- self.set_task_msg(CACHE_KEY_LDAP_SYNC_USER_MSG, ok, msg)
+ @staticmethod
+ def run_sync_user(data):
+ sync_util = LDAPSyncUtil()
+ sync_util.clear_cache()
+ sync_ldap_user()
+ msg = sync_util.get_task_error_msg()
+ ok = False if msg else True
+ return ok, msg
def run_import_user(self, data):
- while True:
- if self.task_is_over(CACHE_KEY_LDAP_IMPORT_USER_TASK_STATUS):
- break
- else:
- ok, msg = self.import_user(data)
- self.set_task_status_over(CACHE_KEY_LDAP_IMPORT_USER_TASK_STATUS, 3)
- self.set_task_msg(CACHE_KEY_LDAP_IMPORT_USER_MSG, ok, msg, 3)
-
- def import_user(self, data):
ok = False
org_ids = data.get('org_ids')
username_list = data.get('username_list', [])
diff --git a/apps/static/img/beian.png b/apps/static/img/beian.png
deleted file mode 100644
index 6fe667f73..000000000
Binary files a/apps/static/img/beian.png and /dev/null differ
diff --git a/apps/static/img/login_lark_logo.png b/apps/static/img/login_lark_logo.png
new file mode 100644
index 000000000..29048014e
Binary files /dev/null and b/apps/static/img/login_lark_logo.png differ
diff --git a/apps/static/js/plugins/markdown-it.min.js b/apps/static/js/plugins/markdown-it.min.js
new file mode 100644
index 000000000..0a4e3bb67
--- /dev/null
+++ b/apps/static/js/plugins/markdown-it.min.js
@@ -0,0 +1,2 @@
+/*! markdown-it 14.0.0 https://github.com/markdown-it/markdown-it @license MIT */
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).markdownit=e()}(this,(function(){"use strict";const t={};function e(r,n){"string"!=typeof n&&(n=e.defaultChars);const s=function(e){let r=t[e];if(r)return r;r=t[e]=[];for(let t=0;t<128;t++){const e=String.fromCharCode(t);r.push(e)}for(let t=0;t=55296&&t<=57343?"\ufffd\ufffd\ufffd":String.fromCharCode(t),r+=6;continue}}if(240==(248&i)&&r+91114111?e+="\ufffd\ufffd\ufffd\ufffd":(t-=65536,e+=String.fromCharCode(55296+(t>>10),56320+(1023&t))),r+=9;continue}}e+="\ufffd"}}return e}))}e.defaultChars=";/?:@&=+$,#",e.componentChars="";const r={};function n(t,e,s){"string"!=typeof e&&(s=e,e=n.defaultChars),void 0===s&&(s=!0);const i=function(t){let e=r[t];if(e)return e;e=r[t]=[];for(let t=0;t<128;t++){const r=String.fromCharCode(t);/^[0-9a-z]$/i.test(r)?e.push(r):e.push("%"+("0"+t.toString(16).toUpperCase()).slice(-2))}for(let r=0;r=55296&&n<=57343){if(n>=55296&&n<=56319&&e+1=56320&&r<=57343){o+=encodeURIComponent(t[e]+t[e+1]),e++;continue}}o+="%EF%BF%BD"}else o+=encodeURIComponent(t[e])}return o}function s(t){let e="";return e+=t.protocol||"",e+=t.slashes?"//":"",e+=t.auth?t.auth+"@":"",t.hostname&&-1!==t.hostname.indexOf(":")?e+="["+t.hostname+"]":e+=t.hostname||"",e+=t.port?":"+t.port:"",e+=t.pathname||"",e+=t.search||"",e+=t.hash||"",e}function i(){this.protocol=null,this.slashes=null,this.auth=null,this.port=null,this.hostname=null,this.hash=null,this.search=null,this.pathname=null}n.defaultChars=";/?:@&=+$,-_.!~*'()#",n.componentChars="-_.!~*'()";const o=/^([a-z0-9.+-]+:)/i,c=/:[0-9]*$/,a=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,l=["{","}","|","\\","^","`"].concat(["<",">",'"',"`"," ","\r","\n","\t"]),u=["'"].concat(l),h=["%","/","?",";","#"].concat(u),p=["/","?","#"],f=/^[+a-z0-9A-Z_-]{0,63}$/,d=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,_={javascript:!0,"javascript:":!0},m={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0};function g(t,e){if(t&&t instanceof i)return t;const r=new i;return r.parse(t,e),r}i.prototype.parse=function(t,e){let r,n,s,i=t;if(i=i.trim(),!e&&1===t.split("#").length){const t=a.exec(i);if(t)return this.pathname=t[1],t[2]&&(this.search=t[2]),this}let c=o.exec(i);if(c&&(c=c[0],r=c.toLowerCase(),this.protocol=c,i=i.substr(c.length)),(e||c||i.match(/^\/\/[^@\/]+@[^@\/]+/))&&(s="//"===i.substr(0,2),!s||c&&_[c]||(i=i.substr(2),this.slashes=!0)),!_[c]&&(s||c&&!m[c])){let t,e,r=-1;for(let t=0;t127?n+="x":n+=r[t];if(!n.match(f)){const n=t.slice(0,e),s=t.slice(e+1),o=r.match(d);o&&(n.push(o[1]),s.unshift(o[2])),s.length&&(i=s.join(".")+i),this.hostname=n.join(".");break}}}}this.hostname.length>255&&(this.hostname=""),o&&(this.hostname=this.hostname.substr(1,this.hostname.length-2))}const l=i.indexOf("#");-1!==l&&(this.hash=i.substr(l),i=i.slice(0,l));const u=i.indexOf("?");return-1!==u&&(this.search=i.substr(u),i=i.slice(0,u)),i&&(this.pathname=i),m[r]&&this.hostname&&!this.pathname&&(this.pathname=""),this},i.prototype.parseHost=function(t){let e=c.exec(t);e&&(e=e[0],":"!==e&&(this.port=e.substr(1)),t=t.substr(0,t.length-e.length)),t&&(this.hostname=t)};var k,y=Object.freeze({__proto__:null,decode:e,encode:n,format:s,parse:g}),b=/[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/,C=/[\0-\x1F\x7F-\x9F]/,A=/[!-#%-\*,-\/:;\?@\[-\]_\{\}\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061D-\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u09FD\u0A76\u0AF0\u0C77\u0C84\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1B7D\u1B7E\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E4F\u2E52-\u2E5D\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD803[\uDEAD\uDF55-\uDF59\uDF86-\uDF89]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC8\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDC4B-\uDC4F\uDC5A\uDC5B\uDC5D\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDE60-\uDE6C\uDEB9\uDF3C-\uDF3E]|\uD806[\uDC3B\uDD44-\uDD46\uDDE2\uDE3F-\uDE46\uDE9A-\uDE9C\uDE9E-\uDEA2\uDF00-\uDF09]|\uD807[\uDC41-\uDC45\uDC70\uDC71\uDEF7\uDEF8\uDF43-\uDF4F\uDFFF]|\uD809[\uDC70-\uDC74]|\uD80B[\uDFF1\uDFF2]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD81B[\uDE97-\uDE9A\uDFE2]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]|\uD83A[\uDD5E\uDD5F]/,E=/[ \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]/,x=Object.freeze({__proto__:null,Any:b,Cc:C,Cf:/[\xAD\u0600-\u0605\u061C\u06DD\u070F\u0890\u0891\u08E2\u180E\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF\uFFF9-\uFFFB]|\uD804[\uDCBD\uDCCD]|\uD80D[\uDC30-\uDC3F]|\uD82F[\uDCA0-\uDCA3]|\uD834[\uDD73-\uDD7A]|\uDB40[\uDC01\uDC20-\uDC7F]/,P:A,Z:E}),D=new Uint16Array('\u1d41<\xd5\u0131\u028a\u049d\u057b\u05d0\u0675\u06de\u07a2\u07d6\u080f\u0a4a\u0a91\u0da1\u0e6d\u0f09\u0f26\u10ca\u1228\u12e1\u1415\u149d\u14c3\u14df\u1525\0\0\0\0\0\0\u156b\u16cd\u198d\u1c12\u1ddd\u1f7e\u2060\u21b0\u228d\u23c0\u23fb\u2442\u2824\u2912\u2d08\u2e48\u2fce\u3016\u32ba\u3639\u37ac\u38fe\u3a28\u3a71\u3ae0\u3b2e\u0800EMabcfglmnoprstu\\bfms\x7f\x84\x8b\x90\x95\x98\xa6\xb3\xb9\xc8\xcflig\u803b\xc6\u40c6P\u803b&\u4026cute\u803b\xc1\u40c1reve;\u4102\u0100iyx}rc\u803b\xc2\u40c2;\u4410r;\uc000\ud835\udd04rave\u803b\xc0\u40c0pha;\u4391acr;\u4100d;\u6a53\u0100gp\x9d\xa1on;\u4104f;\uc000\ud835\udd38plyFunction;\u6061ing\u803b\xc5\u40c5\u0100cs\xbe\xc3r;\uc000\ud835\udc9cign;\u6254ilde\u803b\xc3\u40c3ml\u803b\xc4\u40c4\u0400aceforsu\xe5\xfb\xfe\u0117\u011c\u0122\u0127\u012a\u0100cr\xea\xf2kslash;\u6216\u0176\xf6\xf8;\u6ae7ed;\u6306y;\u4411\u0180crt\u0105\u010b\u0114ause;\u6235noullis;\u612ca;\u4392r;\uc000\ud835\udd05pf;\uc000\ud835\udd39eve;\u42d8c\xf2\u0113mpeq;\u624e\u0700HOacdefhilorsu\u014d\u0151\u0156\u0180\u019e\u01a2\u01b5\u01b7\u01ba\u01dc\u0215\u0273\u0278\u027ecy;\u4427PY\u803b\xa9\u40a9\u0180cpy\u015d\u0162\u017aute;\u4106\u0100;i\u0167\u0168\u62d2talDifferentialD;\u6145leys;\u612d\u0200aeio\u0189\u018e\u0194\u0198ron;\u410cdil\u803b\xc7\u40c7rc;\u4108nint;\u6230ot;\u410a\u0100dn\u01a7\u01adilla;\u40b8terDot;\u40b7\xf2\u017fi;\u43a7rcle\u0200DMPT\u01c7\u01cb\u01d1\u01d6ot;\u6299inus;\u6296lus;\u6295imes;\u6297o\u0100cs\u01e2\u01f8kwiseContourIntegral;\u6232eCurly\u0100DQ\u0203\u020foubleQuote;\u601duote;\u6019\u0200lnpu\u021e\u0228\u0247\u0255on\u0100;e\u0225\u0226\u6237;\u6a74\u0180git\u022f\u0236\u023aruent;\u6261nt;\u622fourIntegral;\u622e\u0100fr\u024c\u024e;\u6102oduct;\u6210nterClockwiseContourIntegral;\u6233oss;\u6a2fcr;\uc000\ud835\udc9ep\u0100;C\u0284\u0285\u62d3ap;\u624d\u0580DJSZacefios\u02a0\u02ac\u02b0\u02b4\u02b8\u02cb\u02d7\u02e1\u02e6\u0333\u048d\u0100;o\u0179\u02a5trahd;\u6911cy;\u4402cy;\u4405cy;\u440f\u0180grs\u02bf\u02c4\u02c7ger;\u6021r;\u61a1hv;\u6ae4\u0100ay\u02d0\u02d5ron;\u410e;\u4414l\u0100;t\u02dd\u02de\u6207a;\u4394r;\uc000\ud835\udd07\u0100af\u02eb\u0327\u0100cm\u02f0\u0322ritical\u0200ADGT\u0300\u0306\u0316\u031ccute;\u40b4o\u0174\u030b\u030d;\u42d9bleAcute;\u42ddrave;\u4060ilde;\u42dcond;\u62c4ferentialD;\u6146\u0470\u033d\0\0\0\u0342\u0354\0\u0405f;\uc000\ud835\udd3b\u0180;DE\u0348\u0349\u034d\u40a8ot;\u60dcqual;\u6250ble\u0300CDLRUV\u0363\u0372\u0382\u03cf\u03e2\u03f8ontourIntegra\xec\u0239o\u0274\u0379\0\0\u037b\xbb\u0349nArrow;\u61d3\u0100eo\u0387\u03a4ft\u0180ART\u0390\u0396\u03a1rrow;\u61d0ightArrow;\u61d4e\xe5\u02cang\u0100LR\u03ab\u03c4eft\u0100AR\u03b3\u03b9rrow;\u67f8ightArrow;\u67faightArrow;\u67f9ight\u0100AT\u03d8\u03derrow;\u61d2ee;\u62a8p\u0241\u03e9\0\0\u03efrrow;\u61d1ownArrow;\u61d5erticalBar;\u6225n\u0300ABLRTa\u0412\u042a\u0430\u045e\u047f\u037crrow\u0180;BU\u041d\u041e\u0422\u6193ar;\u6913pArrow;\u61f5reve;\u4311eft\u02d2\u043a\0\u0446\0\u0450ightVector;\u6950eeVector;\u695eector\u0100;B\u0459\u045a\u61bdar;\u6956ight\u01d4\u0467\0\u0471eeVector;\u695fector\u0100;B\u047a\u047b\u61c1ar;\u6957ee\u0100;A\u0486\u0487\u62a4rrow;\u61a7\u0100ct\u0492\u0497r;\uc000\ud835\udc9frok;\u4110\u0800NTacdfglmopqstux\u04bd\u04c0\u04c4\u04cb\u04de\u04e2\u04e7\u04ee\u04f5\u0521\u052f\u0536\u0552\u055d\u0560\u0565G;\u414aH\u803b\xd0\u40d0cute\u803b\xc9\u40c9\u0180aiy\u04d2\u04d7\u04dcron;\u411arc\u803b\xca\u40ca;\u442dot;\u4116r;\uc000\ud835\udd08rave\u803b\xc8\u40c8ement;\u6208\u0100ap\u04fa\u04fecr;\u4112ty\u0253\u0506\0\0\u0512mallSquare;\u65fberySmallSquare;\u65ab\u0100gp\u0526\u052aon;\u4118f;\uc000\ud835\udd3csilon;\u4395u\u0100ai\u053c\u0549l\u0100;T\u0542\u0543\u6a75ilde;\u6242librium;\u61cc\u0100ci\u0557\u055ar;\u6130m;\u6a73a;\u4397ml\u803b\xcb\u40cb\u0100ip\u056a\u056fsts;\u6203onentialE;\u6147\u0280cfios\u0585\u0588\u058d\u05b2\u05ccy;\u4424r;\uc000\ud835\udd09lled\u0253\u0597\0\0\u05a3mallSquare;\u65fcerySmallSquare;\u65aa\u0370\u05ba\0\u05bf\0\0\u05c4f;\uc000\ud835\udd3dAll;\u6200riertrf;\u6131c\xf2\u05cb\u0600JTabcdfgorst\u05e8\u05ec\u05ef\u05fa\u0600\u0612\u0616\u061b\u061d\u0623\u066c\u0672cy;\u4403\u803b>\u403emma\u0100;d\u05f7\u05f8\u4393;\u43dcreve;\u411e\u0180eiy\u0607\u060c\u0610dil;\u4122rc;\u411c;\u4413ot;\u4120r;\uc000\ud835\udd0a;\u62d9pf;\uc000\ud835\udd3eeater\u0300EFGLST\u0635\u0644\u064e\u0656\u065b\u0666qual\u0100;L\u063e\u063f\u6265ess;\u62dbullEqual;\u6267reater;\u6aa2ess;\u6277lantEqual;\u6a7eilde;\u6273cr;\uc000\ud835\udca2;\u626b\u0400Aacfiosu\u0685\u068b\u0696\u069b\u069e\u06aa\u06be\u06caRDcy;\u442a\u0100ct\u0690\u0694ek;\u42c7;\u405eirc;\u4124r;\u610clbertSpace;\u610b\u01f0\u06af\0\u06b2f;\u610dizontalLine;\u6500\u0100ct\u06c3\u06c5\xf2\u06a9rok;\u4126mp\u0144\u06d0\u06d8ownHum\xf0\u012fqual;\u624f\u0700EJOacdfgmnostu\u06fa\u06fe\u0703\u0707\u070e\u071a\u071e\u0721\u0728\u0744\u0778\u078b\u078f\u0795cy;\u4415lig;\u4132cy;\u4401cute\u803b\xcd\u40cd\u0100iy\u0713\u0718rc\u803b\xce\u40ce;\u4418ot;\u4130r;\u6111rave\u803b\xcc\u40cc\u0180;ap\u0720\u072f\u073f\u0100cg\u0734\u0737r;\u412ainaryI;\u6148lie\xf3\u03dd\u01f4\u0749\0\u0762\u0100;e\u074d\u074e\u622c\u0100gr\u0753\u0758ral;\u622bsection;\u62c2isible\u0100CT\u076c\u0772omma;\u6063imes;\u6062\u0180gpt\u077f\u0783\u0788on;\u412ef;\uc000\ud835\udd40a;\u4399cr;\u6110ilde;\u4128\u01eb\u079a\0\u079ecy;\u4406l\u803b\xcf\u40cf\u0280cfosu\u07ac\u07b7\u07bc\u07c2\u07d0\u0100iy\u07b1\u07b5rc;\u4134;\u4419r;\uc000\ud835\udd0dpf;\uc000\ud835\udd41\u01e3\u07c7\0\u07ccr;\uc000\ud835\udca5rcy;\u4408kcy;\u4404\u0380HJacfos\u07e4\u07e8\u07ec\u07f1\u07fd\u0802\u0808cy;\u4425cy;\u440cppa;\u439a\u0100ey\u07f6\u07fbdil;\u4136;\u441ar;\uc000\ud835\udd0epf;\uc000\ud835\udd42cr;\uc000\ud835\udca6\u0580JTaceflmost\u0825\u0829\u082c\u0850\u0863\u09b3\u09b8\u09c7\u09cd\u0a37\u0a47cy;\u4409\u803b<\u403c\u0280cmnpr\u0837\u083c\u0841\u0844\u084dute;\u4139bda;\u439bg;\u67ealacetrf;\u6112r;\u619e\u0180aey\u0857\u085c\u0861ron;\u413ddil;\u413b;\u441b\u0100fs\u0868\u0970t\u0500ACDFRTUVar\u087e\u08a9\u08b1\u08e0\u08e6\u08fc\u092f\u095b\u0390\u096a\u0100nr\u0883\u088fgleBracket;\u67e8row\u0180;BR\u0899\u089a\u089e\u6190ar;\u61e4ightArrow;\u61c6eiling;\u6308o\u01f5\u08b7\0\u08c3bleBracket;\u67e6n\u01d4\u08c8\0\u08d2eeVector;\u6961ector\u0100;B\u08db\u08dc\u61c3ar;\u6959loor;\u630aight\u0100AV\u08ef\u08f5rrow;\u6194ector;\u694e\u0100er\u0901\u0917e\u0180;AV\u0909\u090a\u0910\u62a3rrow;\u61a4ector;\u695aiangle\u0180;BE\u0924\u0925\u0929\u62b2ar;\u69cfqual;\u62b4p\u0180DTV\u0937\u0942\u094cownVector;\u6951eeVector;\u6960ector\u0100;B\u0956\u0957\u61bfar;\u6958ector\u0100;B\u0965\u0966\u61bcar;\u6952ight\xe1\u039cs\u0300EFGLST\u097e\u098b\u0995\u099d\u09a2\u09adqualGreater;\u62daullEqual;\u6266reater;\u6276ess;\u6aa1lantEqual;\u6a7dilde;\u6272r;\uc000\ud835\udd0f\u0100;e\u09bd\u09be\u62d8ftarrow;\u61daidot;\u413f\u0180npw\u09d4\u0a16\u0a1bg\u0200LRlr\u09de\u09f7\u0a02\u0a10eft\u0100AR\u09e6\u09ecrrow;\u67f5ightArrow;\u67f7ightArrow;\u67f6eft\u0100ar\u03b3\u0a0aight\xe1\u03bfight\xe1\u03caf;\uc000\ud835\udd43er\u0100LR\u0a22\u0a2ceftArrow;\u6199ightArrow;\u6198\u0180cht\u0a3e\u0a40\u0a42\xf2\u084c;\u61b0rok;\u4141;\u626a\u0400acefiosu\u0a5a\u0a5d\u0a60\u0a77\u0a7c\u0a85\u0a8b\u0a8ep;\u6905y;\u441c\u0100dl\u0a65\u0a6fiumSpace;\u605flintrf;\u6133r;\uc000\ud835\udd10nusPlus;\u6213pf;\uc000\ud835\udd44c\xf2\u0a76;\u439c\u0480Jacefostu\u0aa3\u0aa7\u0aad\u0ac0\u0b14\u0b19\u0d91\u0d97\u0d9ecy;\u440acute;\u4143\u0180aey\u0ab4\u0ab9\u0aberon;\u4147dil;\u4145;\u441d\u0180gsw\u0ac7\u0af0\u0b0eative\u0180MTV\u0ad3\u0adf\u0ae8ediumSpace;\u600bhi\u0100cn\u0ae6\u0ad8\xeb\u0ad9eryThi\xee\u0ad9ted\u0100GL\u0af8\u0b06reaterGreate\xf2\u0673essLes\xf3\u0a48Line;\u400ar;\uc000\ud835\udd11\u0200Bnpt\u0b22\u0b28\u0b37\u0b3areak;\u6060BreakingSpace;\u40a0f;\u6115\u0680;CDEGHLNPRSTV\u0b55\u0b56\u0b6a\u0b7c\u0ba1\u0beb\u0c04\u0c5e\u0c84\u0ca6\u0cd8\u0d61\u0d85\u6aec\u0100ou\u0b5b\u0b64ngruent;\u6262pCap;\u626doubleVerticalBar;\u6226\u0180lqx\u0b83\u0b8a\u0b9bement;\u6209ual\u0100;T\u0b92\u0b93\u6260ilde;\uc000\u2242\u0338ists;\u6204reater\u0380;EFGLST\u0bb6\u0bb7\u0bbd\u0bc9\u0bd3\u0bd8\u0be5\u626fqual;\u6271ullEqual;\uc000\u2267\u0338reater;\uc000\u226b\u0338ess;\u6279lantEqual;\uc000\u2a7e\u0338ilde;\u6275ump\u0144\u0bf2\u0bfdownHump;\uc000\u224e\u0338qual;\uc000\u224f\u0338e\u0100fs\u0c0a\u0c27tTriangle\u0180;BE\u0c1a\u0c1b\u0c21\u62eaar;\uc000\u29cf\u0338qual;\u62ecs\u0300;EGLST\u0c35\u0c36\u0c3c\u0c44\u0c4b\u0c58\u626equal;\u6270reater;\u6278ess;\uc000\u226a\u0338lantEqual;\uc000\u2a7d\u0338ilde;\u6274ested\u0100GL\u0c68\u0c79reaterGreater;\uc000\u2aa2\u0338essLess;\uc000\u2aa1\u0338recedes\u0180;ES\u0c92\u0c93\u0c9b\u6280qual;\uc000\u2aaf\u0338lantEqual;\u62e0\u0100ei\u0cab\u0cb9verseElement;\u620cghtTriangle\u0180;BE\u0ccb\u0ccc\u0cd2\u62ebar;\uc000\u29d0\u0338qual;\u62ed\u0100qu\u0cdd\u0d0cuareSu\u0100bp\u0ce8\u0cf9set\u0100;E\u0cf0\u0cf3\uc000\u228f\u0338qual;\u62e2erset\u0100;E\u0d03\u0d06\uc000\u2290\u0338qual;\u62e3\u0180bcp\u0d13\u0d24\u0d4eset\u0100;E\u0d1b\u0d1e\uc000\u2282\u20d2qual;\u6288ceeds\u0200;EST\u0d32\u0d33\u0d3b\u0d46\u6281qual;\uc000\u2ab0\u0338lantEqual;\u62e1ilde;\uc000\u227f\u0338erset\u0100;E\u0d58\u0d5b\uc000\u2283\u20d2qual;\u6289ilde\u0200;EFT\u0d6e\u0d6f\u0d75\u0d7f\u6241qual;\u6244ullEqual;\u6247ilde;\u6249erticalBar;\u6224cr;\uc000\ud835\udca9ilde\u803b\xd1\u40d1;\u439d\u0700Eacdfgmoprstuv\u0dbd\u0dc2\u0dc9\u0dd5\u0ddb\u0de0\u0de7\u0dfc\u0e02\u0e20\u0e22\u0e32\u0e3f\u0e44lig;\u4152cute\u803b\xd3\u40d3\u0100iy\u0dce\u0dd3rc\u803b\xd4\u40d4;\u441eblac;\u4150r;\uc000\ud835\udd12rave\u803b\xd2\u40d2\u0180aei\u0dee\u0df2\u0df6cr;\u414cga;\u43a9cron;\u439fpf;\uc000\ud835\udd46enCurly\u0100DQ\u0e0e\u0e1aoubleQuote;\u601cuote;\u6018;\u6a54\u0100cl\u0e27\u0e2cr;\uc000\ud835\udcaaash\u803b\xd8\u40d8i\u016c\u0e37\u0e3cde\u803b\xd5\u40d5es;\u6a37ml\u803b\xd6\u40d6er\u0100BP\u0e4b\u0e60\u0100ar\u0e50\u0e53r;\u603eac\u0100ek\u0e5a\u0e5c;\u63deet;\u63b4arenthesis;\u63dc\u0480acfhilors\u0e7f\u0e87\u0e8a\u0e8f\u0e92\u0e94\u0e9d\u0eb0\u0efcrtialD;\u6202y;\u441fr;\uc000\ud835\udd13i;\u43a6;\u43a0usMinus;\u40b1\u0100ip\u0ea2\u0eadncareplan\xe5\u069df;\u6119\u0200;eio\u0eb9\u0eba\u0ee0\u0ee4\u6abbcedes\u0200;EST\u0ec8\u0ec9\u0ecf\u0eda\u627aqual;\u6aaflantEqual;\u627cilde;\u627eme;\u6033\u0100dp\u0ee9\u0eeeuct;\u620fortion\u0100;a\u0225\u0ef9l;\u621d\u0100ci\u0f01\u0f06r;\uc000\ud835\udcab;\u43a8\u0200Ufos\u0f11\u0f16\u0f1b\u0f1fOT\u803b"\u4022r;\uc000\ud835\udd14pf;\u611acr;\uc000\ud835\udcac\u0600BEacefhiorsu\u0f3e\u0f43\u0f47\u0f60\u0f73\u0fa7\u0faa\u0fad\u1096\u10a9\u10b4\u10bearr;\u6910G\u803b\xae\u40ae\u0180cnr\u0f4e\u0f53\u0f56ute;\u4154g;\u67ebr\u0100;t\u0f5c\u0f5d\u61a0l;\u6916\u0180aey\u0f67\u0f6c\u0f71ron;\u4158dil;\u4156;\u4420\u0100;v\u0f78\u0f79\u611cerse\u0100EU\u0f82\u0f99\u0100lq\u0f87\u0f8eement;\u620builibrium;\u61cbpEquilibrium;\u696fr\xbb\u0f79o;\u43a1ght\u0400ACDFTUVa\u0fc1\u0feb\u0ff3\u1022\u1028\u105b\u1087\u03d8\u0100nr\u0fc6\u0fd2gleBracket;\u67e9row\u0180;BL\u0fdc\u0fdd\u0fe1\u6192ar;\u61e5eftArrow;\u61c4eiling;\u6309o\u01f5\u0ff9\0\u1005bleBracket;\u67e7n\u01d4\u100a\0\u1014eeVector;\u695dector\u0100;B\u101d\u101e\u61c2ar;\u6955loor;\u630b\u0100er\u102d\u1043e\u0180;AV\u1035\u1036\u103c\u62a2rrow;\u61a6ector;\u695biangle\u0180;BE\u1050\u1051\u1055\u62b3ar;\u69d0qual;\u62b5p\u0180DTV\u1063\u106e\u1078ownVector;\u694feeVector;\u695cector\u0100;B\u1082\u1083\u61bear;\u6954ector\u0100;B\u1091\u1092\u61c0ar;\u6953\u0100pu\u109b\u109ef;\u611dndImplies;\u6970ightarrow;\u61db\u0100ch\u10b9\u10bcr;\u611b;\u61b1leDelayed;\u69f4\u0680HOacfhimoqstu\u10e4\u10f1\u10f7\u10fd\u1119\u111e\u1151\u1156\u1161\u1167\u11b5\u11bb\u11bf\u0100Cc\u10e9\u10eeHcy;\u4429y;\u4428FTcy;\u442ccute;\u415a\u0280;aeiy\u1108\u1109\u110e\u1113\u1117\u6abcron;\u4160dil;\u415erc;\u415c;\u4421r;\uc000\ud835\udd16ort\u0200DLRU\u112a\u1134\u113e\u1149ownArrow\xbb\u041eeftArrow\xbb\u089aightArrow\xbb\u0fddpArrow;\u6191gma;\u43a3allCircle;\u6218pf;\uc000\ud835\udd4a\u0272\u116d\0\0\u1170t;\u621aare\u0200;ISU\u117b\u117c\u1189\u11af\u65a1ntersection;\u6293u\u0100bp\u118f\u119eset\u0100;E\u1197\u1198\u628fqual;\u6291erset\u0100;E\u11a8\u11a9\u6290qual;\u6292nion;\u6294cr;\uc000\ud835\udcaear;\u62c6\u0200bcmp\u11c8\u11db\u1209\u120b\u0100;s\u11cd\u11ce\u62d0et\u0100;E\u11cd\u11d5qual;\u6286\u0100ch\u11e0\u1205eeds\u0200;EST\u11ed\u11ee\u11f4\u11ff\u627bqual;\u6ab0lantEqual;\u627dilde;\u627fTh\xe1\u0f8c;\u6211\u0180;es\u1212\u1213\u1223\u62d1rset\u0100;E\u121c\u121d\u6283qual;\u6287et\xbb\u1213\u0580HRSacfhiors\u123e\u1244\u1249\u1255\u125e\u1271\u1276\u129f\u12c2\u12c8\u12d1ORN\u803b\xde\u40deADE;\u6122\u0100Hc\u124e\u1252cy;\u440by;\u4426\u0100bu\u125a\u125c;\u4009;\u43a4\u0180aey\u1265\u126a\u126fron;\u4164dil;\u4162;\u4422r;\uc000\ud835\udd17\u0100ei\u127b\u1289\u01f2\u1280\0\u1287efore;\u6234a;\u4398\u0100cn\u128e\u1298kSpace;\uc000\u205f\u200aSpace;\u6009lde\u0200;EFT\u12ab\u12ac\u12b2\u12bc\u623cqual;\u6243ullEqual;\u6245ilde;\u6248pf;\uc000\ud835\udd4bipleDot;\u60db\u0100ct\u12d6\u12dbr;\uc000\ud835\udcafrok;\u4166\u0ae1\u12f7\u130e\u131a\u1326\0\u132c\u1331\0\0\0\0\0\u1338\u133d\u1377\u1385\0\u13ff\u1404\u140a\u1410\u0100cr\u12fb\u1301ute\u803b\xda\u40dar\u0100;o\u1307\u1308\u619fcir;\u6949r\u01e3\u1313\0\u1316y;\u440eve;\u416c\u0100iy\u131e\u1323rc\u803b\xdb\u40db;\u4423blac;\u4170r;\uc000\ud835\udd18rave\u803b\xd9\u40d9acr;\u416a\u0100di\u1341\u1369er\u0100BP\u1348\u135d\u0100ar\u134d\u1350r;\u405fac\u0100ek\u1357\u1359;\u63dfet;\u63b5arenthesis;\u63ddon\u0100;P\u1370\u1371\u62c3lus;\u628e\u0100gp\u137b\u137fon;\u4172f;\uc000\ud835\udd4c\u0400ADETadps\u1395\u13ae\u13b8\u13c4\u03e8\u13d2\u13d7\u13f3rrow\u0180;BD\u1150\u13a0\u13a4ar;\u6912ownArrow;\u61c5ownArrow;\u6195quilibrium;\u696eee\u0100;A\u13cb\u13cc\u62a5rrow;\u61a5own\xe1\u03f3er\u0100LR\u13de\u13e8eftArrow;\u6196ightArrow;\u6197i\u0100;l\u13f9\u13fa\u43d2on;\u43a5ing;\u416ecr;\uc000\ud835\udcb0ilde;\u4168ml\u803b\xdc\u40dc\u0480Dbcdefosv\u1427\u142c\u1430\u1433\u143e\u1485\u148a\u1490\u1496ash;\u62abar;\u6aeby;\u4412ash\u0100;l\u143b\u143c\u62a9;\u6ae6\u0100er\u1443\u1445;\u62c1\u0180bty\u144c\u1450\u147aar;\u6016\u0100;i\u144f\u1455cal\u0200BLST\u1461\u1465\u146a\u1474ar;\u6223ine;\u407ceparator;\u6758ilde;\u6240ThinSpace;\u600ar;\uc000\ud835\udd19pf;\uc000\ud835\udd4dcr;\uc000\ud835\udcb1dash;\u62aa\u0280cefos\u14a7\u14ac\u14b1\u14b6\u14bcirc;\u4174dge;\u62c0r;\uc000\ud835\udd1apf;\uc000\ud835\udd4ecr;\uc000\ud835\udcb2\u0200fios\u14cb\u14d0\u14d2\u14d8r;\uc000\ud835\udd1b;\u439epf;\uc000\ud835\udd4fcr;\uc000\ud835\udcb3\u0480AIUacfosu\u14f1\u14f5\u14f9\u14fd\u1504\u150f\u1514\u151a\u1520cy;\u442fcy;\u4407cy;\u442ecute\u803b\xdd\u40dd\u0100iy\u1509\u150drc;\u4176;\u442br;\uc000\ud835\udd1cpf;\uc000\ud835\udd50cr;\uc000\ud835\udcb4ml;\u4178\u0400Hacdefos\u1535\u1539\u153f\u154b\u154f\u155d\u1560\u1564cy;\u4416cute;\u4179\u0100ay\u1544\u1549ron;\u417d;\u4417ot;\u417b\u01f2\u1554\0\u155boWidt\xe8\u0ad9a;\u4396r;\u6128pf;\u6124cr;\uc000\ud835\udcb5\u0be1\u1583\u158a\u1590\0\u15b0\u15b6\u15bf\0\0\0\0\u15c6\u15db\u15eb\u165f\u166d\0\u1695\u169b\u16b2\u16b9\0\u16becute\u803b\xe1\u40e1reve;\u4103\u0300;Ediuy\u159c\u159d\u15a1\u15a3\u15a8\u15ad\u623e;\uc000\u223e\u0333;\u623frc\u803b\xe2\u40e2te\u80bb\xb4\u0306;\u4430lig\u803b\xe6\u40e6\u0100;r\xb2\u15ba;\uc000\ud835\udd1erave\u803b\xe0\u40e0\u0100ep\u15ca\u15d6\u0100fp\u15cf\u15d4sym;\u6135\xe8\u15d3ha;\u43b1\u0100ap\u15dfc\u0100cl\u15e4\u15e7r;\u4101g;\u6a3f\u0264\u15f0\0\0\u160a\u0280;adsv\u15fa\u15fb\u15ff\u1601\u1607\u6227nd;\u6a55;\u6a5clope;\u6a58;\u6a5a\u0380;elmrsz\u1618\u1619\u161b\u161e\u163f\u164f\u1659\u6220;\u69a4e\xbb\u1619sd\u0100;a\u1625\u1626\u6221\u0461\u1630\u1632\u1634\u1636\u1638\u163a\u163c\u163e;\u69a8;\u69a9;\u69aa;\u69ab;\u69ac;\u69ad;\u69ae;\u69aft\u0100;v\u1645\u1646\u621fb\u0100;d\u164c\u164d\u62be;\u699d\u0100pt\u1654\u1657h;\u6222\xbb\xb9arr;\u637c\u0100gp\u1663\u1667on;\u4105f;\uc000\ud835\udd52\u0380;Eaeiop\u12c1\u167b\u167d\u1682\u1684\u1687\u168a;\u6a70cir;\u6a6f;\u624ad;\u624bs;\u4027rox\u0100;e\u12c1\u1692\xf1\u1683ing\u803b\xe5\u40e5\u0180cty\u16a1\u16a6\u16a8r;\uc000\ud835\udcb6;\u402amp\u0100;e\u12c1\u16af\xf1\u0288ilde\u803b\xe3\u40e3ml\u803b\xe4\u40e4\u0100ci\u16c2\u16c8onin\xf4\u0272nt;\u6a11\u0800Nabcdefiklnoprsu\u16ed\u16f1\u1730\u173c\u1743\u1748\u1778\u177d\u17e0\u17e6\u1839\u1850\u170d\u193d\u1948\u1970ot;\u6aed\u0100cr\u16f6\u171ek\u0200ceps\u1700\u1705\u170d\u1713ong;\u624cpsilon;\u43f6rime;\u6035im\u0100;e\u171a\u171b\u623dq;\u62cd\u0176\u1722\u1726ee;\u62bded\u0100;g\u172c\u172d\u6305e\xbb\u172drk\u0100;t\u135c\u1737brk;\u63b6\u0100oy\u1701\u1741;\u4431quo;\u601e\u0280cmprt\u1753\u175b\u1761\u1764\u1768aus\u0100;e\u010a\u0109ptyv;\u69b0s\xe9\u170cno\xf5\u0113\u0180ahw\u176f\u1771\u1773;\u43b2;\u6136een;\u626cr;\uc000\ud835\udd1fg\u0380costuvw\u178d\u179d\u17b3\u17c1\u17d5\u17db\u17de\u0180aiu\u1794\u1796\u179a\xf0\u0760rc;\u65efp\xbb\u1371\u0180dpt\u17a4\u17a8\u17adot;\u6a00lus;\u6a01imes;\u6a02\u0271\u17b9\0\0\u17becup;\u6a06ar;\u6605riangle\u0100du\u17cd\u17d2own;\u65bdp;\u65b3plus;\u6a04e\xe5\u1444\xe5\u14adarow;\u690d\u0180ako\u17ed\u1826\u1835\u0100cn\u17f2\u1823k\u0180lst\u17fa\u05ab\u1802ozenge;\u69ebriangle\u0200;dlr\u1812\u1813\u1818\u181d\u65b4own;\u65beeft;\u65c2ight;\u65b8k;\u6423\u01b1\u182b\0\u1833\u01b2\u182f\0\u1831;\u6592;\u65914;\u6593ck;\u6588\u0100eo\u183e\u184d\u0100;q\u1843\u1846\uc000=\u20e5uiv;\uc000\u2261\u20e5t;\u6310\u0200ptwx\u1859\u185e\u1867\u186cf;\uc000\ud835\udd53\u0100;t\u13cb\u1863om\xbb\u13cctie;\u62c8\u0600DHUVbdhmptuv\u1885\u1896\u18aa\u18bb\u18d7\u18db\u18ec\u18ff\u1905\u190a\u1910\u1921\u0200LRlr\u188e\u1890\u1892\u1894;\u6557;\u6554;\u6556;\u6553\u0280;DUdu\u18a1\u18a2\u18a4\u18a6\u18a8\u6550;\u6566;\u6569;\u6564;\u6567\u0200LRlr\u18b3\u18b5\u18b7\u18b9;\u655d;\u655a;\u655c;\u6559\u0380;HLRhlr\u18ca\u18cb\u18cd\u18cf\u18d1\u18d3\u18d5\u6551;\u656c;\u6563;\u6560;\u656b;\u6562;\u655fox;\u69c9\u0200LRlr\u18e4\u18e6\u18e8\u18ea;\u6555;\u6552;\u6510;\u650c\u0280;DUdu\u06bd\u18f7\u18f9\u18fb\u18fd;\u6565;\u6568;\u652c;\u6534inus;\u629flus;\u629eimes;\u62a0\u0200LRlr\u1919\u191b\u191d\u191f;\u655b;\u6558;\u6518;\u6514\u0380;HLRhlr\u1930\u1931\u1933\u1935\u1937\u1939\u193b\u6502;\u656a;\u6561;\u655e;\u653c;\u6524;\u651c\u0100ev\u0123\u1942bar\u803b\xa6\u40a6\u0200ceio\u1951\u1956\u195a\u1960r;\uc000\ud835\udcb7mi;\u604fm\u0100;e\u171a\u171cl\u0180;bh\u1968\u1969\u196b\u405c;\u69c5sub;\u67c8\u016c\u1974\u197el\u0100;e\u1979\u197a\u6022t\xbb\u197ap\u0180;Ee\u012f\u1985\u1987;\u6aae\u0100;q\u06dc\u06db\u0ce1\u19a7\0\u19e8\u1a11\u1a15\u1a32\0\u1a37\u1a50\0\0\u1ab4\0\0\u1ac1\0\0\u1b21\u1b2e\u1b4d\u1b52\0\u1bfd\0\u1c0c\u0180cpr\u19ad\u19b2\u19ddute;\u4107\u0300;abcds\u19bf\u19c0\u19c4\u19ca\u19d5\u19d9\u6229nd;\u6a44rcup;\u6a49\u0100au\u19cf\u19d2p;\u6a4bp;\u6a47ot;\u6a40;\uc000\u2229\ufe00\u0100eo\u19e2\u19e5t;\u6041\xee\u0693\u0200aeiu\u19f0\u19fb\u1a01\u1a05\u01f0\u19f5\0\u19f8s;\u6a4don;\u410ddil\u803b\xe7\u40e7rc;\u4109ps\u0100;s\u1a0c\u1a0d\u6a4cm;\u6a50ot;\u410b\u0180dmn\u1a1b\u1a20\u1a26il\u80bb\xb8\u01adptyv;\u69b2t\u8100\xa2;e\u1a2d\u1a2e\u40a2r\xe4\u01b2r;\uc000\ud835\udd20\u0180cei\u1a3d\u1a40\u1a4dy;\u4447ck\u0100;m\u1a47\u1a48\u6713ark\xbb\u1a48;\u43c7r\u0380;Ecefms\u1a5f\u1a60\u1a62\u1a6b\u1aa4\u1aaa\u1aae\u65cb;\u69c3\u0180;el\u1a69\u1a6a\u1a6d\u42c6q;\u6257e\u0261\u1a74\0\0\u1a88rrow\u0100lr\u1a7c\u1a81eft;\u61baight;\u61bb\u0280RSacd\u1a92\u1a94\u1a96\u1a9a\u1a9f\xbb\u0f47;\u64c8st;\u629birc;\u629aash;\u629dnint;\u6a10id;\u6aefcir;\u69c2ubs\u0100;u\u1abb\u1abc\u6663it\xbb\u1abc\u02ec\u1ac7\u1ad4\u1afa\0\u1b0aon\u0100;e\u1acd\u1ace\u403a\u0100;q\xc7\xc6\u026d\u1ad9\0\0\u1ae2a\u0100;t\u1ade\u1adf\u402c;\u4040\u0180;fl\u1ae8\u1ae9\u1aeb\u6201\xee\u1160e\u0100mx\u1af1\u1af6ent\xbb\u1ae9e\xf3\u024d\u01e7\u1afe\0\u1b07\u0100;d\u12bb\u1b02ot;\u6a6dn\xf4\u0246\u0180fry\u1b10\u1b14\u1b17;\uc000\ud835\udd54o\xe4\u0254\u8100\xa9;s\u0155\u1b1dr;\u6117\u0100ao\u1b25\u1b29rr;\u61b5ss;\u6717\u0100cu\u1b32\u1b37r;\uc000\ud835\udcb8\u0100bp\u1b3c\u1b44\u0100;e\u1b41\u1b42\u6acf;\u6ad1\u0100;e\u1b49\u1b4a\u6ad0;\u6ad2dot;\u62ef\u0380delprvw\u1b60\u1b6c\u1b77\u1b82\u1bac\u1bd4\u1bf9arr\u0100lr\u1b68\u1b6a;\u6938;\u6935\u0270\u1b72\0\0\u1b75r;\u62dec;\u62dfarr\u0100;p\u1b7f\u1b80\u61b6;\u693d\u0300;bcdos\u1b8f\u1b90\u1b96\u1ba1\u1ba5\u1ba8\u622arcap;\u6a48\u0100au\u1b9b\u1b9ep;\u6a46p;\u6a4aot;\u628dr;\u6a45;\uc000\u222a\ufe00\u0200alrv\u1bb5\u1bbf\u1bde\u1be3rr\u0100;m\u1bbc\u1bbd\u61b7;\u693cy\u0180evw\u1bc7\u1bd4\u1bd8q\u0270\u1bce\0\0\u1bd2re\xe3\u1b73u\xe3\u1b75ee;\u62ceedge;\u62cfen\u803b\xa4\u40a4earrow\u0100lr\u1bee\u1bf3eft\xbb\u1b80ight\xbb\u1bbde\xe4\u1bdd\u0100ci\u1c01\u1c07onin\xf4\u01f7nt;\u6231lcty;\u632d\u0980AHabcdefhijlorstuwz\u1c38\u1c3b\u1c3f\u1c5d\u1c69\u1c75\u1c8a\u1c9e\u1cac\u1cb7\u1cfb\u1cff\u1d0d\u1d7b\u1d91\u1dab\u1dbb\u1dc6\u1dcdr\xf2\u0381ar;\u6965\u0200glrs\u1c48\u1c4d\u1c52\u1c54ger;\u6020eth;\u6138\xf2\u1133h\u0100;v\u1c5a\u1c5b\u6010\xbb\u090a\u016b\u1c61\u1c67arow;\u690fa\xe3\u0315\u0100ay\u1c6e\u1c73ron;\u410f;\u4434\u0180;ao\u0332\u1c7c\u1c84\u0100gr\u02bf\u1c81r;\u61catseq;\u6a77\u0180glm\u1c91\u1c94\u1c98\u803b\xb0\u40b0ta;\u43b4ptyv;\u69b1\u0100ir\u1ca3\u1ca8sht;\u697f;\uc000\ud835\udd21ar\u0100lr\u1cb3\u1cb5\xbb\u08dc\xbb\u101e\u0280aegsv\u1cc2\u0378\u1cd6\u1cdc\u1ce0m\u0180;os\u0326\u1cca\u1cd4nd\u0100;s\u0326\u1cd1uit;\u6666amma;\u43ddin;\u62f2\u0180;io\u1ce7\u1ce8\u1cf8\u40f7de\u8100\xf7;o\u1ce7\u1cf0ntimes;\u62c7n\xf8\u1cf7cy;\u4452c\u026f\u1d06\0\0\u1d0arn;\u631eop;\u630d\u0280lptuw\u1d18\u1d1d\u1d22\u1d49\u1d55lar;\u4024f;\uc000\ud835\udd55\u0280;emps\u030b\u1d2d\u1d37\u1d3d\u1d42q\u0100;d\u0352\u1d33ot;\u6251inus;\u6238lus;\u6214quare;\u62a1blebarwedg\xe5\xfan\u0180adh\u112e\u1d5d\u1d67ownarrow\xf3\u1c83arpoon\u0100lr\u1d72\u1d76ef\xf4\u1cb4igh\xf4\u1cb6\u0162\u1d7f\u1d85karo\xf7\u0f42\u026f\u1d8a\0\0\u1d8ern;\u631fop;\u630c\u0180cot\u1d98\u1da3\u1da6\u0100ry\u1d9d\u1da1;\uc000\ud835\udcb9;\u4455l;\u69f6rok;\u4111\u0100dr\u1db0\u1db4ot;\u62f1i\u0100;f\u1dba\u1816\u65bf\u0100ah\u1dc0\u1dc3r\xf2\u0429a\xf2\u0fa6angle;\u69a6\u0100ci\u1dd2\u1dd5y;\u445fgrarr;\u67ff\u0900Dacdefglmnopqrstux\u1e01\u1e09\u1e19\u1e38\u0578\u1e3c\u1e49\u1e61\u1e7e\u1ea5\u1eaf\u1ebd\u1ee1\u1f2a\u1f37\u1f44\u1f4e\u1f5a\u0100Do\u1e06\u1d34o\xf4\u1c89\u0100cs\u1e0e\u1e14ute\u803b\xe9\u40e9ter;\u6a6e\u0200aioy\u1e22\u1e27\u1e31\u1e36ron;\u411br\u0100;c\u1e2d\u1e2e\u6256\u803b\xea\u40ealon;\u6255;\u444dot;\u4117\u0100Dr\u1e41\u1e45ot;\u6252;\uc000\ud835\udd22\u0180;rs\u1e50\u1e51\u1e57\u6a9aave\u803b\xe8\u40e8\u0100;d\u1e5c\u1e5d\u6a96ot;\u6a98\u0200;ils\u1e6a\u1e6b\u1e72\u1e74\u6a99nters;\u63e7;\u6113\u0100;d\u1e79\u1e7a\u6a95ot;\u6a97\u0180aps\u1e85\u1e89\u1e97cr;\u4113ty\u0180;sv\u1e92\u1e93\u1e95\u6205et\xbb\u1e93p\u01001;\u1e9d\u1ea4\u0133\u1ea1\u1ea3;\u6004;\u6005\u6003\u0100gs\u1eaa\u1eac;\u414bp;\u6002\u0100gp\u1eb4\u1eb8on;\u4119f;\uc000\ud835\udd56\u0180als\u1ec4\u1ece\u1ed2r\u0100;s\u1eca\u1ecb\u62d5l;\u69e3us;\u6a71i\u0180;lv\u1eda\u1edb\u1edf\u43b5on\xbb\u1edb;\u43f5\u0200csuv\u1eea\u1ef3\u1f0b\u1f23\u0100io\u1eef\u1e31rc\xbb\u1e2e\u0269\u1ef9\0\0\u1efb\xed\u0548ant\u0100gl\u1f02\u1f06tr\xbb\u1e5dess\xbb\u1e7a\u0180aei\u1f12\u1f16\u1f1als;\u403dst;\u625fv\u0100;D\u0235\u1f20D;\u6a78parsl;\u69e5\u0100Da\u1f2f\u1f33ot;\u6253rr;\u6971\u0180cdi\u1f3e\u1f41\u1ef8r;\u612fo\xf4\u0352\u0100ah\u1f49\u1f4b;\u43b7\u803b\xf0\u40f0\u0100mr\u1f53\u1f57l\u803b\xeb\u40ebo;\u60ac\u0180cip\u1f61\u1f64\u1f67l;\u4021s\xf4\u056e\u0100eo\u1f6c\u1f74ctatio\xee\u0559nential\xe5\u0579\u09e1\u1f92\0\u1f9e\0\u1fa1\u1fa7\0\0\u1fc6\u1fcc\0\u1fd3\0\u1fe6\u1fea\u2000\0\u2008\u205allingdotse\xf1\u1e44y;\u4444male;\u6640\u0180ilr\u1fad\u1fb3\u1fc1lig;\u8000\ufb03\u0269\u1fb9\0\0\u1fbdg;\u8000\ufb00ig;\u8000\ufb04;\uc000\ud835\udd23lig;\u8000\ufb01lig;\uc000fj\u0180alt\u1fd9\u1fdc\u1fe1t;\u666dig;\u8000\ufb02ns;\u65b1of;\u4192\u01f0\u1fee\0\u1ff3f;\uc000\ud835\udd57\u0100ak\u05bf\u1ff7\u0100;v\u1ffc\u1ffd\u62d4;\u6ad9artint;\u6a0d\u0100ao\u200c\u2055\u0100cs\u2011\u2052\u03b1\u201a\u2030\u2038\u2045\u2048\0\u2050\u03b2\u2022\u2025\u2027\u202a\u202c\0\u202e\u803b\xbd\u40bd;\u6153\u803b\xbc\u40bc;\u6155;\u6159;\u615b\u01b3\u2034\0\u2036;\u6154;\u6156\u02b4\u203e\u2041\0\0\u2043\u803b\xbe\u40be;\u6157;\u615c5;\u6158\u01b6\u204c\0\u204e;\u615a;\u615d8;\u615el;\u6044wn;\u6322cr;\uc000\ud835\udcbb\u0880Eabcdefgijlnorstv\u2082\u2089\u209f\u20a5\u20b0\u20b4\u20f0\u20f5\u20fa\u20ff\u2103\u2112\u2138\u0317\u213e\u2152\u219e\u0100;l\u064d\u2087;\u6a8c\u0180cmp\u2090\u2095\u209dute;\u41f5ma\u0100;d\u209c\u1cda\u43b3;\u6a86reve;\u411f\u0100iy\u20aa\u20aerc;\u411d;\u4433ot;\u4121\u0200;lqs\u063e\u0642\u20bd\u20c9\u0180;qs\u063e\u064c\u20c4lan\xf4\u0665\u0200;cdl\u0665\u20d2\u20d5\u20e5c;\u6aa9ot\u0100;o\u20dc\u20dd\u6a80\u0100;l\u20e2\u20e3\u6a82;\u6a84\u0100;e\u20ea\u20ed\uc000\u22db\ufe00s;\u6a94r;\uc000\ud835\udd24\u0100;g\u0673\u061bmel;\u6137cy;\u4453\u0200;Eaj\u065a\u210c\u210e\u2110;\u6a92;\u6aa5;\u6aa4\u0200Eaes\u211b\u211d\u2129\u2134;\u6269p\u0100;p\u2123\u2124\u6a8arox\xbb\u2124\u0100;q\u212e\u212f\u6a88\u0100;q\u212e\u211bim;\u62e7pf;\uc000\ud835\udd58\u0100ci\u2143\u2146r;\u610am\u0180;el\u066b\u214e\u2150;\u6a8e;\u6a90\u8300>;cdlqr\u05ee\u2160\u216a\u216e\u2173\u2179\u0100ci\u2165\u2167;\u6aa7r;\u6a7aot;\u62d7Par;\u6995uest;\u6a7c\u0280adels\u2184\u216a\u2190\u0656\u219b\u01f0\u2189\0\u218epro\xf8\u209er;\u6978q\u0100lq\u063f\u2196les\xf3\u2088i\xed\u066b\u0100en\u21a3\u21adrtneqq;\uc000\u2269\ufe00\xc5\u21aa\u0500Aabcefkosy\u21c4\u21c7\u21f1\u21f5\u21fa\u2218\u221d\u222f\u2268\u227dr\xf2\u03a0\u0200ilmr\u21d0\u21d4\u21d7\u21dbrs\xf0\u1484f\xbb\u2024il\xf4\u06a9\u0100dr\u21e0\u21e4cy;\u444a\u0180;cw\u08f4\u21eb\u21efir;\u6948;\u61adar;\u610firc;\u4125\u0180alr\u2201\u220e\u2213rts\u0100;u\u2209\u220a\u6665it\xbb\u220alip;\u6026con;\u62b9r;\uc000\ud835\udd25s\u0100ew\u2223\u2229arow;\u6925arow;\u6926\u0280amopr\u223a\u223e\u2243\u225e\u2263rr;\u61fftht;\u623bk\u0100lr\u2249\u2253eftarrow;\u61a9ightarrow;\u61aaf;\uc000\ud835\udd59bar;\u6015\u0180clt\u226f\u2274\u2278r;\uc000\ud835\udcbdas\xe8\u21f4rok;\u4127\u0100bp\u2282\u2287ull;\u6043hen\xbb\u1c5b\u0ae1\u22a3\0\u22aa\0\u22b8\u22c5\u22ce\0\u22d5\u22f3\0\0\u22f8\u2322\u2367\u2362\u237f\0\u2386\u23aa\u23b4cute\u803b\xed\u40ed\u0180;iy\u0771\u22b0\u22b5rc\u803b\xee\u40ee;\u4438\u0100cx\u22bc\u22bfy;\u4435cl\u803b\xa1\u40a1\u0100fr\u039f\u22c9;\uc000\ud835\udd26rave\u803b\xec\u40ec\u0200;ino\u073e\u22dd\u22e9\u22ee\u0100in\u22e2\u22e6nt;\u6a0ct;\u622dfin;\u69dcta;\u6129lig;\u4133\u0180aop\u22fe\u231a\u231d\u0180cgt\u2305\u2308\u2317r;\u412b\u0180elp\u071f\u230f\u2313in\xe5\u078ear\xf4\u0720h;\u4131f;\u62b7ed;\u41b5\u0280;cfot\u04f4\u232c\u2331\u233d\u2341are;\u6105in\u0100;t\u2338\u2339\u621eie;\u69dddo\xf4\u2319\u0280;celp\u0757\u234c\u2350\u235b\u2361al;\u62ba\u0100gr\u2355\u2359er\xf3\u1563\xe3\u234darhk;\u6a17rod;\u6a3c\u0200cgpt\u236f\u2372\u2376\u237by;\u4451on;\u412ff;\uc000\ud835\udd5aa;\u43b9uest\u803b\xbf\u40bf\u0100ci\u238a\u238fr;\uc000\ud835\udcben\u0280;Edsv\u04f4\u239b\u239d\u23a1\u04f3;\u62f9ot;\u62f5\u0100;v\u23a6\u23a7\u62f4;\u62f3\u0100;i\u0777\u23aelde;\u4129\u01eb\u23b8\0\u23bccy;\u4456l\u803b\xef\u40ef\u0300cfmosu\u23cc\u23d7\u23dc\u23e1\u23e7\u23f5\u0100iy\u23d1\u23d5rc;\u4135;\u4439r;\uc000\ud835\udd27ath;\u4237pf;\uc000\ud835\udd5b\u01e3\u23ec\0\u23f1r;\uc000\ud835\udcbfrcy;\u4458kcy;\u4454\u0400acfghjos\u240b\u2416\u2422\u2427\u242d\u2431\u2435\u243bppa\u0100;v\u2413\u2414\u43ba;\u43f0\u0100ey\u241b\u2420dil;\u4137;\u443ar;\uc000\ud835\udd28reen;\u4138cy;\u4445cy;\u445cpf;\uc000\ud835\udd5ccr;\uc000\ud835\udcc0\u0b80ABEHabcdefghjlmnoprstuv\u2470\u2481\u2486\u248d\u2491\u250e\u253d\u255a\u2580\u264e\u265e\u2665\u2679\u267d\u269a\u26b2\u26d8\u275d\u2768\u278b\u27c0\u2801\u2812\u0180art\u2477\u247a\u247cr\xf2\u09c6\xf2\u0395ail;\u691barr;\u690e\u0100;g\u0994\u248b;\u6a8bar;\u6962\u0963\u24a5\0\u24aa\0\u24b1\0\0\0\0\0\u24b5\u24ba\0\u24c6\u24c8\u24cd\0\u24f9ute;\u413amptyv;\u69b4ra\xee\u084cbda;\u43bbg\u0180;dl\u088e\u24c1\u24c3;\u6991\xe5\u088e;\u6a85uo\u803b\xab\u40abr\u0400;bfhlpst\u0899\u24de\u24e6\u24e9\u24eb\u24ee\u24f1\u24f5\u0100;f\u089d\u24e3s;\u691fs;\u691d\xeb\u2252p;\u61abl;\u6939im;\u6973l;\u61a2\u0180;ae\u24ff\u2500\u2504\u6aabil;\u6919\u0100;s\u2509\u250a\u6aad;\uc000\u2aad\ufe00\u0180abr\u2515\u2519\u251drr;\u690crk;\u6772\u0100ak\u2522\u252cc\u0100ek\u2528\u252a;\u407b;\u405b\u0100es\u2531\u2533;\u698bl\u0100du\u2539\u253b;\u698f;\u698d\u0200aeuy\u2546\u254b\u2556\u2558ron;\u413e\u0100di\u2550\u2554il;\u413c\xec\u08b0\xe2\u2529;\u443b\u0200cqrs\u2563\u2566\u256d\u257da;\u6936uo\u0100;r\u0e19\u1746\u0100du\u2572\u2577har;\u6967shar;\u694bh;\u61b2\u0280;fgqs\u258b\u258c\u0989\u25f3\u25ff\u6264t\u0280ahlrt\u2598\u25a4\u25b7\u25c2\u25e8rrow\u0100;t\u0899\u25a1a\xe9\u24f6arpoon\u0100du\u25af\u25b4own\xbb\u045ap\xbb\u0966eftarrows;\u61c7ight\u0180ahs\u25cd\u25d6\u25derrow\u0100;s\u08f4\u08a7arpoon\xf3\u0f98quigarro\xf7\u21f0hreetimes;\u62cb\u0180;qs\u258b\u0993\u25falan\xf4\u09ac\u0280;cdgs\u09ac\u260a\u260d\u261d\u2628c;\u6aa8ot\u0100;o\u2614\u2615\u6a7f\u0100;r\u261a\u261b\u6a81;\u6a83\u0100;e\u2622\u2625\uc000\u22da\ufe00s;\u6a93\u0280adegs\u2633\u2639\u263d\u2649\u264bppro\xf8\u24c6ot;\u62d6q\u0100gq\u2643\u2645\xf4\u0989gt\xf2\u248c\xf4\u099bi\xed\u09b2\u0180ilr\u2655\u08e1\u265asht;\u697c;\uc000\ud835\udd29\u0100;E\u099c\u2663;\u6a91\u0161\u2669\u2676r\u0100du\u25b2\u266e\u0100;l\u0965\u2673;\u696alk;\u6584cy;\u4459\u0280;acht\u0a48\u2688\u268b\u2691\u2696r\xf2\u25c1orne\xf2\u1d08ard;\u696bri;\u65fa\u0100io\u269f\u26a4dot;\u4140ust\u0100;a\u26ac\u26ad\u63b0che\xbb\u26ad\u0200Eaes\u26bb\u26bd\u26c9\u26d4;\u6268p\u0100;p\u26c3\u26c4\u6a89rox\xbb\u26c4\u0100;q\u26ce\u26cf\u6a87\u0100;q\u26ce\u26bbim;\u62e6\u0400abnoptwz\u26e9\u26f4\u26f7\u271a\u272f\u2741\u2747\u2750\u0100nr\u26ee\u26f1g;\u67ecr;\u61fdr\xeb\u08c1g\u0180lmr\u26ff\u270d\u2714eft\u0100ar\u09e6\u2707ight\xe1\u09f2apsto;\u67fcight\xe1\u09fdparrow\u0100lr\u2725\u2729ef\xf4\u24edight;\u61ac\u0180afl\u2736\u2739\u273dr;\u6985;\uc000\ud835\udd5dus;\u6a2dimes;\u6a34\u0161\u274b\u274fst;\u6217\xe1\u134e\u0180;ef\u2757\u2758\u1800\u65cange\xbb\u2758ar\u0100;l\u2764\u2765\u4028t;\u6993\u0280achmt\u2773\u2776\u277c\u2785\u2787r\xf2\u08a8orne\xf2\u1d8car\u0100;d\u0f98\u2783;\u696d;\u600eri;\u62bf\u0300achiqt\u2798\u279d\u0a40\u27a2\u27ae\u27bbquo;\u6039r;\uc000\ud835\udcc1m\u0180;eg\u09b2\u27aa\u27ac;\u6a8d;\u6a8f\u0100bu\u252a\u27b3o\u0100;r\u0e1f\u27b9;\u601arok;\u4142\u8400<;cdhilqr\u082b\u27d2\u2639\u27dc\u27e0\u27e5\u27ea\u27f0\u0100ci\u27d7\u27d9;\u6aa6r;\u6a79re\xe5\u25f2mes;\u62c9arr;\u6976uest;\u6a7b\u0100Pi\u27f5\u27f9ar;\u6996\u0180;ef\u2800\u092d\u181b\u65c3r\u0100du\u2807\u280dshar;\u694ahar;\u6966\u0100en\u2817\u2821rtneqq;\uc000\u2268\ufe00\xc5\u281e\u0700Dacdefhilnopsu\u2840\u2845\u2882\u288e\u2893\u28a0\u28a5\u28a8\u28da\u28e2\u28e4\u0a83\u28f3\u2902Dot;\u623a\u0200clpr\u284e\u2852\u2863\u287dr\u803b\xaf\u40af\u0100et\u2857\u2859;\u6642\u0100;e\u285e\u285f\u6720se\xbb\u285f\u0100;s\u103b\u2868to\u0200;dlu\u103b\u2873\u2877\u287bow\xee\u048cef\xf4\u090f\xf0\u13d1ker;\u65ae\u0100oy\u2887\u288cmma;\u6a29;\u443cash;\u6014asuredangle\xbb\u1626r;\uc000\ud835\udd2ao;\u6127\u0180cdn\u28af\u28b4\u28c9ro\u803b\xb5\u40b5\u0200;acd\u1464\u28bd\u28c0\u28c4s\xf4\u16a7ir;\u6af0ot\u80bb\xb7\u01b5us\u0180;bd\u28d2\u1903\u28d3\u6212\u0100;u\u1d3c\u28d8;\u6a2a\u0163\u28de\u28e1p;\u6adb\xf2\u2212\xf0\u0a81\u0100dp\u28e9\u28eeels;\u62a7f;\uc000\ud835\udd5e\u0100ct\u28f8\u28fdr;\uc000\ud835\udcc2pos\xbb\u159d\u0180;lm\u2909\u290a\u290d\u43bctimap;\u62b8\u0c00GLRVabcdefghijlmoprstuvw\u2942\u2953\u297e\u2989\u2998\u29da\u29e9\u2a15\u2a1a\u2a58\u2a5d\u2a83\u2a95\u2aa4\u2aa8\u2b04\u2b07\u2b44\u2b7f\u2bae\u2c34\u2c67\u2c7c\u2ce9\u0100gt\u2947\u294b;\uc000\u22d9\u0338\u0100;v\u2950\u0bcf\uc000\u226b\u20d2\u0180elt\u295a\u2972\u2976ft\u0100ar\u2961\u2967rrow;\u61cdightarrow;\u61ce;\uc000\u22d8\u0338\u0100;v\u297b\u0c47\uc000\u226a\u20d2ightarrow;\u61cf\u0100Dd\u298e\u2993ash;\u62afash;\u62ae\u0280bcnpt\u29a3\u29a7\u29ac\u29b1\u29ccla\xbb\u02deute;\u4144g;\uc000\u2220\u20d2\u0280;Eiop\u0d84\u29bc\u29c0\u29c5\u29c8;\uc000\u2a70\u0338d;\uc000\u224b\u0338s;\u4149ro\xf8\u0d84ur\u0100;a\u29d3\u29d4\u666el\u0100;s\u29d3\u0b38\u01f3\u29df\0\u29e3p\u80bb\xa0\u0b37mp\u0100;e\u0bf9\u0c00\u0280aeouy\u29f4\u29fe\u2a03\u2a10\u2a13\u01f0\u29f9\0\u29fb;\u6a43on;\u4148dil;\u4146ng\u0100;d\u0d7e\u2a0aot;\uc000\u2a6d\u0338p;\u6a42;\u443dash;\u6013\u0380;Aadqsx\u0b92\u2a29\u2a2d\u2a3b\u2a41\u2a45\u2a50rr;\u61d7r\u0100hr\u2a33\u2a36k;\u6924\u0100;o\u13f2\u13f0ot;\uc000\u2250\u0338ui\xf6\u0b63\u0100ei\u2a4a\u2a4ear;\u6928\xed\u0b98ist\u0100;s\u0ba0\u0b9fr;\uc000\ud835\udd2b\u0200Eest\u0bc5\u2a66\u2a79\u2a7c\u0180;qs\u0bbc\u2a6d\u0be1\u0180;qs\u0bbc\u0bc5\u2a74lan\xf4\u0be2i\xed\u0bea\u0100;r\u0bb6\u2a81\xbb\u0bb7\u0180Aap\u2a8a\u2a8d\u2a91r\xf2\u2971rr;\u61aear;\u6af2\u0180;sv\u0f8d\u2a9c\u0f8c\u0100;d\u2aa1\u2aa2\u62fc;\u62facy;\u445a\u0380AEadest\u2ab7\u2aba\u2abe\u2ac2\u2ac5\u2af6\u2af9r\xf2\u2966;\uc000\u2266\u0338rr;\u619ar;\u6025\u0200;fqs\u0c3b\u2ace\u2ae3\u2aeft\u0100ar\u2ad4\u2ad9rro\xf7\u2ac1ightarro\xf7\u2a90\u0180;qs\u0c3b\u2aba\u2aealan\xf4\u0c55\u0100;s\u0c55\u2af4\xbb\u0c36i\xed\u0c5d\u0100;r\u0c35\u2afei\u0100;e\u0c1a\u0c25i\xe4\u0d90\u0100pt\u2b0c\u2b11f;\uc000\ud835\udd5f\u8180\xac;in\u2b19\u2b1a\u2b36\u40acn\u0200;Edv\u0b89\u2b24\u2b28\u2b2e;\uc000\u22f9\u0338ot;\uc000\u22f5\u0338\u01e1\u0b89\u2b33\u2b35;\u62f7;\u62f6i\u0100;v\u0cb8\u2b3c\u01e1\u0cb8\u2b41\u2b43;\u62fe;\u62fd\u0180aor\u2b4b\u2b63\u2b69r\u0200;ast\u0b7b\u2b55\u2b5a\u2b5flle\xec\u0b7bl;\uc000\u2afd\u20e5;\uc000\u2202\u0338lint;\u6a14\u0180;ce\u0c92\u2b70\u2b73u\xe5\u0ca5\u0100;c\u0c98\u2b78\u0100;e\u0c92\u2b7d\xf1\u0c98\u0200Aait\u2b88\u2b8b\u2b9d\u2ba7r\xf2\u2988rr\u0180;cw\u2b94\u2b95\u2b99\u619b;\uc000\u2933\u0338;\uc000\u219d\u0338ghtarrow\xbb\u2b95ri\u0100;e\u0ccb\u0cd6\u0380chimpqu\u2bbd\u2bcd\u2bd9\u2b04\u0b78\u2be4\u2bef\u0200;cer\u0d32\u2bc6\u0d37\u2bc9u\xe5\u0d45;\uc000\ud835\udcc3ort\u026d\u2b05\0\0\u2bd6ar\xe1\u2b56m\u0100;e\u0d6e\u2bdf\u0100;q\u0d74\u0d73su\u0100bp\u2beb\u2bed\xe5\u0cf8\xe5\u0d0b\u0180bcp\u2bf6\u2c11\u2c19\u0200;Ees\u2bff\u2c00\u0d22\u2c04\u6284;\uc000\u2ac5\u0338et\u0100;e\u0d1b\u2c0bq\u0100;q\u0d23\u2c00c\u0100;e\u0d32\u2c17\xf1\u0d38\u0200;Ees\u2c22\u2c23\u0d5f\u2c27\u6285;\uc000\u2ac6\u0338et\u0100;e\u0d58\u2c2eq\u0100;q\u0d60\u2c23\u0200gilr\u2c3d\u2c3f\u2c45\u2c47\xec\u0bd7lde\u803b\xf1\u40f1\xe7\u0c43iangle\u0100lr\u2c52\u2c5ceft\u0100;e\u0c1a\u2c5a\xf1\u0c26ight\u0100;e\u0ccb\u2c65\xf1\u0cd7\u0100;m\u2c6c\u2c6d\u43bd\u0180;es\u2c74\u2c75\u2c79\u4023ro;\u6116p;\u6007\u0480DHadgilrs\u2c8f\u2c94\u2c99\u2c9e\u2ca3\u2cb0\u2cb6\u2cd3\u2ce3ash;\u62adarr;\u6904p;\uc000\u224d\u20d2ash;\u62ac\u0100et\u2ca8\u2cac;\uc000\u2265\u20d2;\uc000>\u20d2nfin;\u69de\u0180Aet\u2cbd\u2cc1\u2cc5rr;\u6902;\uc000\u2264\u20d2\u0100;r\u2cca\u2ccd\uc000<\u20d2ie;\uc000\u22b4\u20d2\u0100At\u2cd8\u2cdcrr;\u6903rie;\uc000\u22b5\u20d2im;\uc000\u223c\u20d2\u0180Aan\u2cf0\u2cf4\u2d02rr;\u61d6r\u0100hr\u2cfa\u2cfdk;\u6923\u0100;o\u13e7\u13e5ear;\u6927\u1253\u1a95\0\0\0\0\0\0\0\0\0\0\0\0\0\u2d2d\0\u2d38\u2d48\u2d60\u2d65\u2d72\u2d84\u1b07\0\0\u2d8d\u2dab\0\u2dc8\u2dce\0\u2ddc\u2e19\u2e2b\u2e3e\u2e43\u0100cs\u2d31\u1a97ute\u803b\xf3\u40f3\u0100iy\u2d3c\u2d45r\u0100;c\u1a9e\u2d42\u803b\xf4\u40f4;\u443e\u0280abios\u1aa0\u2d52\u2d57\u01c8\u2d5alac;\u4151v;\u6a38old;\u69bclig;\u4153\u0100cr\u2d69\u2d6dir;\u69bf;\uc000\ud835\udd2c\u036f\u2d79\0\0\u2d7c\0\u2d82n;\u42dbave\u803b\xf2\u40f2;\u69c1\u0100bm\u2d88\u0df4ar;\u69b5\u0200acit\u2d95\u2d98\u2da5\u2da8r\xf2\u1a80\u0100ir\u2d9d\u2da0r;\u69beoss;\u69bbn\xe5\u0e52;\u69c0\u0180aei\u2db1\u2db5\u2db9cr;\u414dga;\u43c9\u0180cdn\u2dc0\u2dc5\u01cdron;\u43bf;\u69b6pf;\uc000\ud835\udd60\u0180ael\u2dd4\u2dd7\u01d2r;\u69b7rp;\u69b9\u0380;adiosv\u2dea\u2deb\u2dee\u2e08\u2e0d\u2e10\u2e16\u6228r\xf2\u1a86\u0200;efm\u2df7\u2df8\u2e02\u2e05\u6a5dr\u0100;o\u2dfe\u2dff\u6134f\xbb\u2dff\u803b\xaa\u40aa\u803b\xba\u40bagof;\u62b6r;\u6a56lope;\u6a57;\u6a5b\u0180clo\u2e1f\u2e21\u2e27\xf2\u2e01ash\u803b\xf8\u40f8l;\u6298i\u016c\u2e2f\u2e34de\u803b\xf5\u40f5es\u0100;a\u01db\u2e3as;\u6a36ml\u803b\xf6\u40f6bar;\u633d\u0ae1\u2e5e\0\u2e7d\0\u2e80\u2e9d\0\u2ea2\u2eb9\0\0\u2ecb\u0e9c\0\u2f13\0\0\u2f2b\u2fbc\0\u2fc8r\u0200;ast\u0403\u2e67\u2e72\u0e85\u8100\xb6;l\u2e6d\u2e6e\u40b6le\xec\u0403\u0269\u2e78\0\0\u2e7bm;\u6af3;\u6afdy;\u443fr\u0280cimpt\u2e8b\u2e8f\u2e93\u1865\u2e97nt;\u4025od;\u402eil;\u6030enk;\u6031r;\uc000\ud835\udd2d\u0180imo\u2ea8\u2eb0\u2eb4\u0100;v\u2ead\u2eae\u43c6;\u43d5ma\xf4\u0a76ne;\u660e\u0180;tv\u2ebf\u2ec0\u2ec8\u43c0chfork\xbb\u1ffd;\u43d6\u0100au\u2ecf\u2edfn\u0100ck\u2ed5\u2eddk\u0100;h\u21f4\u2edb;\u610e\xf6\u21f4s\u0480;abcdemst\u2ef3\u2ef4\u1908\u2ef9\u2efd\u2f04\u2f06\u2f0a\u2f0e\u402bcir;\u6a23ir;\u6a22\u0100ou\u1d40\u2f02;\u6a25;\u6a72n\u80bb\xb1\u0e9dim;\u6a26wo;\u6a27\u0180ipu\u2f19\u2f20\u2f25ntint;\u6a15f;\uc000\ud835\udd61nd\u803b\xa3\u40a3\u0500;Eaceinosu\u0ec8\u2f3f\u2f41\u2f44\u2f47\u2f81\u2f89\u2f92\u2f7e\u2fb6;\u6ab3p;\u6ab7u\xe5\u0ed9\u0100;c\u0ece\u2f4c\u0300;acens\u0ec8\u2f59\u2f5f\u2f66\u2f68\u2f7eppro\xf8\u2f43urlye\xf1\u0ed9\xf1\u0ece\u0180aes\u2f6f\u2f76\u2f7approx;\u6ab9qq;\u6ab5im;\u62e8i\xed\u0edfme\u0100;s\u2f88\u0eae\u6032\u0180Eas\u2f78\u2f90\u2f7a\xf0\u2f75\u0180dfp\u0eec\u2f99\u2faf\u0180als\u2fa0\u2fa5\u2faalar;\u632eine;\u6312urf;\u6313\u0100;t\u0efb\u2fb4\xef\u0efbrel;\u62b0\u0100ci\u2fc0\u2fc5r;\uc000\ud835\udcc5;\u43c8ncsp;\u6008\u0300fiopsu\u2fda\u22e2\u2fdf\u2fe5\u2feb\u2ff1r;\uc000\ud835\udd2epf;\uc000\ud835\udd62rime;\u6057cr;\uc000\ud835\udcc6\u0180aeo\u2ff8\u3009\u3013t\u0100ei\u2ffe\u3005rnion\xf3\u06b0nt;\u6a16st\u0100;e\u3010\u3011\u403f\xf1\u1f19\xf4\u0f14\u0a80ABHabcdefhilmnoprstux\u3040\u3051\u3055\u3059\u30e0\u310e\u312b\u3147\u3162\u3172\u318e\u3206\u3215\u3224\u3229\u3258\u326e\u3272\u3290\u32b0\u32b7\u0180art\u3047\u304a\u304cr\xf2\u10b3\xf2\u03ddail;\u691car\xf2\u1c65ar;\u6964\u0380cdenqrt\u3068\u3075\u3078\u307f\u308f\u3094\u30cc\u0100eu\u306d\u3071;\uc000\u223d\u0331te;\u4155i\xe3\u116emptyv;\u69b3g\u0200;del\u0fd1\u3089\u308b\u308d;\u6992;\u69a5\xe5\u0fd1uo\u803b\xbb\u40bbr\u0580;abcfhlpstw\u0fdc\u30ac\u30af\u30b7\u30b9\u30bc\u30be\u30c0\u30c3\u30c7\u30cap;\u6975\u0100;f\u0fe0\u30b4s;\u6920;\u6933s;\u691e\xeb\u225d\xf0\u272el;\u6945im;\u6974l;\u61a3;\u619d\u0100ai\u30d1\u30d5il;\u691ao\u0100;n\u30db\u30dc\u6236al\xf3\u0f1e\u0180abr\u30e7\u30ea\u30eer\xf2\u17e5rk;\u6773\u0100ak\u30f3\u30fdc\u0100ek\u30f9\u30fb;\u407d;\u405d\u0100es\u3102\u3104;\u698cl\u0100du\u310a\u310c;\u698e;\u6990\u0200aeuy\u3117\u311c\u3127\u3129ron;\u4159\u0100di\u3121\u3125il;\u4157\xec\u0ff2\xe2\u30fa;\u4440\u0200clqs\u3134\u3137\u313d\u3144a;\u6937dhar;\u6969uo\u0100;r\u020e\u020dh;\u61b3\u0180acg\u314e\u315f\u0f44l\u0200;ips\u0f78\u3158\u315b\u109cn\xe5\u10bbar\xf4\u0fa9t;\u65ad\u0180ilr\u3169\u1023\u316esht;\u697d;\uc000\ud835\udd2f\u0100ao\u3177\u3186r\u0100du\u317d\u317f\xbb\u047b\u0100;l\u1091\u3184;\u696c\u0100;v\u318b\u318c\u43c1;\u43f1\u0180gns\u3195\u31f9\u31fcht\u0300ahlrst\u31a4\u31b0\u31c2\u31d8\u31e4\u31eerrow\u0100;t\u0fdc\u31ada\xe9\u30c8arpoon\u0100du\u31bb\u31bfow\xee\u317ep\xbb\u1092eft\u0100ah\u31ca\u31d0rrow\xf3\u0feaarpoon\xf3\u0551ightarrows;\u61c9quigarro\xf7\u30cbhreetimes;\u62ccg;\u42daingdotse\xf1\u1f32\u0180ahm\u320d\u3210\u3213r\xf2\u0feaa\xf2\u0551;\u600foust\u0100;a\u321e\u321f\u63b1che\xbb\u321fmid;\u6aee\u0200abpt\u3232\u323d\u3240\u3252\u0100nr\u3237\u323ag;\u67edr;\u61fer\xeb\u1003\u0180afl\u3247\u324a\u324er;\u6986;\uc000\ud835\udd63us;\u6a2eimes;\u6a35\u0100ap\u325d\u3267r\u0100;g\u3263\u3264\u4029t;\u6994olint;\u6a12ar\xf2\u31e3\u0200achq\u327b\u3280\u10bc\u3285quo;\u603ar;\uc000\ud835\udcc7\u0100bu\u30fb\u328ao\u0100;r\u0214\u0213\u0180hir\u3297\u329b\u32a0re\xe5\u31f8mes;\u62cai\u0200;efl\u32aa\u1059\u1821\u32ab\u65b9tri;\u69celuhar;\u6968;\u611e\u0d61\u32d5\u32db\u32df\u332c\u3338\u3371\0\u337a\u33a4\0\0\u33ec\u33f0\0\u3428\u3448\u345a\u34ad\u34b1\u34ca\u34f1\0\u3616\0\0\u3633cute;\u415bqu\xef\u27ba\u0500;Eaceinpsy\u11ed\u32f3\u32f5\u32ff\u3302\u330b\u330f\u331f\u3326\u3329;\u6ab4\u01f0\u32fa\0\u32fc;\u6ab8on;\u4161u\xe5\u11fe\u0100;d\u11f3\u3307il;\u415frc;\u415d\u0180Eas\u3316\u3318\u331b;\u6ab6p;\u6abaim;\u62e9olint;\u6a13i\xed\u1204;\u4441ot\u0180;be\u3334\u1d47\u3335\u62c5;\u6a66\u0380Aacmstx\u3346\u334a\u3357\u335b\u335e\u3363\u336drr;\u61d8r\u0100hr\u3350\u3352\xeb\u2228\u0100;o\u0a36\u0a34t\u803b\xa7\u40a7i;\u403bwar;\u6929m\u0100in\u3369\xf0nu\xf3\xf1t;\u6736r\u0100;o\u3376\u2055\uc000\ud835\udd30\u0200acoy\u3382\u3386\u3391\u33a0rp;\u666f\u0100hy\u338b\u338fcy;\u4449;\u4448rt\u026d\u3399\0\0\u339ci\xe4\u1464ara\xec\u2e6f\u803b\xad\u40ad\u0100gm\u33a8\u33b4ma\u0180;fv\u33b1\u33b2\u33b2\u43c3;\u43c2\u0400;deglnpr\u12ab\u33c5\u33c9\u33ce\u33d6\u33de\u33e1\u33e6ot;\u6a6a\u0100;q\u12b1\u12b0\u0100;E\u33d3\u33d4\u6a9e;\u6aa0\u0100;E\u33db\u33dc\u6a9d;\u6a9fe;\u6246lus;\u6a24arr;\u6972ar\xf2\u113d\u0200aeit\u33f8\u3408\u340f\u3417\u0100ls\u33fd\u3404lsetm\xe9\u336ahp;\u6a33parsl;\u69e4\u0100dl\u1463\u3414e;\u6323\u0100;e\u341c\u341d\u6aaa\u0100;s\u3422\u3423\u6aac;\uc000\u2aac\ufe00\u0180flp\u342e\u3433\u3442tcy;\u444c\u0100;b\u3438\u3439\u402f\u0100;a\u343e\u343f\u69c4r;\u633ff;\uc000\ud835\udd64a\u0100dr\u344d\u0402es\u0100;u\u3454\u3455\u6660it\xbb\u3455\u0180csu\u3460\u3479\u349f\u0100au\u3465\u346fp\u0100;s\u1188\u346b;\uc000\u2293\ufe00p\u0100;s\u11b4\u3475;\uc000\u2294\ufe00u\u0100bp\u347f\u348f\u0180;es\u1197\u119c\u3486et\u0100;e\u1197\u348d\xf1\u119d\u0180;es\u11a8\u11ad\u3496et\u0100;e\u11a8\u349d\xf1\u11ae\u0180;af\u117b\u34a6\u05b0r\u0165\u34ab\u05b1\xbb\u117car\xf2\u1148\u0200cemt\u34b9\u34be\u34c2\u34c5r;\uc000\ud835\udcc8tm\xee\xf1i\xec\u3415ar\xe6\u11be\u0100ar\u34ce\u34d5r\u0100;f\u34d4\u17bf\u6606\u0100an\u34da\u34edight\u0100ep\u34e3\u34eapsilo\xee\u1ee0h\xe9\u2eafs\xbb\u2852\u0280bcmnp\u34fb\u355e\u1209\u358b\u358e\u0480;Edemnprs\u350e\u350f\u3511\u3515\u351e\u3523\u352c\u3531\u3536\u6282;\u6ac5ot;\u6abd\u0100;d\u11da\u351aot;\u6ac3ult;\u6ac1\u0100Ee\u3528\u352a;\u6acb;\u628alus;\u6abfarr;\u6979\u0180eiu\u353d\u3552\u3555t\u0180;en\u350e\u3545\u354bq\u0100;q\u11da\u350feq\u0100;q\u352b\u3528m;\u6ac7\u0100bp\u355a\u355c;\u6ad5;\u6ad3c\u0300;acens\u11ed\u356c\u3572\u3579\u357b\u3326ppro\xf8\u32faurlye\xf1\u11fe\xf1\u11f3\u0180aes\u3582\u3588\u331bppro\xf8\u331aq\xf1\u3317g;\u666a\u0680123;Edehlmnps\u35a9\u35ac\u35af\u121c\u35b2\u35b4\u35c0\u35c9\u35d5\u35da\u35df\u35e8\u35ed\u803b\xb9\u40b9\u803b\xb2\u40b2\u803b\xb3\u40b3;\u6ac6\u0100os\u35b9\u35bct;\u6abeub;\u6ad8\u0100;d\u1222\u35c5ot;\u6ac4s\u0100ou\u35cf\u35d2l;\u67c9b;\u6ad7arr;\u697bult;\u6ac2\u0100Ee\u35e4\u35e6;\u6acc;\u628blus;\u6ac0\u0180eiu\u35f4\u3609\u360ct\u0180;en\u121c\u35fc\u3602q\u0100;q\u1222\u35b2eq\u0100;q\u35e7\u35e4m;\u6ac8\u0100bp\u3611\u3613;\u6ad4;\u6ad6\u0180Aan\u361c\u3620\u362drr;\u61d9r\u0100hr\u3626\u3628\xeb\u222e\u0100;o\u0a2b\u0a29war;\u692alig\u803b\xdf\u40df\u0be1\u3651\u365d\u3660\u12ce\u3673\u3679\0\u367e\u36c2\0\0\0\0\0\u36db\u3703\0\u3709\u376c\0\0\0\u3787\u0272\u3656\0\0\u365bget;\u6316;\u43c4r\xeb\u0e5f\u0180aey\u3666\u366b\u3670ron;\u4165dil;\u4163;\u4442lrec;\u6315r;\uc000\ud835\udd31\u0200eiko\u3686\u369d\u36b5\u36bc\u01f2\u368b\0\u3691e\u01004f\u1284\u1281a\u0180;sv\u3698\u3699\u369b\u43b8ym;\u43d1\u0100cn\u36a2\u36b2k\u0100as\u36a8\u36aeppro\xf8\u12c1im\xbb\u12acs\xf0\u129e\u0100as\u36ba\u36ae\xf0\u12c1rn\u803b\xfe\u40fe\u01ec\u031f\u36c6\u22e7es\u8180\xd7;bd\u36cf\u36d0\u36d8\u40d7\u0100;a\u190f\u36d5r;\u6a31;\u6a30\u0180eps\u36e1\u36e3\u3700\xe1\u2a4d\u0200;bcf\u0486\u36ec\u36f0\u36f4ot;\u6336ir;\u6af1\u0100;o\u36f9\u36fc\uc000\ud835\udd65rk;\u6ada\xe1\u3362rime;\u6034\u0180aip\u370f\u3712\u3764d\xe5\u1248\u0380adempst\u3721\u374d\u3740\u3751\u3757\u375c\u375fngle\u0280;dlqr\u3730\u3731\u3736\u3740\u3742\u65b5own\xbb\u1dbbeft\u0100;e\u2800\u373e\xf1\u092e;\u625cight\u0100;e\u32aa\u374b\xf1\u105aot;\u65ecinus;\u6a3alus;\u6a39b;\u69cdime;\u6a3bezium;\u63e2\u0180cht\u3772\u377d\u3781\u0100ry\u3777\u377b;\uc000\ud835\udcc9;\u4446cy;\u445brok;\u4167\u0100io\u378b\u378ex\xf4\u1777head\u0100lr\u3797\u37a0eftarro\xf7\u084fightarrow\xbb\u0f5d\u0900AHabcdfghlmoprstuw\u37d0\u37d3\u37d7\u37e4\u37f0\u37fc\u380e\u381c\u3823\u3834\u3851\u385d\u386b\u38a9\u38cc\u38d2\u38ea\u38f6r\xf2\u03edar;\u6963\u0100cr\u37dc\u37e2ute\u803b\xfa\u40fa\xf2\u1150r\u01e3\u37ea\0\u37edy;\u445eve;\u416d\u0100iy\u37f5\u37farc\u803b\xfb\u40fb;\u4443\u0180abh\u3803\u3806\u380br\xf2\u13adlac;\u4171a\xf2\u13c3\u0100ir\u3813\u3818sht;\u697e;\uc000\ud835\udd32rave\u803b\xf9\u40f9\u0161\u3827\u3831r\u0100lr\u382c\u382e\xbb\u0957\xbb\u1083lk;\u6580\u0100ct\u3839\u384d\u026f\u383f\0\0\u384arn\u0100;e\u3845\u3846\u631cr\xbb\u3846op;\u630fri;\u65f8\u0100al\u3856\u385acr;\u416b\u80bb\xa8\u0349\u0100gp\u3862\u3866on;\u4173f;\uc000\ud835\udd66\u0300adhlsu\u114b\u3878\u387d\u1372\u3891\u38a0own\xe1\u13b3arpoon\u0100lr\u3888\u388cef\xf4\u382digh\xf4\u382fi\u0180;hl\u3899\u389a\u389c\u43c5\xbb\u13faon\xbb\u389aparrows;\u61c8\u0180cit\u38b0\u38c4\u38c8\u026f\u38b6\0\0\u38c1rn\u0100;e\u38bc\u38bd\u631dr\xbb\u38bdop;\u630eng;\u416fri;\u65f9cr;\uc000\ud835\udcca\u0180dir\u38d9\u38dd\u38e2ot;\u62f0lde;\u4169i\u0100;f\u3730\u38e8\xbb\u1813\u0100am\u38ef\u38f2r\xf2\u38a8l\u803b\xfc\u40fcangle;\u69a7\u0780ABDacdeflnoprsz\u391c\u391f\u3929\u392d\u39b5\u39b8\u39bd\u39df\u39e4\u39e8\u39f3\u39f9\u39fd\u3a01\u3a20r\xf2\u03f7ar\u0100;v\u3926\u3927\u6ae8;\u6ae9as\xe8\u03e1\u0100nr\u3932\u3937grt;\u699c\u0380eknprst\u34e3\u3946\u394b\u3952\u395d\u3964\u3996app\xe1\u2415othin\xe7\u1e96\u0180hir\u34eb\u2ec8\u3959op\xf4\u2fb5\u0100;h\u13b7\u3962\xef\u318d\u0100iu\u3969\u396dgm\xe1\u33b3\u0100bp\u3972\u3984setneq\u0100;q\u397d\u3980\uc000\u228a\ufe00;\uc000\u2acb\ufe00setneq\u0100;q\u398f\u3992\uc000\u228b\ufe00;\uc000\u2acc\ufe00\u0100hr\u399b\u399fet\xe1\u369ciangle\u0100lr\u39aa\u39afeft\xbb\u0925ight\xbb\u1051y;\u4432ash\xbb\u1036\u0180elr\u39c4\u39d2\u39d7\u0180;be\u2dea\u39cb\u39cfar;\u62bbq;\u625alip;\u62ee\u0100bt\u39dc\u1468a\xf2\u1469r;\uc000\ud835\udd33tr\xe9\u39aesu\u0100bp\u39ef\u39f1\xbb\u0d1c\xbb\u0d59pf;\uc000\ud835\udd67ro\xf0\u0efbtr\xe9\u39b4\u0100cu\u3a06\u3a0br;\uc000\ud835\udccb\u0100bp\u3a10\u3a18n\u0100Ee\u3980\u3a16\xbb\u397en\u0100Ee\u3992\u3a1e\xbb\u3990igzag;\u699a\u0380cefoprs\u3a36\u3a3b\u3a56\u3a5b\u3a54\u3a61\u3a6airc;\u4175\u0100di\u3a40\u3a51\u0100bg\u3a45\u3a49ar;\u6a5fe\u0100;q\u15fa\u3a4f;\u6259erp;\u6118r;\uc000\ud835\udd34pf;\uc000\ud835\udd68\u0100;e\u1479\u3a66at\xe8\u1479cr;\uc000\ud835\udccc\u0ae3\u178e\u3a87\0\u3a8b\0\u3a90\u3a9b\0\0\u3a9d\u3aa8\u3aab\u3aaf\0\0\u3ac3\u3ace\0\u3ad8\u17dc\u17dftr\xe9\u17d1r;\uc000\ud835\udd35\u0100Aa\u3a94\u3a97r\xf2\u03c3r\xf2\u09f6;\u43be\u0100Aa\u3aa1\u3aa4r\xf2\u03b8r\xf2\u09eba\xf0\u2713is;\u62fb\u0180dpt\u17a4\u3ab5\u3abe\u0100fl\u3aba\u17a9;\uc000\ud835\udd69im\xe5\u17b2\u0100Aa\u3ac7\u3acar\xf2\u03cer\xf2\u0a01\u0100cq\u3ad2\u17b8r;\uc000\ud835\udccd\u0100pt\u17d6\u3adcr\xe9\u17d4\u0400acefiosu\u3af0\u3afd\u3b08\u3b0c\u3b11\u3b15\u3b1b\u3b21c\u0100uy\u3af6\u3afbte\u803b\xfd\u40fd;\u444f\u0100iy\u3b02\u3b06rc;\u4177;\u444bn\u803b\xa5\u40a5r;\uc000\ud835\udd36cy;\u4457pf;\uc000\ud835\udd6acr;\uc000\ud835\udcce\u0100cm\u3b26\u3b29y;\u444el\u803b\xff\u40ff\u0500acdefhiosw\u3b42\u3b48\u3b54\u3b58\u3b64\u3b69\u3b6d\u3b74\u3b7a\u3b80cute;\u417a\u0100ay\u3b4d\u3b52ron;\u417e;\u4437ot;\u417c\u0100et\u3b5d\u3b61tr\xe6\u155fa;\u43b6r;\uc000\ud835\udd37cy;\u4436grarr;\u61ddpf;\uc000\ud835\udd6bcr;\uc000\ud835\udccf\u0100jn\u3b85\u3b87;\u600dj;\u600c'.split("").map((t=>t.charCodeAt(0)))),w=new Uint16Array("\u0200aglq\t\x15\x18\x1b\u026d\x0f\0\0\x12p;\u4026os;\u4027t;\u403et;\u403cuot;\u4022".split("").map((t=>t.charCodeAt(0))));const F=new Map([[0,65533],[128,8364],[130,8218],[131,402],[132,8222],[133,8230],[134,8224],[135,8225],[136,710],[137,8240],[138,352],[139,8249],[140,338],[142,381],[145,8216],[146,8217],[147,8220],[148,8221],[149,8226],[150,8211],[151,8212],[152,732],[153,8482],[154,353],[155,8250],[156,339],[158,382],[159,376]]),v=null!==(k=String.fromCodePoint)&&void 0!==k?k:function(t){let e="";return t>65535&&(t-=65536,e+=String.fromCharCode(t>>>10&1023|55296),t=56320|1023&t),e+=String.fromCharCode(t),e};var z;!function(t){t[t.NUM=35]="NUM",t[t.SEMI=59]="SEMI",t[t.EQUALS=61]="EQUALS",t[t.ZERO=48]="ZERO",t[t.NINE=57]="NINE",t[t.LOWER_A=97]="LOWER_A",t[t.LOWER_F=102]="LOWER_F",t[t.LOWER_X=120]="LOWER_X",t[t.LOWER_Z=122]="LOWER_Z",t[t.UPPER_A=65]="UPPER_A",t[t.UPPER_F=70]="UPPER_F",t[t.UPPER_Z=90]="UPPER_Z"}(z||(z={}));var S,q,L;function I(t){return t>=z.ZERO&&t<=z.NINE}function M(t){return t>=z.UPPER_A&&t<=z.UPPER_F||t>=z.LOWER_A&&t<=z.LOWER_F}function T(t){return t===z.EQUALS||function(t){return t>=z.UPPER_A&&t<=z.UPPER_Z||t>=z.LOWER_A&&t<=z.LOWER_Z||I(t)}(t)}!function(t){t[t.VALUE_LENGTH=49152]="VALUE_LENGTH",t[t.BRANCH_LENGTH=16256]="BRANCH_LENGTH",t[t.JUMP_TABLE=127]="JUMP_TABLE"}(S||(S={})),function(t){t[t.EntityStart=0]="EntityStart",t[t.NumericStart=1]="NumericStart",t[t.NumericDecimal=2]="NumericDecimal",t[t.NumericHex=3]="NumericHex",t[t.NamedEntity=4]="NamedEntity"}(q||(q={})),function(t){t[t.Legacy=0]="Legacy",t[t.Strict=1]="Strict",t[t.Attribute=2]="Attribute"}(L||(L={}));class R{constructor(t,e,r){this.decodeTree=t,this.emitCodePoint=e,this.errors=r,this.state=q.EntityStart,this.consumed=1,this.result=0,this.treeIndex=0,this.excess=1,this.decodeMode=L.Strict}startEntity(t){this.decodeMode=t,this.state=q.EntityStart,this.result=0,this.treeIndex=0,this.excess=1,this.consumed=1}write(t,e){switch(this.state){case q.EntityStart:return t.charCodeAt(e)===z.NUM?(this.state=q.NumericStart,this.consumed+=1,this.stateNumericStart(t,e+1)):(this.state=q.NamedEntity,this.stateNamedEntity(t,e));case q.NumericStart:return this.stateNumericStart(t,e);case q.NumericDecimal:return this.stateNumericDecimal(t,e);case q.NumericHex:return this.stateNumericHex(t,e);case q.NamedEntity:return this.stateNamedEntity(t,e)}}stateNumericStart(t,e){return e>=t.length?-1:(32|t.charCodeAt(e))===z.LOWER_X?(this.state=q.NumericHex,this.consumed+=1,this.stateNumericHex(t,e+1)):(this.state=q.NumericDecimal,this.stateNumericDecimal(t,e))}addToNumericResult(t,e,r,n){if(e!==r){const s=r-e;this.result=this.result*Math.pow(n,s)+parseInt(t.substr(e,s),n),this.consumed+=s}}stateNumericHex(t,e){const r=e;for(;e=55296&&t<=57343||t>1114111?65533:null!==(e=F.get(t))&&void 0!==e?e:t}(this.result),this.consumed),this.errors&&(t!==z.SEMI&&this.errors.missingSemicolonAfterCharacterReference(),this.errors.validateNumericCharacterReference(this.result)),this.consumed}stateNamedEntity(t,e){const{decodeTree:r}=this;let n=r[this.treeIndex],s=(n&S.VALUE_LENGTH)>>14;for(;e>14,0!==s){if(i===z.SEMI)return this.emitNamedEntityData(this.treeIndex,s,this.consumed+this.excess);this.decodeMode!==L.Strict&&(this.result=this.treeIndex,this.consumed+=this.excess,this.excess=0)}}return-1}emitNotTerminatedNamedEntity(){var t;const{result:e,decodeTree:r}=this,n=(r[e]&S.VALUE_LENGTH)>>14;return this.emitNamedEntityData(e,n,this.consumed),null===(t=this.errors)||void 0===t||t.missingSemicolonAfterCharacterReference(),this.consumed}emitNamedEntityData(t,e,r){const{decodeTree:n}=this;return this.emitCodePoint(1===e?n[t]&~S.VALUE_LENGTH:n[t+1],r),3===e&&this.emitCodePoint(n[t+2],r),r}end(){var t;switch(this.state){case q.NamedEntity:return 0===this.result||this.decodeMode===L.Attribute&&this.result!==this.treeIndex?0:this.emitNotTerminatedNamedEntity();case q.NumericDecimal:return this.emitNumericEntity(0,2);case q.NumericHex:return this.emitNumericEntity(0,3);case q.NumericStart:return null===(t=this.errors)||void 0===t||t.absenceOfDigitsInNumericCharacterReference(this.consumed),0;case q.EntityStart:return 0}}}function B(t){let e="";const r=new R(t,(t=>e+=v(t)));return function(t,n){let s=0,i=0;for(;(i=t.indexOf("&",i))>=0;){e+=t.slice(s,i),r.startEntity(n);const o=r.write(t,i+1);if(o<0){s=i+r.end();break}s=i+o,i=0===o?s+1:s}const o=e+t.slice(s);return e="",o}}function N(t,e,r,n){const s=(e&S.BRANCH_LENGTH)>>7,i=e&S.JUMP_TABLE;if(0===s)return 0!==i&&n===i?r:-1;if(i){const e=n-i;return e<0||e>=s?-1:t[r+e]-1}let o=r,c=o+s-1;for(;o<=c;){const e=o+c>>>1,r=t[e];if(rn))return t[e+s];c=e-1}}return-1}const P=B(D);function O(t,e=L.Legacy){return P(t,e)}function j(t){return"[object String]"===function(t){return Object.prototype.toString.call(t)}(t)}B(w);const Z=Object.prototype.hasOwnProperty;function $(t){return Array.prototype.slice.call(arguments,1).forEach((function(e){if(e){if("object"!=typeof e)throw new TypeError(e+"must be object");Object.keys(e).forEach((function(r){t[r]=e[r]}))}})),t}function U(t,e,r){return[].concat(t.slice(0,e),r,t.slice(e+1))}function H(t){return!(t>=55296&&t<=57343)&&(!(t>=64976&&t<=65007)&&(65535!=(65535&t)&&65534!=(65535&t)&&(!(t>=0&&t<=8)&&(11!==t&&(!(t>=14&&t<=31)&&(!(t>=127&&t<=159)&&!(t>1114111)))))))}function V(t){if(t>65535){const e=55296+((t-=65536)>>10),r=56320+(1023&t);return String.fromCharCode(e,r)}return String.fromCharCode(t)}const G=/\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/g,W=new RegExp(G.source+"|"+/&([a-z#][a-z0-9]{1,31});/gi.source,"gi"),J=/^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))$/i;function Q(t){return t.indexOf("\\")<0&&t.indexOf("&")<0?t:t.replace(W,(function(t,e,r){return e||function(t,e){if(35===e.charCodeAt(0)&&J.test(e)){const r="x"===e[1].toLowerCase()?parseInt(e.slice(2),16):parseInt(e.slice(1),10);return H(r)?V(r):t}const r=O(t);return r!==t?r:t}(t,r)}))}const X=/[&<>"]/,Y=/[&<>"]/g,K={"&":"&","<":"<",">":">",'"':"""};function tt(t){return K[t]}function et(t){return X.test(t)?t.replace(Y,tt):t}const rt=/[.?*+^$[\]\\(){}|-]/g;function nt(t){switch(t){case 9:case 32:return!0}return!1}function st(t){if(t>=8192&&t<=8202)return!0;switch(t){case 9:case 10:case 11:case 12:case 13:case 32:case 160:case 5760:case 8239:case 8287:case 12288:return!0}return!1}function it(t){return A.test(t)}function ot(t){switch(t){case 33:case 34:case 35:case 36:case 37:case 38:case 39:case 40:case 41:case 42:case 43:case 44:case 45:case 46:case 47:case 58:case 59:case 60:case 61:case 62:case 63:case 64:case 91:case 92:case 93:case 94:case 95:case 96:case 123:case 124:case 125:case 126:return!0;default:return!1}}function ct(t){return t=t.trim().replace(/\s+/g," "),"\u1e7e"==="\u1e9e".toLowerCase()&&(t=t.replace(/\u1e9e/g,"\xdf")),t.toLowerCase().toUpperCase()}const at={mdurl:y,ucmicro:x};var lt=Object.freeze({__proto__:null,arrayReplaceAt:U,assign:$,escapeHtml:et,escapeRE:function(t){return t.replace(rt,"\\$&")},fromCodePoint:V,has:function(t,e){return Z.call(t,e)},isMdAsciiPunct:ot,isPunctChar:it,isSpace:nt,isString:j,isValidEntityCode:H,isWhiteSpace:st,lib:at,normalizeReference:ct,unescapeAll:Q,unescapeMd:function(t){return t.indexOf("\\")<0?t:t.replace(G,"$1")}});var ut=Object.freeze({__proto__:null,parseLinkDestination:function(t,e,r){let n,s=e;const i={ok:!1,pos:0,lines:0,str:""};if(60===t.charCodeAt(s)){for(s++;s32))return i;if(41===n){if(0===o)break;o--}s++}return e===s||0!==o||(i.str=Q(t.slice(e,s)),i.pos=s,i.ok=!0),i},parseLinkLabel:function(t,e,r){let n,s,i,o;const c=t.posMax,a=t.pos;for(t.pos=e+1,n=1;t.pos=r)return c;if(s=t.charCodeAt(o),34!==s&&39!==s&&40!==s)return c;for(o++,40===s&&(s=41);o"+et(i.content)+""},ht.code_block=function(t,e,r,n,s){const i=t[e];return""+et(t[e].content)+"
\n"},ht.fence=function(t,e,r,n,s){const i=t[e],o=i.info?Q(i.info).trim():"";let c,a="",l="";if(o){const t=o.split(/(\s+)/g);a=t[0],l=t.slice(2).join("")}if(c=r.highlight&&r.highlight(i.content,a,l)||et(i.content),0===c.indexOf("${c}
\n`}return`${c}
\n`},ht.image=function(t,e,r,n,s){const i=t[e];return i.attrs[i.attrIndex("alt")][1]=s.renderInlineAsText(i.children,r,n),s.renderToken(t,e,r)},ht.hardbreak=function(t,e,r){return r.xhtmlOut?"
\n":"
\n"},ht.softbreak=function(t,e,r){return r.breaks?r.xhtmlOut?"
\n":"
\n":"\n"},ht.text=function(t,e){return et(t[e].content)},ht.html_block=function(t,e){return t[e].content},ht.html_inline=function(t,e){return t[e].content},pt.prototype.renderAttrs=function(t){let e,r,n;if(!t.attrs)return"";for(n="",e=0,r=t.attrs.length;e\n":">",s},pt.prototype.renderInline=function(t,e,r){let n="";const s=this.rules;for(let i=0,o=t.length;i=0&&(r=this.attrs[e][1]),r},dt.prototype.attrJoin=function(t,e){const r=this.attrIndex(t);r<0?this.attrPush([t,e]):this.attrs[r][1]=this.attrs[r][1]+" "+e},_t.prototype.Token=dt;const mt=/\r\n?|\n/g,gt=/\0/g;function kt(t){return/^<\/a\s*>/i.test(t)}const yt=/\+-|\.\.|\?\?\?\?|!!!!|,,|--/,bt=/\((c|tm|r)\)/i,Ct=/\((c|tm|r)\)/gi,At={c:"\xa9",r:"\xae",tm:"\u2122"};function Et(t,e){return At[e.toLowerCase()]}function xt(t){let e=0;for(let r=t.length-1;r>=0;r--){const n=t[r];"text"!==n.type||e||(n.content=n.content.replace(Ct,Et)),"link_open"===n.type&&"auto"===n.info&&e--,"link_close"===n.type&&"auto"===n.info&&e++}}function Dt(t){let e=0;for(let r=t.length-1;r>=0;r--){const n=t[r];"text"!==n.type||e||yt.test(n.content)&&(n.content=n.content.replace(/\+-/g,"\xb1").replace(/\.{2,}/g,"\u2026").replace(/([?!])\u2026/g,"$1..").replace(/([?!]){4,}/g,"$1$1$1").replace(/,{2,}/g,",").replace(/(^|[^-])---(?=[^-]|$)/gm,"$1\u2014").replace(/(^|\s)--(?=\s|$)/gm,"$1\u2013").replace(/(^|[^-\s])--(?=[^-\s]|$)/gm,"$1\u2013")),"link_open"===n.type&&"auto"===n.info&&e--,"link_close"===n.type&&"auto"===n.info&&e++}}const wt=/['"]/,Ft=/['"]/g,vt="\u2019";function zt(t,e,r){return t.slice(0,e)+r+t.slice(e+1)}function St(t,e){let r;const n=[];for(let s=0;s=0&&!(n[r].level<=o);r--);if(n.length=r+1,"text"!==i.type)continue;let c=i.content,a=0,l=c.length;t:for(;a=0)d=c.charCodeAt(u.index-1);else for(r=s-1;r>=0&&("softbreak"!==t[r].type&&"hardbreak"!==t[r].type);r--)if(t[r].content){d=t[r].content.charCodeAt(t[r].content.length-1);break}let _=32;if(a=48&&d<=57&&(p=h=!1),h&&p&&(h=m,p=g),h||p){if(p)for(r=n.length-1;r>=0;r--){let h=n[r];if(n[r].level=0;o--){const c=s[o];if("link_close"!==c.type){if("html_inline"===c.type&&(r=c.content,/^\s]/i.test(r)&&i>0&&i--,kt(c.content)&&i++),!(i>0)&&"text"===c.type&&t.md.linkify.test(c.content)){const r=c.content;let i=t.md.linkify.match(r);const a=[];let l=c.level,u=0;i.length>0&&0===i[0].index&&o>0&&"text_special"===s[o-1].type&&(i=i.slice(1));for(let e=0;eu){const e=new t.Token("text","",0);e.content=r.slice(u,c),e.level=l,a.push(e)}const h=new t.Token("link_open","a",1);h.attrs=[["href",s]],h.level=l++,h.markup="linkify",h.info="auto",a.push(h);const p=new t.Token("text","",0);p.content=o,p.level=l,a.push(p);const f=new t.Token("link_close","a",-1);f.level=--l,f.markup="linkify",f.info="auto",a.push(f),u=i[e].lastIndex}if(u=0;e--)"inline"===t.tokens[e].type&&(bt.test(t.tokens[e].content)&&xt(t.tokens[e].children),yt.test(t.tokens[e].content)&&Dt(t.tokens[e].children))}],["smartquotes",function(t){if(t.md.options.typographer)for(let e=t.tokens.length-1;e>=0;e--)"inline"===t.tokens[e].type&&wt.test(t.tokens[e].content)&&St(t.tokens[e].children,t)}],["text_join",function(t){let e,r;const n=t.tokens,s=n.length;for(let t=0;t=n)return-1;let i=t.src.charCodeAt(s++);if(i<48||i>57)return-1;for(;;){if(s>=n)return-1;if(i=t.src.charCodeAt(s++),!(i>=48&&i<=57)){if(41===i||46===i)break;return-1}if(s-r>=10)return-1}return s0&&this.level++,this.tokens.push(n),n},It.prototype.isEmpty=function(t){return this.bMarks[t]+this.tShift[t]>=this.eMarks[t]},It.prototype.skipEmptyLines=function(t){for(let e=this.lineMax;te;)if(!nt(this.src.charCodeAt(--t)))return t+1;return t},It.prototype.skipChars=function(t,e){for(let r=this.src.length;tr;)if(e!==this.src.charCodeAt(--t))return t+1;return t},It.prototype.getLines=function(t,e,r,n){if(t>=e)return"";const s=new Array(e-t);for(let i=0,o=t;or?new Array(t-r+1).join(" ")+this.src.slice(l,a):this.src.slice(l,a)}return s.join("")},It.prototype.Token=dt;const Nt="<[A-Za-z][A-Za-z0-9\\-]*(?:\\s+[a-zA-Z_:][a-zA-Z0-9:._-]*(?:\\s*=\\s*(?:[^\"'=<>`\\x00-\\x20]+|'[^']*'|\"[^\"]*\"))?)*\\s*\\/?>",Pt="<\\/[A-Za-z][A-Za-z0-9\\-]*\\s*>",Ot=new RegExp("^(?:"+Nt+"|"+Pt+"|\x3c!----\x3e|\x3c!--(?:-?[^>-])(?:-?[^-])*--\x3e|<[?][\\s\\S]*?[?]>|]*>|)"),jt=new RegExp("^(?:"+Nt+"|"+Pt+")"),Zt=[[/^<(script|pre|style|textarea)(?=(\s|>|$))/i,/<\/(script|pre|style|textarea)>/i,!0],[/^/,!0],[/^<\?/,/\?>/,!0],[/^/,!0],[/^/,!0],[new RegExp("^?("+["address","article","aside","base","basefont","blockquote","body","caption","center","col","colgroup","dd","details","dialog","dir","div","dl","dt","fieldset","figcaption","figure","footer","form","frame","frameset","h1","h2","h3","h4","h5","h6","head","header","hr","html","iframe","legend","li","link","main","menu","menuitem","nav","noframes","ol","optgroup","option","p","param","section","source","summary","table","tbody","td","tfoot","th","thead","title","tr","track","ul"].join("|")+")(?=(\\s|/?>|$))","i"),/^$/,!0],[new RegExp(jt.source+"\\s*$"),/^$/,!1]];const $t=[["table",function(t,e,r,n){if(e+2>r)return!1;let s=e+1;if(t.sCount[s]=4)return!1;let i=t.bMarks[s]+t.tShift[s];if(i>=t.eMarks[s])return!1;const o=t.src.charCodeAt(i++);if(124!==o&&45!==o&&58!==o)return!1;if(i>=t.eMarks[s])return!1;const c=t.src.charCodeAt(i++);if(124!==c&&45!==c&&58!==c&&!nt(c))return!1;if(45===o&&nt(c))return!1;for(;i=4)return!1;l=Tt(a),l.length&&""===l[0]&&l.shift(),l.length&&""===l[l.length-1]&&l.pop();const h=l.length;if(0===h||h!==u.length)return!1;if(n)return!0;const p=t.parentType;t.parentType="table";const f=t.md.block.ruler.getRules("blockquote"),d=[e,0];t.push("table_open","table",1).map=d,t.push("thead_open","thead",1).map=[e,e+1],t.push("tr_open","tr",1).map=[e,e+1];for(let e=0;e=4)break;if(l=Tt(a),l.length&&""===l[0]&&l.shift(),l.length&&""===l[l.length-1]&&l.pop(),s===e+2){t.push("tbody_open","tbody",1).map=_=[e+2,0]}t.push("tr_open","tr",1).map=[s,s+1];for(let e=0;e=4))break;n++,s=n}t.line=s;const i=t.push("code_block","code",0);return i.content=t.getLines(e,s,4+t.blkIndent,!1)+"\n",i.map=[e,t.line],!0}],["fence",function(t,e,r,n){let s=t.bMarks[e]+t.tShift[e],i=t.eMarks[e];if(t.sCount[e]-t.blkIndent>=4)return!1;if(s+3>i)return!1;const o=t.src.charCodeAt(s);if(126!==o&&96!==o)return!1;let c=s;s=t.skipChars(s,o);let a=s-c;if(a<3)return!1;const l=t.src.slice(c,s),u=t.src.slice(s,i);if(96===o&&u.indexOf(String.fromCharCode(o))>=0)return!1;if(n)return!0;let h=e,p=!1;for(;(h++,!(h>=r))&&(s=c=t.bMarks[h]+t.tShift[h],i=t.eMarks[h],!(s=4||(s=t.skipChars(s,o),s-c=4)return!1;if(62!==t.src.charCodeAt(s))return!1;if(n)return!0;const c=[],a=[],l=[],u=[],h=t.md.block.ruler.getRules("blockquote"),p=t.parentType;t.parentType="blockquote";let f,d=!1;for(f=e;f=i)break;if(62===t.src.charCodeAt(s++)&&!e){let e,r,n=t.sCount[f]+1;32===t.src.charCodeAt(s)?(s++,n++,r=!1,e=!0):9===t.src.charCodeAt(s)?(e=!0,(t.bsCount[f]+n)%4==3?(s++,n++,r=!1):r=!0):e=!1;let o=n;for(c.push(t.bMarks[f]),t.bMarks[f]=s;s=i,a.push(t.bsCount[f]),t.bsCount[f]=t.sCount[f]+1+(e?1:0),l.push(t.sCount[f]),t.sCount[f]=o-n,u.push(t.tShift[f]),t.tShift[f]=s-t.bMarks[f];continue}if(d)break;let n=!1;for(let e=0,s=h.length;e";const g=[e,0];m.map=g,t.md.block.tokenize(t,e,f),t.push("blockquote_close","blockquote",-1).markup=">",t.lineMax=o,t.parentType=p,g[1]=t.line;for(let r=0;r=4)return!1;let i=t.bMarks[e]+t.tShift[e];const o=t.src.charCodeAt(i++);if(42!==o&&45!==o&&95!==o)return!1;let c=1;for(;i=4)return!1;if(t.listIndent>=0&&t.sCount[a]-t.listIndent>=4&&t.sCount[a]=t.blkIndent&&(f=!0),(p=Bt(t,a))>=0){if(u=!0,o=t.bMarks[a]+t.tShift[a],h=Number(t.src.slice(o,p-1)),f&&1!==h)return!1}else{if(!((p=Rt(t,a))>=0))return!1;u=!1}if(f&&t.skipSpaces(p)>=t.eMarks[a])return!1;if(n)return!0;const d=t.src.charCodeAt(p-1),_=t.tokens.length;u?(c=t.push("ordered_list_open","ol",1),1!==h&&(c.attrs=[["start",h]])):c=t.push("bullet_list_open","ul",1);const m=[a,0];c.map=m,c.markup=String.fromCharCode(d);let g=!1;const k=t.md.block.ruler.getRules("list"),y=t.parentType;for(t.parentType="list";a=s?1:n-e,f>4&&(f=1);const _=e+f;c=t.push("list_item_open","li",1),c.markup=String.fromCharCode(d);const m=[a,0];c.map=m,u&&(c.info=t.src.slice(o,p-1));const y=t.tight,b=t.tShift[a],C=t.sCount[a],A=t.listIndent;if(t.listIndent=t.blkIndent,t.blkIndent=_,t.tight=!0,t.tShift[a]=h-t.bMarks[a],t.sCount[a]=n,h>=s&&t.isEmpty(a+1)?t.line=Math.min(t.line+2,r):t.md.block.tokenize(t,a,r,!0),t.tight&&!g||(l=!1),g=t.line-a>1&&t.isEmpty(t.line-1),t.blkIndent=t.listIndent,t.listIndent=A,t.tShift[a]=b,t.sCount[a]=C,t.tight=y,c=t.push("list_item_close","li",-1),c.markup=String.fromCharCode(d),a=t.line,m[1]=a,a>=r)break;if(t.sCount[a]=4)break;let E=!1;for(let e=0,n=k.length;e=4)return!1;if(91!==t.src.charCodeAt(i))return!1;for(;++i3)continue;if(t.sCount[c]<0)continue;let e=!1;for(let r=0,n=l.length;r=4)return!1;if(!t.md.options.html)return!1;if(60!==t.src.charCodeAt(s))return!1;let o=t.src.slice(s,i),c=0;for(;c=4)return!1;let o=t.src.charCodeAt(s);if(35!==o||s>=i)return!1;let c=1;for(o=t.src.charCodeAt(++s);35===o&&s6||ss&&nt(t.src.charCodeAt(a-1))&&(i=a),t.line=e+1;const l=t.push("heading_open","h"+String(c),1);l.markup="########".slice(0,c),l.map=[e,t.line];const u=t.push("inline","",0);return u.content=t.src.slice(s,i).trim(),u.map=[e,t.line],u.children=[],t.push("heading_close","h"+String(c),-1).markup="########".slice(0,c),!0},["paragraph","reference","blockquote"]],["lheading",function(t,e,r){const n=t.md.block.ruler.getRules("paragraph");if(t.sCount[e]-t.blkIndent>=4)return!1;const s=t.parentType;t.parentType="paragraph";let i,o=0,c=e+1;for(;c3)continue;if(t.sCount[c]>=t.blkIndent){let e=t.bMarks[c]+t.tShift[c];const r=t.eMarks[c];if(e=r))){o=61===i?1:2;break}}if(t.sCount[c]<0)continue;let e=!1;for(let s=0,i=n.length;s3)continue;if(t.sCount[i]<0)continue;let e=!1;for(let s=0,o=n.length;s=r))&&!(t.sCount[o]=i){t.line=r;break}const e=t.line;let a=!1;for(let i=0;i=t.line)throw new Error("block rule didn't increment state.line");break}if(!a)throw new Error("none of the block rules matched");t.tight=!c,t.isEmpty(t.line-1)&&(c=!0),o=t.line,o0&&(this.level++,this._prev_delimiters.push(this.delimiters),this.delimiters=[],s={delimiters:this.delimiters}),this.pendingLevel=this.level,this.tokens.push(n),this.tokens_meta.push(s),n},Ht.prototype.scanDelims=function(t,e){let r,n,s=!0,i=!0;const o=this.posMax,c=this.src.charCodeAt(t),a=t>0?this.src.charCodeAt(t-1):32;let l=t;for(;l?@[]^_`{|}~-".split("").forEach((function(t){Wt[t.charCodeAt(0)]=1}));var Qt={tokenize:function(t,e){const r=t.pos,n=t.src.charCodeAt(r);if(e)return!1;if(126!==n)return!1;const s=t.scanDelims(t.pos,!0);let i=s.length;const o=String.fromCharCode(n);if(i<2)return!1;let c;i%2&&(c=t.push("text","",0),c.content=o,i--);for(let e=0;e=0;r--){const n=e[r];if(95!==n.marker&&42!==n.marker)continue;if(-1===n.end)continue;const s=e[n.end],i=r>0&&e[r-1].end===n.end+1&&e[r-1].marker===n.marker&&e[r-1].token===n.token-1&&e[n.end+1].token===s.token+1,o=String.fromCharCode(n.marker),c=t.tokens[n.token];c.type=i?"strong_open":"em_open",c.tag=i?"strong":"em",c.nesting=1,c.markup=i?o+o:o,c.content="";const a=t.tokens[s.token];a.type=i?"strong_close":"em_close",a.tag=i?"strong":"em",a.nesting=-1,a.markup=i?o+o:o,a.content="",i&&(t.tokens[e[r-1].token].content="",t.tokens[e[n.end+1].token].content="",r--)}}var Yt={tokenize:function(t,e){const r=t.pos,n=t.src.charCodeAt(r);if(e)return!1;if(95!==n&&42!==n)return!1;const s=t.scanDelims(t.pos,42===n);for(let e=0;e\x00-\x20]*)$/;const ee=/^((?:x[a-f0-9]{1,6}|[0-9]{1,7}));/i,re=/^&([a-z][a-z0-9]{1,31});/i;function ne(t){const e={},r=t.length;if(!r)return;let n=0,s=-2;const i=[];for(let o=0;oc;a-=i[a]+1){const e=t[a];if(e.marker===r.marker&&(e.open&&e.end<0)){let n=!1;if((e.close||r.open)&&(e.length+r.length)%3==0&&(e.length%3==0&&r.length%3==0||(n=!0)),!n){const n=a>0&&!t[a-1].open?i[a-1]+1:0;i[o]=o-a+n,i[a]=n,r.open=!1,e.end=o,e.close=!1,l=-1,s=-2;break}}}-1!==l&&(e[r.marker][(r.open?3:0)+(r.length||0)%3]=l)}}const se=[["text",function(t,e){let r=t.pos;for(;r0)return!1;const r=t.pos;if(r+3>t.posMax)return!1;if(58!==t.src.charCodeAt(r))return!1;if(47!==t.src.charCodeAt(r+1))return!1;if(47!==t.src.charCodeAt(r+2))return!1;const n=t.pending.match(Gt);if(!n)return!1;const s=n[1],i=t.md.linkify.matchAtStart(t.src.slice(r-s.length));if(!i)return!1;let o=i.url;if(o.length<=s.length)return!1;o=o.replace(/\*+$/,"");const c=t.md.normalizeLink(o);if(!t.md.validateLink(c))return!1;if(!e){t.pending=t.pending.slice(0,-s.length);const e=t.push("link_open","a",1);e.attrs=[["href",c]],e.markup="linkify",e.info="auto";t.push("text","",0).content=t.md.normalizeLinkText(o);const r=t.push("link_close","a",-1);r.markup="linkify",r.info="auto"}return t.pos+=o.length-s.length,!0}],["newline",function(t,e){let r=t.pos;if(10!==t.src.charCodeAt(r))return!1;const n=t.pending.length-1,s=t.posMax;if(!e)if(n>=0&&32===t.pending.charCodeAt(n))if(n>=1&&32===t.pending.charCodeAt(n-1)){let e=n-1;for(;e>=1&&32===t.pending.charCodeAt(e-1);)e--;t.pending=t.pending.slice(0,e),t.push("hardbreak","br",0)}else t.pending=t.pending.slice(0,-1),t.push("softbreak","br",0);else t.push("softbreak","br",0);for(r++;r=n)return!1;let s=t.src.charCodeAt(r);if(10===s){for(e||t.push("hardbreak","br",0),r++;r=55296&&s<=56319&&r+1=56320&&e<=57343&&(i+=t.src[r+1],r++)}const o="\\"+i;if(!e){const e=t.push("text_special","",0);s<256&&0!==Wt[s]?e.content=i:e.content=o,e.markup=o,e.info="escape"}return t.pos=r+1,!0}],["backticks",function(t,e){let r=t.pos;if(96!==t.src.charCodeAt(r))return!1;const n=r;r++;const s=t.posMax;for(;r=h)return!1;if(a=d,s=t.md.helpers.parseLinkDestination(t.src,d,t.posMax),s.ok){for(o=t.md.normalizeLink(s.str),t.md.validateLink(o)?d=s.pos:o="",a=d;d=h||41!==t.src.charCodeAt(d))&&(l=!0),d++}if(l){if(void 0===t.env.references)return!1;if(d=0?n=t.src.slice(a,d++):d=f+1):d=f+1,n||(n=t.src.slice(p,f)),i=t.env.references[ct(n)],!i)return t.pos=u,!1;o=i.href,c=i.title}if(!e){t.pos=p,t.posMax=f;const e=[["href",o]];t.push("link_open","a",1).attrs=e,c&&e.push(["title",c]),t.linkLevel++,t.md.inline.tokenize(t),t.linkLevel--,t.push("link_close","a",-1)}return t.pos=d,t.posMax=h,!0}],["image",function(t,e){let r,n,s,i,o,c,a,l,u="";const h=t.pos,p=t.posMax;if(33!==t.src.charCodeAt(t.pos))return!1;if(91!==t.src.charCodeAt(t.pos+1))return!1;const f=t.pos+2,d=t.md.helpers.parseLinkLabel(t,t.pos+1,!1);if(d<0)return!1;if(i=d+1,i=p)return!1;for(l=i,c=t.md.helpers.parseLinkDestination(t.src,i,t.posMax),c.ok&&(u=t.md.normalizeLink(c.str),t.md.validateLink(u)?i=c.pos:u=""),l=i;i
=p||41!==t.src.charCodeAt(i))return t.pos=h,!1;i++}else{if(void 0===t.env.references)return!1;if(i
=0?s=t.src.slice(l,i++):i=d+1):i=d+1,s||(s=t.src.slice(f,d)),o=t.env.references[ct(s)],!o)return t.pos=h,!1;u=o.href,a=o.title}if(!e){n=t.src.slice(f,d);const e=[];t.md.inline.parse(n,t.md,t.env,e);const r=t.push("image","img",0),s=[["src",u],["alt",""]];r.attrs=s,r.children=e,r.content=n,a&&s.push(["title",a])}return t.pos=i,t.posMax=p,!0}],["autolink",function(t,e){let r=t.pos;if(60!==t.src.charCodeAt(r))return!1;const n=t.pos,s=t.posMax;for(;;){if(++r>=s)return!1;const e=t.src.charCodeAt(r);if(60===e)return!1;if(62===e)break}const i=t.src.slice(n+1,r);if(te.test(i)){const r=t.md.normalizeLink(i);if(!t.md.validateLink(r))return!1;if(!e){const e=t.push("link_open","a",1);e.attrs=[["href",r]],e.markup="autolink",e.info="auto";t.push("text","",0).content=t.md.normalizeLinkText(i);const n=t.push("link_close","a",-1);n.markup="autolink",n.info="auto"}return t.pos+=i.length+2,!0}if(Kt.test(i)){const r=t.md.normalizeLink("mailto:"+i);if(!t.md.validateLink(r))return!1;if(!e){const e=t.push("link_open","a",1);e.attrs=[["href",r]],e.markup="autolink",e.info="auto";t.push("text","",0).content=t.md.normalizeLinkText(i);const n=t.push("link_close","a",-1);n.markup="autolink",n.info="auto"}return t.pos+=i.length+2,!0}return!1}],["html_inline",function(t,e){if(!t.md.options.html)return!1;const r=t.posMax,n=t.pos;if(60!==t.src.charCodeAt(n)||n+2>=r)return!1;const s=t.src.charCodeAt(n+1);if(33!==s&&63!==s&&47!==s&&!function(t){const e=32|t;return e>=97&&e<=122}(s))return!1;const i=t.src.slice(n).match(Ot);if(!i)return!1;if(!e){const e=t.push("html_inline","",0);e.content=i[0],o=e.content,/^\s]/i.test(o)&&t.linkLevel++,function(t){return/^<\/a\s*>/i.test(t)}(e.content)&&t.linkLevel--}var o;return t.pos+=i[0].length,!0}],["entity",function(t,e){const r=t.pos,n=t.posMax;if(38!==t.src.charCodeAt(r))return!1;if(r+1>=n)return!1;if(35===t.src.charCodeAt(r+1)){const n=t.src.slice(r).match(ee);if(n){if(!e){const e="x"===n[1][0].toLowerCase()?parseInt(n[1].slice(1),16):parseInt(n[1],10),r=t.push("text_special","",0);r.content=H(e)?V(e):V(65533),r.markup=n[0],r.info="entity"}return t.pos+=n[0].length,!0}}else{const n=t.src.slice(r).match(re);if(n){const r=O(n[0]);if(r!==n[0]){if(!e){const e=t.push("text_special","",0);e.content=r,e.markup=n[0],e.info="entity"}return t.pos+=n[0].length,!0}}}return!1}]],ie=[["balance_pairs",function(t){const e=t.tokens_meta,r=t.tokens_meta.length;ne(t.delimiters);for(let t=0;t0&&n++,"text"===s[e].type&&e+1=t.pos)throw new Error("inline rule didn't increment state.pos");break}}else t.pos=t.posMax;o||t.pos++,i[e]=t.pos},oe.prototype.tokenize=function(t){const e=this.ruler.getRules(""),r=e.length,n=t.posMax,s=t.md.options.maxNesting;for(;t.pos=t.pos)throw new Error("inline rule didn't increment state.pos");break}if(o){if(t.pos>=n)break}else t.pending+=t.src[t.pos++]}t.pending&&t.pushPending()},oe.prototype.parse=function(t,e,r,n){const s=new this.State(t,e,r,n);this.tokenize(s);const i=this.ruler2.getRules(""),o=i.length;for(let t=0;t=3&&":"===t[e-3]||e>=3&&"/"===t[e-3]?0:n.match(r.re.no_http)[0].length:0}},"mailto:":{validate:function(t,e,r){const n=t.slice(e);return r.re.mailto||(r.re.mailto=new RegExp("^"+r.re.src_email_name+"@"+r.re.src_host_strict,"i")),r.re.mailto.test(n)?n.match(r.re.mailto)[0].length:0}}},fe="a[cdefgilmnoqrstuwxz]|b[abdefghijmnorstvwyz]|c[acdfghiklmnoruvwxyz]|d[ejkmoz]|e[cegrstu]|f[ijkmor]|g[abdefghilmnpqrstuwy]|h[kmnrtu]|i[delmnoqrst]|j[emop]|k[eghimnprwyz]|l[abcikrstuvy]|m[acdeghklmnopqrstuvwxyz]|n[acefgilopruz]|om|p[aefghklmnrstwy]|qa|r[eosuw]|s[abcdeghijklmnortuvxyz]|t[cdfghjklmnortvwz]|u[agksyz]|v[aceginu]|w[fs]|y[et]|z[amw]",de="biz|com|edu|gov|net|org|pro|web|xxx|aero|asia|coop|info|museum|name|shop|\u0440\u0444".split("|");function _e(t){const e=t.re=function(t){const e={};t=t||{},e.src_Any=b.source,e.src_Cc=C.source,e.src_Z=E.source,e.src_P=A.source,e.src_ZPCc=[e.src_Z,e.src_P,e.src_Cc].join("|"),e.src_ZCc=[e.src_Z,e.src_Cc].join("|");const r="[><\uff5c]";return e.src_pseudo_letter="(?:(?![><\uff5c]|"+e.src_ZPCc+")"+e.src_Any+")",e.src_ip4="(?:(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)",e.src_auth="(?:(?:(?!"+e.src_ZCc+"|[@/\\[\\]()]).)+@)?",e.src_port="(?::(?:6(?:[0-4]\\d{3}|5(?:[0-4]\\d{2}|5(?:[0-2]\\d|3[0-5])))|[1-5]?\\d{1,4}))?",e.src_host_terminator="(?=$|[><\uff5c]|"+e.src_ZPCc+")(?!"+(t["---"]?"-(?!--)|":"-|")+"_|:\\d|\\.-|\\.(?!$|"+e.src_ZPCc+"))",e.src_path="(?:[/?#](?:(?!"+e.src_ZCc+"|"+r+"|[()[\\]{}.,\"'?!\\-;]).|\\[(?:(?!"+e.src_ZCc+"|\\]).)*\\]|\\((?:(?!"+e.src_ZCc+"|[)]).)*\\)|\\{(?:(?!"+e.src_ZCc+'|[}]).)*\\}|\\"(?:(?!'+e.src_ZCc+'|["]).)+\\"|\\\'(?:(?!'+e.src_ZCc+"|[']).)+\\'|\\'(?="+e.src_pseudo_letter+"|[-])|\\.{2,}[a-zA-Z0-9%/&]|\\.(?!"+e.src_ZCc+"|[.]|$)|"+(t["---"]?"\\-(?!--(?:[^-]|$))(?:-*)|":"\\-+|")+",(?!"+e.src_ZCc+"|$)|;(?!"+e.src_ZCc+"|$)|\\!+(?!"+e.src_ZCc+"|[!]|$)|\\?(?!"+e.src_ZCc+"|[?]|$))+|\\/)?",e.src_email_name='[\\-;:&=\\+\\$,\\.a-zA-Z0-9_][\\-;:&=\\+\\$,\\"\\.a-zA-Z0-9_]*',e.src_xn="xn--[a-z0-9\\-]{1,59}",e.src_domain_root="(?:"+e.src_xn+"|"+e.src_pseudo_letter+"{1,63})",e.src_domain="(?:"+e.src_xn+"|(?:"+e.src_pseudo_letter+")|(?:"+e.src_pseudo_letter+"(?:-|"+e.src_pseudo_letter+"){0,61}"+e.src_pseudo_letter+"))",e.src_host="(?:(?:(?:(?:"+e.src_domain+")\\.)*"+e.src_domain+"))",e.tpl_host_fuzzy="(?:"+e.src_ip4+"|(?:(?:(?:"+e.src_domain+")\\.)+(?:%TLDS%)))",e.tpl_host_no_ip_fuzzy="(?:(?:(?:"+e.src_domain+")\\.)+(?:%TLDS%))",e.src_host_strict=e.src_host+e.src_host_terminator,e.tpl_host_fuzzy_strict=e.tpl_host_fuzzy+e.src_host_terminator,e.src_host_port_strict=e.src_host+e.src_port+e.src_host_terminator,e.tpl_host_port_fuzzy_strict=e.tpl_host_fuzzy+e.src_port+e.src_host_terminator,e.tpl_host_port_no_ip_fuzzy_strict=e.tpl_host_no_ip_fuzzy+e.src_port+e.src_host_terminator,e.tpl_host_fuzzy_test="localhost|www\\.|\\.\\d{1,3}\\.|(?:\\.(?:%TLDS%)(?:"+e.src_ZPCc+"|>|$))",e.tpl_email_fuzzy='(^|[><\uff5c]|"|\\(|'+e.src_ZCc+")("+e.src_email_name+"@"+e.tpl_host_fuzzy_strict+")",e.tpl_link_fuzzy="(^|(?![.:/\\-_@])(?:[$+<=>^`|\uff5c]|"+e.src_ZPCc+"))((?![$+<=>^`|\uff5c])"+e.tpl_host_port_fuzzy_strict+e.src_path+")",e.tpl_link_no_ip_fuzzy="(^|(?![.:/\\-_@])(?:[$+<=>^`|\uff5c]|"+e.src_ZPCc+"))((?![$+<=>^`|\uff5c])"+e.tpl_host_port_no_ip_fuzzy_strict+e.src_path+")",e}(t.__opts__),r=t.__tlds__.slice();function n(t){return t.replace("%TLDS%",e.src_tlds)}t.onCompile(),t.__tlds_replaced__||r.push(fe),r.push(e.src_xn),e.src_tlds=r.join("|"),e.email_fuzzy=RegExp(n(e.tpl_email_fuzzy),"i"),e.link_fuzzy=RegExp(n(e.tpl_link_fuzzy),"i"),e.link_no_ip_fuzzy=RegExp(n(e.tpl_link_no_ip_fuzzy),"i"),e.host_fuzzy_test=RegExp(n(e.tpl_host_fuzzy_test),"i");const s=[];function i(t,e){throw new Error('(LinkifyIt) Invalid schema "'+t+'": '+e)}t.__compiled__={},Object.keys(t.__schemas__).forEach((function(e){const r=t.__schemas__[e];if(null===r)return;const n={validate:null,link:null};if(t.__compiled__[e]=n,"[object Object]"===ae(r))return!function(t){return"[object RegExp]"===ae(t)}(r.validate)?le(r.validate)?n.validate=r.validate:i(e,r):n.validate=function(t){return function(e,r){const n=e.slice(r);return t.test(n)?n.match(t)[0].length:0}}(r.validate),void(le(r.normalize)?n.normalize=r.normalize:r.normalize?i(e,r):n.normalize=function(t,e){e.normalize(t)});!function(t){return"[object String]"===ae(t)}(r)?i(e,r):s.push(e)})),s.forEach((function(e){t.__compiled__[t.__schemas__[e]]&&(t.__compiled__[e].validate=t.__compiled__[t.__schemas__[e]].validate,t.__compiled__[e].normalize=t.__compiled__[t.__schemas__[e]].normalize)})),t.__compiled__[""]={validate:null,normalize:function(t,e){e.normalize(t)}};const o=Object.keys(t.__compiled__).filter((function(e){return e.length>0&&t.__compiled__[e]})).map(ue).join("|");t.re.schema_test=RegExp("(^|(?!_)(?:[><\uff5c]|"+e.src_ZPCc+"))("+o+")","i"),t.re.schema_search=RegExp("(^|(?!_)(?:[><\uff5c]|"+e.src_ZPCc+"))("+o+")","ig"),t.re.schema_at_start=RegExp("^"+t.re.schema_search.source,"i"),t.re.pretest=RegExp("("+t.re.schema_test.source+")|("+t.re.host_fuzzy_test.source+")|@","i"),function(t){t.__index__=-1,t.__text_cache__=""}(t)}function me(t,e){const r=t.__index__,n=t.__last_index__,s=t.__text_cache__.slice(r,n);this.schema=t.__schema__.toLowerCase(),this.index=r+e,this.lastIndex=n+e,this.raw=s,this.text=s,this.url=s}function ge(t,e){const r=new me(t,e);return t.__compiled__[r.schema].normalize(r,t),r}function ke(t,e){if(!(this instanceof ke))return new ke(t,e);var r;e||(r=t,Object.keys(r||{}).reduce((function(t,e){return t||he.hasOwnProperty(e)}),!1)&&(e=t,t={})),this.__opts__=ce({},he,e),this.__index__=-1,this.__last_index__=-1,this.__schema__="",this.__text_cache__="",this.__schemas__=ce({},pe,t),this.__compiled__={},this.__tlds__=de,this.__tlds_replaced__=!1,this.re={},_e(this)}ke.prototype.add=function(t,e){return this.__schemas__[t]=e,_e(this),this},ke.prototype.set=function(t){return this.__opts__=ce(this.__opts__,t),this},ke.prototype.test=function(t){if(this.__text_cache__=t,this.__index__=-1,!t.length)return!1;let e,r,n,s,i,o,c,a,l;if(this.re.schema_test.test(t))for(c=this.re.schema_search,c.lastIndex=0;null!==(e=c.exec(t));)if(s=this.testSchemaAt(t,e[2],c.lastIndex),s){this.__schema__=e[2],this.__index__=e.index+e[1].length,this.__last_index__=e.index+e[0].length+s;break}return this.__opts__.fuzzyLink&&this.__compiled__["http:"]&&(a=t.search(this.re.host_fuzzy_test),a>=0&&(this.__index__<0||a=0&&null!==(n=t.match(this.re.email_fuzzy))&&(i=n.index+n[1].length,o=n.index+n[0].length,(this.__index__<0||ithis.__last_index__)&&(this.__schema__="mailto:",this.__index__=i,this.__last_index__=o))),this.__index__>=0},ke.prototype.pretest=function(t){return this.re.pretest.test(t)},ke.prototype.testSchemaAt=function(t,e,r){return this.__compiled__[e.toLowerCase()]?this.__compiled__[e.toLowerCase()].validate(t,r,this):0},ke.prototype.match=function(t){const e=[];let r=0;this.__index__>=0&&this.__text_cache__===t&&(e.push(ge(this,r)),r=this.__last_index__);let n=r?t.slice(r):t;for(;this.test(n);)e.push(ge(this,r)),n=n.slice(this.__last_index__),r+=this.__last_index__;return e.length?e:null},ke.prototype.matchAtStart=function(t){if(this.__text_cache__=t,this.__index__=-1,!t.length)return null;const e=this.re.schema_at_start.exec(t);if(!e)return null;const r=this.testSchemaAt(t,e[2],e[0].length);return r?(this.__schema__=e[2],this.__index__=e.index+e[1].length,this.__last_index__=e.index+e[0].length+r,ge(this,0)):null},ke.prototype.tlds=function(t,e){return t=Array.isArray(t)?t:[t],e?(this.__tlds__=this.__tlds__.concat(t).sort().filter((function(t,e,r){return t!==r[e-1]})).reverse(),_e(this),this):(this.__tlds__=t.slice(),this.__tlds_replaced__=!0,_e(this),this)},ke.prototype.normalize=function(t){t.schema||(t.url="http://"+t.url),"mailto:"!==t.schema||/^mailto:/i.test(t.url)||(t.url="mailto:"+t.url)},ke.prototype.onCompile=function(){};const ye=2147483647,be=36,Ce=/^xn--/,Ae=/[^\0-\x7F]/,Ee=/[\x2E\u3002\uFF0E\uFF61]/g,xe={overflow:"Overflow: input needs wider integers to process","not-basic":"Illegal input >= 0x80 (not a basic code point)","invalid-input":"Invalid input"},De=Math.floor,we=String.fromCharCode;function Fe(t){throw new RangeError(xe[t])}function ve(t,e){const r=t.split("@");let n="";r.length>1&&(n=r[0]+"@",t=r[1]);const s=function(t,e){const r=[];let n=t.length;for(;n--;)r[n]=e(t[n]);return r}((t=t.replace(Ee,".")).split("."),e).join(".");return n+s}function ze(t){const e=[];let r=0;const n=t.length;for(;r=55296&&s<=56319&&r>1,t+=De(t/e);t>455;n+=be)t=De(t/35);return De(n+36*t/(t+38))},Le=function(t){const e=[],r=t.length;let n=0,s=128,i=72,o=t.lastIndexOf("-");o<0&&(o=0);for(let r=0;r=128&&Fe("not-basic"),e.push(t.charCodeAt(r));for(let a=o>0?o+1:0;a=r&&Fe("invalid-input");const o=(c=t.charCodeAt(a++))>=48&&c<58?c-48+26:c>=65&&c<91?c-65:c>=97&&c<123?c-97:be;o>=be&&Fe("invalid-input"),o>De((ye-n)/e)&&Fe("overflow"),n+=o*e;const l=s<=i?1:s>=i+26?26:s-i;if(oDe(ye/u)&&Fe("overflow"),e*=u}const l=e.length+1;i=qe(n-o,l,0==o),De(n/l)>ye-s&&Fe("overflow"),s+=De(n/l),n%=l,e.splice(n++,0,s)}var c;return String.fromCodePoint(...e)},Ie=function(t){const e=[],r=(t=ze(t)).length;let n=128,s=0,i=72;for(const r of t)r<128&&e.push(we(r));const o=e.length;let c=o;for(o&&e.push("-");c=n&&eDe((ye-s)/a)&&Fe("overflow"),s+=(r-n)*a,n=r;for(const r of t)if(rye&&Fe("overflow"),r===n){let t=s;for(let r=be;;r+=be){const n=r<=i?1:r>=i+26?26:r-i;if(tString.fromCodePoint(...t)},decode:Le,encode:Ie,toASCII:function(t){return ve(t,(function(t){return Ae.test(t)?"xn--"+Ie(t):t}))},toUnicode:function(t){return ve(t,(function(t){return Ce.test(t)?Le(t.slice(4).toLowerCase()):t}))}};const Te={default:{options:{html:!1,xhtmlOut:!1,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"\u201c\u201d\u2018\u2019",highlight:null,maxNesting:100},components:{core:{},block:{},inline:{}}},zero:{options:{html:!1,xhtmlOut:!1,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"\u201c\u201d\u2018\u2019",highlight:null,maxNesting:20},components:{core:{rules:["normalize","block","inline","text_join"]},block:{rules:["paragraph"]},inline:{rules:["text"],rules2:["balance_pairs","fragments_join"]}}},commonmark:{options:{html:!0,xhtmlOut:!0,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"\u201c\u201d\u2018\u2019",highlight:null,maxNesting:20},components:{core:{rules:["normalize","block","inline","text_join"]},block:{rules:["blockquote","code","fence","heading","hr","html_block","lheading","list","reference","paragraph"]},inline:{rules:["autolink","backticks","emphasis","entity","escape","html_inline","image","link","newline","text"],rules2:["balance_pairs","emphasis","fragments_join"]}}}},Re=/^(vbscript|javascript|file|data):/,Be=/^data:image\/(gif|png|jpeg|webp);/;function Ne(t){const e=t.trim().toLowerCase();return!Re.test(e)||Be.test(e)}const Pe=["http:","https:","mailto:"];function Oe(t){const e=g(t,!0);if(e.hostname&&(!e.protocol||Pe.indexOf(e.protocol)>=0))try{e.hostname=Me.toASCII(e.hostname)}catch(t){}return n(s(e))}function je(t){const r=g(t,!0);if(r.hostname&&(!r.protocol||Pe.indexOf(r.protocol)>=0))try{r.hostname=Me.toUnicode(r.hostname)}catch(t){}return e(s(r),e.defaultChars+"%")}function Ze(t,e){if(!(this instanceof Ze))return new Ze(t,e);e||j(t)||(e=t||{},t="default"),this.inline=new oe,this.block=new Ut,this.core=new Lt,this.renderer=new pt,this.linkify=new ke,this.validateLink=Ne,this.normalizeLink=Oe,this.normalizeLinkText=je,this.utils=lt,this.helpers=$({},ut),this.options={},this.configure(t),e&&this.set(e)}return Ze.prototype.set=function(t){return $(this.options,t),this},Ze.prototype.configure=function(t){const e=this;if(j(t)){const e=t;if(!(t=Te[e]))throw new Error('Wrong `markdown-it` preset "'+e+'", check name')}if(!t)throw new Error("Wrong `markdown-it` preset, can't be empty");return t.options&&e.set(t.options),t.components&&Object.keys(t.components).forEach((function(r){t.components[r].rules&&e[r].ruler.enableOnly(t.components[r].rules),t.components[r].rules2&&e[r].ruler2.enableOnly(t.components[r].rules2)})),this},Ze.prototype.enable=function(t,e){let r=[];Array.isArray(t)||(t=[t]),["core","block","inline"].forEach((function(e){r=r.concat(this[e].ruler.enable(t,!0))}),this),r=r.concat(this.inline.ruler2.enable(t,!0));const n=t.filter((function(t){return r.indexOf(t)<0}));if(n.length&&!e)throw new Error("MarkdownIt. Failed to enable unknown rule(s): "+n);return this},Ze.prototype.disable=function(t,e){let r=[];Array.isArray(t)||(t=[t]),["core","block","inline"].forEach((function(e){r=r.concat(this[e].ruler.disable(t,!0))}),this),r=r.concat(this.inline.ruler2.disable(t,!0));const n=t.filter((function(t){return r.indexOf(t)<0}));if(n.length&&!e)throw new Error("MarkdownIt. Failed to disable unknown rule(s): "+n);return this},Ze.prototype.use=function(t){const e=[this].concat(Array.prototype.slice.call(arguments,1));return t.apply(t,e),this},Ze.prototype.parse=function(t,e){if("string"!=typeof t)throw new Error("Input data should be a String");const r=new this.core.State(t,this,e);return this.core.process(r),r.tokens},Ze.prototype.render=function(t,e){return e=e||{},this.renderer.render(this.parse(t,e),this.options,e)},Ze.prototype.parseInline=function(t,e){const r=new this.core.State(t,this,e);return r.inlineMode=!0,this.core.process(r),r.tokens},Ze.prototype.renderInline=function(t,e){return e=e||{},this.renderer.render(this.parseInline(t,e),this.options,e)},Ze}));
diff --git a/apps/templates/_foot_js.html b/apps/templates/_foot_js.html
index 0362e0253..0f6bf03c0 100644
--- a/apps/templates/_foot_js.html
+++ b/apps/templates/_foot_js.html
@@ -9,6 +9,28 @@
+
+
+{% if INTERFACE.footer_content %}
+
+
+{% endif %}
+
+
+
-{% if INTERFACE.beian_text %}
-
-
-{% endif %}
+
+
diff --git a/apps/templates/_header_bar.html b/apps/templates/_header_bar.html
index 72fb77201..230d1acde 100644
--- a/apps/templates/_header_bar.html
+++ b/apps/templates/_header_bar.html
@@ -48,6 +48,12 @@
中文
+
+
+
+ 中文(繁體)
+
+
diff --git a/apps/templates/_mfa_login_field.html b/apps/templates/_mfa_login_field.html
index a5720aa71..412b369ca 100644
--- a/apps/templates/_mfa_login_field.html
+++ b/apps/templates/_mfa_login_field.html
@@ -47,6 +47,7 @@
width: 156px !important;
height: 100%;
vertical-align: top;
+ padding: 8px 12px;
}