mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-05 05:16:03 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e366fc536 | ||
|
|
8bd6442965 | ||
|
|
1a1eadb282 | ||
|
|
eed72b1c12 | ||
|
|
351350ea03 | ||
|
|
bc3d6ba92f | ||
|
|
345e4baf2a | ||
|
|
6c64dc057f | ||
|
|
eec0a9c9d9 |
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "langbot"
|
name = "langbot"
|
||||||
version = "4.9.2"
|
version = "4.9.3"
|
||||||
description = "Production-grade platform for building agentic IM bots"
|
description = "Production-grade platform for building agentic IM bots"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
@@ -64,7 +64,7 @@ dependencies = [
|
|||||||
"chromadb>=1.0.0,<2.0.0",
|
"chromadb>=1.0.0,<2.0.0",
|
||||||
"qdrant-client (>=1.15.1,<2.0.0)",
|
"qdrant-client (>=1.15.1,<2.0.0)",
|
||||||
"pyseekdb==1.1.0.post3",
|
"pyseekdb==1.1.0.post3",
|
||||||
"langbot-plugin==0.3.1",
|
"langbot-plugin==0.3.2",
|
||||||
"asyncpg>=0.30.0",
|
"asyncpg>=0.30.0",
|
||||||
"line-bot-sdk>=3.19.0",
|
"line-bot-sdk>=3.19.0",
|
||||||
"tboxsdk>=0.0.10",
|
"tboxsdk>=0.0.10",
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""LangBot - Production-grade platform for building agentic IM bots"""
|
"""LangBot - Production-grade platform for building agentic IM bots"""
|
||||||
|
|
||||||
__version__ = '4.9.2'
|
__version__ = '4.9.3'
|
||||||
|
|||||||
@@ -74,20 +74,26 @@ def _apply_env_overrides_to_config(cfg: dict) -> dict:
|
|||||||
current = cfg
|
current = cfg
|
||||||
|
|
||||||
for i, key in enumerate(keys):
|
for i, key in enumerate(keys):
|
||||||
if not isinstance(current, dict) or key not in current:
|
if not isinstance(current, dict):
|
||||||
break
|
break
|
||||||
|
|
||||||
if i == len(keys) - 1:
|
if i == len(keys) - 1:
|
||||||
# At the final key - check if it's a scalar value
|
# At the final key
|
||||||
if isinstance(current[key], (dict, list)):
|
if key in current:
|
||||||
# Skip dict and list types
|
if isinstance(current[key], (dict, list)):
|
||||||
pass
|
# Skip dict and list types
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
# Valid scalar value - convert and set it
|
||||||
|
converted_value = convert_value(env_value, current[key])
|
||||||
|
current[key] = converted_value
|
||||||
else:
|
else:
|
||||||
# Valid scalar value - convert and set it
|
# Key doesn't exist yet - create it as string
|
||||||
converted_value = convert_value(env_value, current[key])
|
current[key] = env_value
|
||||||
current[key] = converted_value
|
|
||||||
else:
|
else:
|
||||||
# Navigate deeper
|
# Navigate deeper - create intermediate dict if needed
|
||||||
|
if key not in current:
|
||||||
|
current[key] = {}
|
||||||
current = current[key]
|
current = current[key]
|
||||||
|
|
||||||
return cfg
|
return cfg
|
||||||
@@ -146,16 +152,50 @@ class LoadConfigStage(stage.BootingStage):
|
|||||||
await ap.instance_config.dump_config()
|
await ap.instance_config.dump_config()
|
||||||
|
|
||||||
# load or generate instance id
|
# load or generate instance id
|
||||||
ap.instance_id = await config.load_json_config(
|
# Priority:
|
||||||
'data/labels/instance_id.json',
|
# 1. system.instance_id from config.yaml (can be set via SYSTEM__INSTANCE_ID env var)
|
||||||
template_data={
|
# 2. data/labels/instance_id.json (if file exists)
|
||||||
'instance_id': f'instance_{str(uuid.uuid4())}',
|
# 3. Generate new and save to file
|
||||||
'instance_create_ts': int(time.time()),
|
config_instance_id = ap.instance_config.data.get('system', {}).get('instance_id', '')
|
||||||
},
|
|
||||||
completion=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
constants.instance_id = ap.instance_id.data['instance_id']
|
if config_instance_id:
|
||||||
|
# Use the instance_id from config.yaml
|
||||||
|
constants.instance_id = config_instance_id
|
||||||
|
# Still load/create the file for backward compat, but don't use its value
|
||||||
|
ap.instance_id = await config.load_json_config(
|
||||||
|
'data/labels/instance_id.json',
|
||||||
|
template_data={
|
||||||
|
'instance_id': f'instance_{str(uuid.uuid4())}',
|
||||||
|
'instance_create_ts': int(time.time()),
|
||||||
|
},
|
||||||
|
completion=False,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Try loading file-based instance id
|
||||||
|
instance_id_path = os.path.join('data', 'labels', 'instance_id.json')
|
||||||
|
if os.path.exists(instance_id_path):
|
||||||
|
# File exists, read it
|
||||||
|
ap.instance_id = await config.load_json_config(
|
||||||
|
'data/labels/instance_id.json',
|
||||||
|
template_data={
|
||||||
|
'instance_id': '',
|
||||||
|
'instance_create_ts': 0,
|
||||||
|
},
|
||||||
|
completion=False,
|
||||||
|
)
|
||||||
|
constants.instance_id = ap.instance_id.data['instance_id']
|
||||||
|
else:
|
||||||
|
# Neither config nor file, generate new and save to file
|
||||||
|
new_id = f'instance_{str(uuid.uuid4())}'
|
||||||
|
ap.instance_id = await config.load_json_config(
|
||||||
|
'data/labels/instance_id.json',
|
||||||
|
template_data={
|
||||||
|
'instance_id': new_id,
|
||||||
|
'instance_create_ts': int(time.time()),
|
||||||
|
},
|
||||||
|
completion=False,
|
||||||
|
)
|
||||||
|
constants.instance_id = new_id
|
||||||
constants.edition = ap.instance_config.data.get('system', {}).get('edition', 'community')
|
constants.edition = ap.instance_config.data.get('system', {}).get('edition', 'community')
|
||||||
|
|
||||||
print(f'LangBot instance id: {constants.instance_id}')
|
print(f'LangBot instance id: {constants.instance_id}')
|
||||||
|
|||||||
@@ -176,6 +176,16 @@ class PreProcessor(stage.PipelineStage):
|
|||||||
query.variables['user_message_text'] = plain_text
|
query.variables['user_message_text'] = plain_text
|
||||||
|
|
||||||
query.user_message = provider_message.Message(role='user', content=content_list)
|
query.user_message = provider_message.Message(role='user', content=content_list)
|
||||||
|
|
||||||
|
# Extract knowledge base UUIDs into query variables so plugins can modify them
|
||||||
|
# during PromptPreProcessing before the runner performs retrieval.
|
||||||
|
kb_uuids = query.pipeline_config['ai']['local-agent'].get('knowledge-bases', [])
|
||||||
|
if not kb_uuids:
|
||||||
|
old_kb_uuid = query.pipeline_config['ai']['local-agent'].get('knowledge-base', '')
|
||||||
|
if old_kb_uuid and old_kb_uuid != '__none__':
|
||||||
|
kb_uuids = [old_kb_uuid]
|
||||||
|
query.variables['_knowledge_base_uuids'] = list(kb_uuids)
|
||||||
|
|
||||||
# =========== 触发事件 PromptPreProcessing
|
# =========== 触发事件 PromptPreProcessing
|
||||||
|
|
||||||
event = events.PromptPreProcessing(
|
event = events.PromptPreProcessing(
|
||||||
|
|||||||
@@ -675,11 +675,15 @@ class RuntimeConnectionHandler(handler.Handler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
session_name = f'{query.session.launcher_type.value}_{query.session.launcher_id}'
|
||||||
entries = await kb.retrieve(
|
entries = await kb.retrieve(
|
||||||
query_text,
|
query_text,
|
||||||
settings={
|
settings={
|
||||||
'top_k': top_k,
|
'top_k': top_k,
|
||||||
'filters': filters,
|
'filters': filters,
|
||||||
|
'session_name': session_name,
|
||||||
|
'bot_uuid': query.bot_uuid or '',
|
||||||
|
'sender_id': str(query.sender_id),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
results = [entry.model_dump(mode='json') for entry in entries]
|
results = [entry.model_dump(mode='json') for entry in entries]
|
||||||
|
|||||||
@@ -132,14 +132,9 @@ class LocalAgentRunner(runner.RequestRunner):
|
|||||||
"""Run request"""
|
"""Run request"""
|
||||||
pending_tool_calls = []
|
pending_tool_calls = []
|
||||||
|
|
||||||
# Get knowledge bases list (new field)
|
# Get knowledge bases list from query variables (set by PreProcessor,
|
||||||
kb_uuids = query.pipeline_config['ai']['local-agent'].get('knowledge-bases', [])
|
# may have been modified by plugins during PromptPreProcessing)
|
||||||
|
kb_uuids = query.variables.get('_knowledge_base_uuids', [])
|
||||||
# Fallback to old field for backward compatibility
|
|
||||||
if not kb_uuids:
|
|
||||||
old_kb_uuid = query.pipeline_config['ai']['local-agent'].get('knowledge-base', '')
|
|
||||||
if old_kb_uuid and old_kb_uuid != '__none__':
|
|
||||||
kb_uuids = [old_kb_uuid]
|
|
||||||
|
|
||||||
user_message = copy.deepcopy(query.user_message)
|
user_message = copy.deepcopy(query.user_message)
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ proxy:
|
|||||||
http: ''
|
http: ''
|
||||||
https: ''
|
https: ''
|
||||||
system:
|
system:
|
||||||
|
instance_id: ''
|
||||||
edition: community
|
edition: community
|
||||||
recovery_key: ''
|
recovery_key: ''
|
||||||
allow_modify_login_info: true
|
allow_modify_login_info: true
|
||||||
|
|||||||
10
uv.lock
generated
10
uv.lock
generated
@@ -1832,7 +1832,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "langbot"
|
name = "langbot"
|
||||||
version = "4.9.2"
|
version = "4.9.3"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiocqhttp" },
|
{ name = "aiocqhttp" },
|
||||||
@@ -1937,7 +1937,7 @@ requires-dist = [
|
|||||||
{ name = "ebooklib", specifier = ">=0.18" },
|
{ name = "ebooklib", specifier = ">=0.18" },
|
||||||
{ name = "gewechat-client", specifier = ">=0.1.5" },
|
{ name = "gewechat-client", specifier = ">=0.1.5" },
|
||||||
{ name = "html2text", specifier = ">=2024.2.26" },
|
{ name = "html2text", specifier = ">=2024.2.26" },
|
||||||
{ name = "langbot-plugin", specifier = "==0.3.1" },
|
{ name = "langbot-plugin", specifier = "==0.3.2" },
|
||||||
{ name = "langchain", specifier = ">=0.2.0" },
|
{ name = "langchain", specifier = ">=0.2.0" },
|
||||||
{ name = "langchain-text-splitters", specifier = ">=0.0.1" },
|
{ name = "langchain-text-splitters", specifier = ">=0.0.1" },
|
||||||
{ name = "lark-oapi", specifier = ">=1.4.15" },
|
{ name = "lark-oapi", specifier = ">=1.4.15" },
|
||||||
@@ -1993,7 +1993,7 @@ dev = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "langbot-plugin"
|
name = "langbot-plugin"
|
||||||
version = "0.3.1"
|
version = "0.3.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiofiles" },
|
{ name = "aiofiles" },
|
||||||
@@ -2011,9 +2011,9 @@ dependencies = [
|
|||||||
{ name = "watchdog" },
|
{ name = "watchdog" },
|
||||||
{ name = "websockets" },
|
{ name = "websockets" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/4e/ed/b440e26ebc40983abf00dd343338101ada3381065fb3347401ba75f873fe/langbot_plugin-0.3.1.tar.gz", hash = "sha256:0839dcb4cfe689fc670d0ded29b57e6a3f683d8f7326eaa771a5b753675459ac", size = 170285, upload-time = "2026-03-12T15:07:01.918Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/05/32/1b029961d718b5418d9d139687287193d4657914c467833176d9c060fb4c/langbot_plugin-0.3.2.tar.gz", hash = "sha256:2f7f16285600ec019a4e8cc8b40bc8f8d404e3bbc69c9d129620dc70b3bde2f8", size = 170431, upload-time = "2026-03-14T12:42:55.175Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/81/d3c4142911792838b90384a28f7dd1540d0862303293c53ba77e69fc0e15/langbot_plugin-0.3.1-py3-none-any.whl", hash = "sha256:8139796926fe8385b7b546ef865e29b1b8d8e28249e20f3b5417d42d3181ec62", size = 144813, upload-time = "2026-03-12T15:07:03.69Z" },
|
{ url = "https://files.pythonhosted.org/packages/91/d7/a5dd6cff000a4f7fe88692689be276875b04485f48fc26e162f1ad2a341c/langbot_plugin-0.3.2-py3-none-any.whl", hash = "sha256:a1de527c4d651e6b6ba2458b6fac09a300e6b1ffdc5a25bbfad88d970cfd6cfd", size = 144906, upload-time = "2026-03-14T12:42:56.486Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { httpClient, initializeUserInfo } from '@/app/infra/http';
|
import { httpClient, initializeUserInfo } from '@/app/infra/http';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { Mail, Lock, Loader2 } from 'lucide-react';
|
import { Mail, Lock, Loader2, AlertCircle, RefreshCw } from 'lucide-react';
|
||||||
import langbotIcon from '@/app/assets/langbot-logo.webp';
|
import langbotIcon from '@/app/assets/langbot-logo.webp';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -46,6 +46,8 @@ export default function Login() {
|
|||||||
const [accountType, setAccountType] = useState<AccountType | null>(null);
|
const [accountType, setAccountType] = useState<AccountType | null>(null);
|
||||||
const [hasPassword, setHasPassword] = useState(false);
|
const [hasPassword, setHasPassword] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
|
const [retrying, setRetrying] = useState(false);
|
||||||
|
|
||||||
const form = useForm<z.infer<ReturnType<typeof formSchema>>>({
|
const form = useForm<z.infer<ReturnType<typeof formSchema>>>({
|
||||||
resolver: zodResolver(formSchema(t)),
|
resolver: zodResolver(formSchema(t)),
|
||||||
@@ -61,6 +63,7 @@ export default function Login() {
|
|||||||
|
|
||||||
async function checkAccountInfo() {
|
async function checkAccountInfo() {
|
||||||
try {
|
try {
|
||||||
|
setLoadError(null);
|
||||||
const res = await httpClient.getAccountInfo();
|
const res = await httpClient.getAccountInfo();
|
||||||
if (!res.initialized) {
|
if (!res.initialized) {
|
||||||
router.push('/register');
|
router.push('/register');
|
||||||
@@ -72,11 +75,22 @@ export default function Login() {
|
|||||||
|
|
||||||
// Also check if already logged in
|
// Also check if already logged in
|
||||||
checkIfAlreadyLoggedIn();
|
checkIfAlreadyLoggedIn();
|
||||||
} catch {
|
} catch (err) {
|
||||||
|
const errorMessage =
|
||||||
|
err instanceof Error ? err.message : t('common.loginLoadError');
|
||||||
|
setLoadError(errorMessage);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleRetry() {
|
||||||
|
setRetrying(true);
|
||||||
|
setLoading(true);
|
||||||
|
setLoadError(null);
|
||||||
|
await checkAccountInfo();
|
||||||
|
setRetrying(false);
|
||||||
|
}
|
||||||
|
|
||||||
function checkIfAlreadyLoggedIn() {
|
function checkIfAlreadyLoggedIn() {
|
||||||
httpClient
|
httpClient
|
||||||
.checkUserToken()
|
.checkUserToken()
|
||||||
@@ -129,6 +143,54 @@ export default function Login() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show error state when account info failed to load
|
||||||
|
if (loadError) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-neutral-900">
|
||||||
|
<Card className="w-[375px] shadow-lg dark:shadow-white/10">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<ThemeToggle />
|
||||||
|
<LanguageSelector />
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
src={langbotIcon.src}
|
||||||
|
alt="LangBot"
|
||||||
|
className="w-16 h-16 mb-4 mx-auto"
|
||||||
|
/>
|
||||||
|
<CardTitle className="text-2xl text-center">
|
||||||
|
{t('common.welcome')}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex flex-col items-center gap-3 py-4">
|
||||||
|
<AlertCircle className="h-10 w-10 text-destructive" />
|
||||||
|
<p className="text-sm text-center text-muted-foreground">
|
||||||
|
{t('common.loginLoadErrorDesc')}
|
||||||
|
</p>
|
||||||
|
<code className="text-xs bg-muted px-3 py-2 rounded max-w-full overflow-x-auto block text-center text-muted-foreground">
|
||||||
|
{loadError}
|
||||||
|
</code>
|
||||||
|
<Button
|
||||||
|
onClick={handleRetry}
|
||||||
|
disabled={retrying}
|
||||||
|
variant="outline"
|
||||||
|
className="mt-2 cursor-pointer"
|
||||||
|
>
|
||||||
|
{retrying ? (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<RefreshCw className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
{t('common.retry')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Determine what to show based on account type
|
// Determine what to show based on account type
|
||||||
const showLocalLogin =
|
const showLocalLogin =
|
||||||
accountType === 'local' || (accountType === 'space' && hasPassword);
|
accountType === 'local' || (accountType === 'space' && hasPassword);
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ const enUS = {
|
|||||||
continueToLogin: 'Login to continue',
|
continueToLogin: 'Login to continue',
|
||||||
loginSuccess: 'Login successful',
|
loginSuccess: 'Login successful',
|
||||||
loginFailed: 'Login failed, please check your email and password',
|
loginFailed: 'Login failed, please check your email and password',
|
||||||
|
loginLoadError: 'Unable to connect to server',
|
||||||
|
loginLoadErrorDesc:
|
||||||
|
'Unable to connect to the LangBot backend. Please make sure the service is running and try again.',
|
||||||
|
retry: 'Retry',
|
||||||
enterEmail: 'Enter email address',
|
enterEmail: 'Enter email address',
|
||||||
enterPassword: 'Enter password',
|
enterPassword: 'Enter password',
|
||||||
invalidEmail: 'Please enter a valid email address',
|
invalidEmail: 'Please enter a valid email address',
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
loginSuccess: 'ログインに成功しました',
|
loginSuccess: 'ログインに成功しました',
|
||||||
loginFailed:
|
loginFailed:
|
||||||
'ログインに失敗しました。メールアドレスまたはパスワードをご確認ください',
|
'ログインに失敗しました。メールアドレスまたはパスワードをご確認ください',
|
||||||
|
loginLoadError: 'サーバーに接続できません',
|
||||||
|
loginLoadErrorDesc:
|
||||||
|
'LangBot バックエンドに接続できません。サービスが起動していることを確認してから再試行してください。',
|
||||||
|
retry: '再試行',
|
||||||
enterEmail: 'メールアドレスを入力',
|
enterEmail: 'メールアドレスを入力',
|
||||||
enterPassword: 'パスワードを入力',
|
enterPassword: 'パスワードを入力',
|
||||||
invalidEmail: '有効なメールアドレスを入力してください',
|
invalidEmail: '有効なメールアドレスを入力してください',
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ const zhHans = {
|
|||||||
continueToLogin: '登录以继续',
|
continueToLogin: '登录以继续',
|
||||||
loginSuccess: '登录成功',
|
loginSuccess: '登录成功',
|
||||||
loginFailed: '登录失败,请检查邮箱和密码是否正确',
|
loginFailed: '登录失败,请检查邮箱和密码是否正确',
|
||||||
|
loginLoadError: '无法连接到服务器',
|
||||||
|
loginLoadErrorDesc: '无法连接到 LangBot 后端服务,请确认服务已启动后重试。',
|
||||||
|
retry: '重试',
|
||||||
enterEmail: '输入邮箱地址',
|
enterEmail: '输入邮箱地址',
|
||||||
enterPassword: '输入密码',
|
enterPassword: '输入密码',
|
||||||
invalidEmail: '请输入有效的邮箱地址',
|
invalidEmail: '请输入有效的邮箱地址',
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ const zhHant = {
|
|||||||
continueToLogin: '登入以繼續',
|
continueToLogin: '登入以繼續',
|
||||||
loginSuccess: '登入成功',
|
loginSuccess: '登入成功',
|
||||||
loginFailed: '登入失敗,請檢查電子郵件和密碼是否正確',
|
loginFailed: '登入失敗,請檢查電子郵件和密碼是否正確',
|
||||||
|
loginLoadError: '無法連線到伺服器',
|
||||||
|
loginLoadErrorDesc: '無法連線到 LangBot 後端服務,請確認服務已啟動後重試。',
|
||||||
|
retry: '重試',
|
||||||
enterEmail: '輸入電子郵件地址',
|
enterEmail: '輸入電子郵件地址',
|
||||||
enterPassword: '輸入密碼',
|
enterPassword: '輸入密碼',
|
||||||
invalidEmail: '請輸入有效的電子郵件地址',
|
invalidEmail: '請輸入有效的電子郵件地址',
|
||||||
|
|||||||
Reference in New Issue
Block a user