mirror of
https://github.com/lkddi/Xboard.git
synced 2026-04-23 19:37:35 +08:00
feat: add plugin migrations and fix plan management bugs
- Plugin database migration support - Fix empty prices error in plan management - Plugin update functionality - Custom shadowsocks encryption algorithms
This commit is contained in:
@@ -38,7 +38,7 @@ class PluginController extends Controller
|
||||
],
|
||||
[
|
||||
'value' => Plugin::TYPE_PAYMENT,
|
||||
'label' => '支付方式',
|
||||
'label' => '支付方式',
|
||||
'description' => '提供支付接口的插件,如支付宝、微信支付等',
|
||||
'icon' => '💳'
|
||||
]
|
||||
@@ -52,14 +52,14 @@ class PluginController extends Controller
|
||||
public function index(Request $request)
|
||||
{
|
||||
$type = $request->query('type');
|
||||
|
||||
$installedPlugins = Plugin::when($type, function($query) use ($type) {
|
||||
return $query->byType($type);
|
||||
})
|
||||
|
||||
$installedPlugins = Plugin::when($type, function ($query) use ($type) {
|
||||
return $query->byType($type);
|
||||
})
|
||||
->get()
|
||||
->keyBy('code')
|
||||
->toArray();
|
||||
|
||||
|
||||
$pluginPath = base_path('plugins');
|
||||
$plugins = [];
|
||||
|
||||
@@ -72,19 +72,26 @@ class PluginController extends Controller
|
||||
$config = json_decode(File::get($configFile), true);
|
||||
$code = $config['code'];
|
||||
$pluginType = $config['type'] ?? Plugin::TYPE_FEATURE;
|
||||
|
||||
|
||||
// 如果指定了类型,过滤插件
|
||||
if ($type && $pluginType !== $type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
$installed = isset($installedPlugins[$code]);
|
||||
$pluginConfig = $installed ? $this->configService->getConfig($code) : ($config['config'] ?? []);
|
||||
$readmeFile = collect(['README.md', 'readme.md'])
|
||||
->map(fn($f) => $directory . '/' . $f)
|
||||
->first(fn($path) => File::exists($path));
|
||||
$readmeContent = $readmeFile ? File::get($readmeFile) : '';
|
||||
|
||||
$needUpgrade = false;
|
||||
if ($installed) {
|
||||
$installedVersion = $installedPlugins[$code]['version'] ?? null;
|
||||
$localVersion = $config['version'] ?? null;
|
||||
if ($installedVersion && $localVersion && version_compare($localVersion, $installedVersion, '>')) {
|
||||
$needUpgrade = true;
|
||||
}
|
||||
}
|
||||
$plugins[] = [
|
||||
'code' => $config['code'],
|
||||
'name' => $config['name'],
|
||||
@@ -98,6 +105,7 @@ class PluginController extends Controller
|
||||
'can_be_deleted' => !in_array($code, Plugin::PROTECTED_PLUGINS),
|
||||
'config' => $pluginConfig,
|
||||
'readme' => $readmeContent,
|
||||
'need_upgrade' => $needUpgrade,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -138,8 +146,16 @@ class PluginController extends Controller
|
||||
'code' => 'required|string'
|
||||
]);
|
||||
|
||||
$code = $request->input('code');
|
||||
$plugin = Plugin::where('code', $code)->first();
|
||||
if ($plugin && $plugin->is_enabled) {
|
||||
return response()->json([
|
||||
'message' => '请先禁用插件后再卸载'
|
||||
], 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->pluginManager->uninstall($request->input('code'));
|
||||
$this->pluginManager->uninstall($code);
|
||||
return response()->json([
|
||||
'message' => '插件卸载成功'
|
||||
]);
|
||||
@@ -150,6 +166,26 @@ class PluginController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 升级插件
|
||||
*/
|
||||
public function upgrade(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'code' => 'required|string',
|
||||
]);
|
||||
try {
|
||||
$this->pluginManager->update($request->input('code'));
|
||||
return response()->json([
|
||||
'message' => '插件升级成功'
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'message' => '插件升级失败:' . $e->getMessage()
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用插件
|
||||
*/
|
||||
|
||||
@@ -257,6 +257,7 @@ class AdminRoute
|
||||
$router->post('disable', [\App\Http\Controllers\V2\Admin\PluginController::class, 'disable']);
|
||||
$router->get('config', [\App\Http\Controllers\V2\Admin\PluginController::class, 'getConfig']);
|
||||
$router->post('config', [\App\Http\Controllers\V2\Admin\PluginController::class, 'updateConfig']);
|
||||
$router->post('upgrade', [\App\Http\Controllers\V2\Admin\PluginController::class, 'upgrade']);
|
||||
});
|
||||
|
||||
// 流量重置管理
|
||||
|
||||
@@ -19,6 +19,7 @@ use Illuminate\Support\Facades\Log;
|
||||
* @property string $requires
|
||||
* @property string $config
|
||||
* @property string $type
|
||||
* @property boolean $is_enabled
|
||||
*/
|
||||
class Plugin extends Model
|
||||
{
|
||||
|
||||
@@ -137,32 +137,32 @@ class PluginManager
|
||||
*/
|
||||
public function install(string $pluginCode): bool
|
||||
{
|
||||
$configFile = $this->getPluginPath($pluginCode) . '/config.json';
|
||||
|
||||
if (!File::exists($configFile)) {
|
||||
throw new \Exception('Plugin config file not found');
|
||||
}
|
||||
|
||||
$config = json_decode(File::get($configFile), true);
|
||||
if (!$this->validateConfig($config)) {
|
||||
throw new \Exception('Invalid plugin config');
|
||||
}
|
||||
|
||||
// 检查插件是否已安装
|
||||
if (Plugin::where('code', $pluginCode)->exists()) {
|
||||
throw new \Exception('Plugin already installed');
|
||||
}
|
||||
|
||||
// 检查依赖
|
||||
if (!$this->checkDependencies($config['require'] ?? [])) {
|
||||
throw new \Exception('Dependencies not satisfied');
|
||||
}
|
||||
|
||||
// 运行数据库迁移
|
||||
$this->runMigrations(pluginCode: $pluginCode);
|
||||
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$configFile = $this->getPluginPath($pluginCode) . '/config.json';
|
||||
|
||||
if (!File::exists($configFile)) {
|
||||
throw new \Exception('Plugin config file not found');
|
||||
}
|
||||
|
||||
$config = json_decode(File::get($configFile), true);
|
||||
if (!$this->validateConfig($config)) {
|
||||
throw new \Exception('Invalid plugin config');
|
||||
}
|
||||
|
||||
// 检查插件是否已安装
|
||||
if (Plugin::where('code', $pluginCode)->exists()) {
|
||||
throw new \Exception('Plugin already installed');
|
||||
}
|
||||
|
||||
// 检查依赖
|
||||
if (!$this->checkDependencies($config['require'] ?? [])) {
|
||||
throw new \Exception('Dependencies not satisfied');
|
||||
}
|
||||
|
||||
// 运行数据库迁移
|
||||
$this->runMigrations($pluginCode);
|
||||
|
||||
// 提取配置默认值
|
||||
$defaultValues = $this->extractDefaultConfig($config);
|
||||
|
||||
@@ -170,7 +170,7 @@ class PluginManager
|
||||
$plugin = $this->loadPlugin($pluginCode);
|
||||
|
||||
// 注册到数据库
|
||||
$dbPlugin = Plugin::create([
|
||||
Plugin::create([
|
||||
'code' => $pluginCode,
|
||||
'name' => $config['name'],
|
||||
'version' => $config['version'],
|
||||
@@ -191,7 +191,9 @@ class PluginManager
|
||||
DB::commit();
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
if (DB::transactionLevel() > 0) {
|
||||
DB::rollBack();
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
@@ -223,7 +225,22 @@ class PluginManager
|
||||
|
||||
if (File::exists($migrationsPath)) {
|
||||
Artisan::call('migrate', [
|
||||
'--path' => "plugins/{$pluginCode}/database/migrations",
|
||||
'--path' => "plugins/" . Str::studly($pluginCode) . "/database/migrations",
|
||||
'--force' => true
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚插件数据库迁移
|
||||
*/
|
||||
protected function runMigrationsRollback(string $pluginCode): void
|
||||
{
|
||||
$migrationsPath = $this->getPluginPath($pluginCode) . '/database/migrations';
|
||||
|
||||
if (File::exists($migrationsPath)) {
|
||||
Artisan::call('migrate:rollback', [
|
||||
'--path' => "plugins/" . Str::studly($pluginCode) . "/database/migrations",
|
||||
'--force' => true
|
||||
]);
|
||||
}
|
||||
@@ -352,10 +369,8 @@ class PluginManager
|
||||
*/
|
||||
public function uninstall(string $pluginCode): bool
|
||||
{
|
||||
// 先禁用插件
|
||||
$this->disable($pluginCode);
|
||||
|
||||
// 删除数据库记录
|
||||
$this->runMigrationsRollback($pluginCode);
|
||||
Plugin::query()->where('code', $pluginCode)->delete();
|
||||
|
||||
return true;
|
||||
@@ -400,6 +415,62 @@ class PluginManager
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 升级插件
|
||||
*
|
||||
* @param string $pluginCode
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function update(string $pluginCode): bool
|
||||
{
|
||||
$dbPlugin = Plugin::where('code', $pluginCode)->first();
|
||||
if (!$dbPlugin) {
|
||||
throw new \Exception('Plugin not installed: ' . $pluginCode);
|
||||
}
|
||||
|
||||
// 获取插件配置文件中的最新版本
|
||||
$configFile = $this->getPluginPath($pluginCode) . '/config.json';
|
||||
if (!File::exists($configFile)) {
|
||||
throw new \Exception('Plugin config file not found');
|
||||
}
|
||||
|
||||
$config = json_decode(File::get($configFile), true);
|
||||
if (!$config || !isset($config['version'])) {
|
||||
throw new \Exception('Invalid plugin config or missing version');
|
||||
}
|
||||
|
||||
$newVersion = $config['version'];
|
||||
$oldVersion = $dbPlugin->version;
|
||||
|
||||
if (version_compare($newVersion, $oldVersion, '<=')) {
|
||||
throw new \Exception('Plugin is already up to date');
|
||||
}
|
||||
|
||||
$this->disable($pluginCode);
|
||||
$this->runMigrations($pluginCode);
|
||||
|
||||
$plugin = $this->loadPlugin($pluginCode);
|
||||
if ($plugin) {
|
||||
if (!empty($dbPlugin->config)) {
|
||||
$plugin->setConfig(json_decode($dbPlugin->config, true));
|
||||
}
|
||||
|
||||
if (method_exists($plugin, 'update')) {
|
||||
$plugin->update($oldVersion, $newVersion);
|
||||
}
|
||||
}
|
||||
|
||||
$dbPlugin->update([
|
||||
'version' => $newVersion,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->enable($pluginCode);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传插件
|
||||
*
|
||||
@@ -466,6 +537,10 @@ class PluginManager
|
||||
File::deleteDirectory($pluginPath);
|
||||
File::deleteDirectory($extractPath);
|
||||
|
||||
if (Plugin::where('code', $config['code'])->exists()) {
|
||||
return $this->update($config['code']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Vendored
+16
-16
File diff suppressed because one or more lines are too long
Vendored
+193
-183
File diff suppressed because one or more lines are too long
@@ -16,7 +16,7 @@
|
||||
title: 'Xboard',
|
||||
version: '1.0.0',
|
||||
logo: 'https://xboard.io/i6mages/logo.png',
|
||||
secure_path: '/afbced4e',
|
||||
secure_path: '/ea25d015',
|
||||
}
|
||||
</script>
|
||||
<script src="./locales/en-US.js"></script>
|
||||
|
||||
Vendored
+33
-1
@@ -196,6 +196,7 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
},
|
||||
"button": {
|
||||
"install": "Install",
|
||||
"upgrade": "Upgrade",
|
||||
"config": "Configure",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable",
|
||||
@@ -224,6 +225,11 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
"description": "Are you sure you want to uninstall this plugin? Plugin data will be cleared after uninstallation.",
|
||||
"button": "Uninstall"
|
||||
},
|
||||
"upgrade": {
|
||||
"title": "Upgrade Plugin",
|
||||
"description": "Are you sure you want to upgrade this plugin? The plugin will be temporarily unavailable during the upgrade process.",
|
||||
"button": "Upgrade"
|
||||
},
|
||||
"config": {
|
||||
"title": "Configuration",
|
||||
"description": "Modify plugin configuration",
|
||||
@@ -237,6 +243,8 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
"messages": {
|
||||
"installSuccess": "Plugin installed successfully",
|
||||
"installError": "Failed to install plugin",
|
||||
"upgradeSuccess": "Plugin upgraded successfully",
|
||||
"upgradeError": "Failed to upgrade plugin",
|
||||
"uninstallSuccess": "Plugin uninstalled successfully",
|
||||
"uninstallError": "Failed to uninstall plugin",
|
||||
"enableSuccess": "Plugin enabled successfully",
|
||||
@@ -2033,8 +2041,31 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
"shadowsocks": {
|
||||
"cipher": {
|
||||
"label": "Encryption Method",
|
||||
"placeholder": "Select encryption method"
|
||||
"placeholder": "Select encryption method",
|
||||
"search_placeholder": "Search or enter custom encryption method...",
|
||||
"description": "Select preset encryption method or enter custom encryption method",
|
||||
"preset_group": "Preset Encryption Methods",
|
||||
"custom_group": "Custom Encryption Method",
|
||||
"current_value": "Current Value",
|
||||
"use_custom": "Use",
|
||||
"no_results": "No matching encryption method found",
|
||||
"custom_hint": "You can directly enter a custom encryption method, such as: aes-256-cfb",
|
||||
"custom_label": "Custom"
|
||||
},
|
||||
"plugin": {
|
||||
"label": "Plugin",
|
||||
"placeholder": "Select plugin",
|
||||
"obfs_hint": "Hint: Configuration format like obfs=http;obfs-host=www.bing.com;path=/",
|
||||
"v2ray_hint": "Hint: WebSocket mode format is mode=websocket;host=mydomain.me;path=/;tls=true, QUIC mode format is mode=quic;host=mydomain.me"
|
||||
},
|
||||
"plugin_opts": {
|
||||
"label": "Plugin Options",
|
||||
"description": "Enter plugin options in key=value;key2=value2 format",
|
||||
"placeholder": "Example: mode=tls;host=bing.com"
|
||||
},
|
||||
"client_fingerprint": "Client Fingerprint",
|
||||
"client_fingerprint_placeholder": "Select client fingerprint",
|
||||
"client_fingerprint_description": "Client spoofing fingerprint to reduce detection risk",
|
||||
"obfs": {
|
||||
"label": "Obfuscation",
|
||||
"placeholder": "Select obfuscation method",
|
||||
@@ -2676,6 +2707,7 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
"three_yearly": "Three Years",
|
||||
"onetime": "One Time",
|
||||
"reset_traffic": "Reset Traffic",
|
||||
"no_price": "No Price",
|
||||
"unit": {
|
||||
"month": "/month",
|
||||
"quarter": "/quarter",
|
||||
|
||||
Vendored
+24
-370
@@ -162,7 +162,6 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
"orderManagement": "주문 관리",
|
||||
"couponManagement": "쿠폰 관리",
|
||||
"userManagement": "사용자 관리",
|
||||
"trafficResetLogs": "트래픽 재설정 로그",
|
||||
"ticketManagement": "티켓 관리"
|
||||
},
|
||||
"plugin": {
|
||||
@@ -414,42 +413,20 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
"description": "허용된 이메일 접미사를 한 줄에 하나씩 입력하세요"
|
||||
}
|
||||
},
|
||||
"captcha": {
|
||||
"recaptcha": {
|
||||
"enable": {
|
||||
"label": "캡차 활성화",
|
||||
"description": "활성화하면 사용자는 등록 시 캡차 인증을 통과해야 합니다."
|
||||
"label": "reCAPTCHA 활성화",
|
||||
"description": "활성화하면 사용자는 등록 시 reCAPTCHA 인증을 통과해야 합니다."
|
||||
},
|
||||
"type": {
|
||||
"label": "캡차 유형",
|
||||
"description": "사용할 캡차 서비스 유형을 선택하세요",
|
||||
"options": {
|
||||
"recaptcha": "Google reCAPTCHA v2",
|
||||
"turnstile": "Cloudflare Turnstile"
|
||||
}
|
||||
"key": {
|
||||
"label": "reCAPTCHA 키",
|
||||
"placeholder": "reCAPTCHA 키 입력",
|
||||
"description": "reCAPTCHA 키를 입력하세요"
|
||||
},
|
||||
"recaptcha": {
|
||||
"key": {
|
||||
"label": "reCAPTCHA 키",
|
||||
"placeholder": "reCAPTCHA 키 입력",
|
||||
"description": "reCAPTCHA 키를 입력하세요"
|
||||
},
|
||||
"siteKey": {
|
||||
"label": "reCAPTCHA 사이트 키",
|
||||
"placeholder": "reCAPTCHA 사이트 키 입력",
|
||||
"description": "reCAPTCHA 사이트 키를 입력하세요"
|
||||
}
|
||||
},
|
||||
"turnstile": {
|
||||
"secretKey": {
|
||||
"label": "Turnstile 키",
|
||||
"placeholder": "Turnstile 키 입력",
|
||||
"description": "Cloudflare Turnstile 키를 입력하세요"
|
||||
},
|
||||
"siteKey": {
|
||||
"label": "Turnstile 사이트 키",
|
||||
"placeholder": "Turnstile 사이트 키 입력",
|
||||
"description": "Cloudflare Turnstile 사이트 키를 입력하세요"
|
||||
}
|
||||
"siteKey": {
|
||||
"label": "reCAPTCHA 사이트 키",
|
||||
"placeholder": "reCAPTCHA 사이트 키 입력",
|
||||
"description": "reCAPTCHA 사이트 키를 입력하세요"
|
||||
}
|
||||
},
|
||||
"registerLimit": {
|
||||
@@ -464,8 +441,8 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
},
|
||||
"expire": {
|
||||
"label": "제한 기간",
|
||||
"placeholder": "제한 기간을 분 단위로 입력",
|
||||
"description": "등록 제한 기간(분)"
|
||||
"placeholder": "제한 기간을 시간 단위로 입력",
|
||||
"description": "등록 제한 기간(시간)"
|
||||
}
|
||||
},
|
||||
"passwordLimit": {
|
||||
@@ -480,8 +457,8 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
},
|
||||
"expire": {
|
||||
"label": "잠금 기간",
|
||||
"placeholder": "잠금 기간을 분 단위로 입력",
|
||||
"description": "계정 잠금 기간(분)"
|
||||
"placeholder": "잠금 기간을 시간 단위로 입력",
|
||||
"description": "계정 잠금 기간(시간)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -819,10 +796,6 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
"surge": {
|
||||
"title": "Surge 템플릿",
|
||||
"description": "Surge의 구독 템플릿 형식 설정"
|
||||
},
|
||||
"surfboard": {
|
||||
"title": "Surfboard 템플릿",
|
||||
"description": "Surfboard의 구독 템플릿 형식 설정"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -887,7 +860,6 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
"save": "저장",
|
||||
"cancel": "취소",
|
||||
"confirm": "확인",
|
||||
"close": "닫기",
|
||||
"delete": {
|
||||
"success": "삭제되었습니다",
|
||||
"failed": "삭제에 실패했습니다"
|
||||
@@ -1050,21 +1022,7 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
"result": "결과",
|
||||
"duration": "소요 시간",
|
||||
"attempts": "시도 횟수",
|
||||
"nextRetry": "다음 재시도",
|
||||
"failedJobsDetailTitle": "실패한 작업 세부 정보",
|
||||
"viewFailedJobs": "실패한 작업 보기",
|
||||
"jobDetailTitle": "작업 세부 정보",
|
||||
"time": "시간",
|
||||
"queue": "대기열",
|
||||
"name": "작업 이름",
|
||||
"exception": "예외",
|
||||
"noFailedJobs": "실패한 작업 없음",
|
||||
"connection": "연결",
|
||||
"payload": "작업 페이로드",
|
||||
"viewDetail": "세부 정보 보기",
|
||||
"action": "작업",
|
||||
"noRecentOrder": "최근 주문 없음",
|
||||
"viewAll": "모두 보기"
|
||||
"nextRetry": "다음 재시도"
|
||||
},
|
||||
"actions": {
|
||||
"retry": "재시도",
|
||||
@@ -1074,86 +1032,7 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
},
|
||||
"empty": "대기열에 작업 없음",
|
||||
"loading": "대기열 상태 로딩 중...",
|
||||
"error": "대기열 상태 로드 실패",
|
||||
"recentOrders": {
|
||||
"title": "최근 주문"
|
||||
},
|
||||
"jobs": {
|
||||
"title": "작업 현황",
|
||||
"failedJobsDetailTitle": "실패한 작업 세부 정보",
|
||||
"viewFailedJobs": "실패한 작업 보기",
|
||||
"jobDetailTitle": "작업 세부 정보",
|
||||
"time": "시간",
|
||||
"queue": "대기열",
|
||||
"name": "작업 이름",
|
||||
"exception": "예외",
|
||||
"noFailedJobs": "실패한 작업 없음",
|
||||
"connection": "연결",
|
||||
"payload": "작업 페이로드",
|
||||
"viewDetail": "세부 정보 보기",
|
||||
"action": "작업"
|
||||
}
|
||||
},
|
||||
"systemLog": {
|
||||
"title": "시스템 로그",
|
||||
"description": "시스템 운영 로그 조회",
|
||||
"viewAll": "모두 보기",
|
||||
"level": "레벨",
|
||||
"time": "시간",
|
||||
"message": "메시지",
|
||||
"logTitle": "제목",
|
||||
"method": "요청 방법",
|
||||
"action": "작업",
|
||||
"context": "컨텍스트",
|
||||
"search": "로그 검색...",
|
||||
"noLogs": "로그 없음",
|
||||
"noInfoLogs": "정보 로그 없음",
|
||||
"noWarningLogs": "경고 로그 없음",
|
||||
"noErrorLogs": "오류 로그 없음",
|
||||
"noSearchResults": "일치하는 로그가 없습니다",
|
||||
"detailTitle": "로그 세부 정보",
|
||||
"viewDetail": "세부 정보 보기",
|
||||
"host": "호스트",
|
||||
"ip": "IP 주소",
|
||||
"uri": "URI",
|
||||
"requestData": "요청 데이터",
|
||||
"exception": "예외",
|
||||
"totalLogs": "총 로그 수",
|
||||
"tabs": {
|
||||
"all": "전체",
|
||||
"info": "정보",
|
||||
"warning": "경고",
|
||||
"error": "오류"
|
||||
},
|
||||
"filter": {
|
||||
"searchAndLevel": "필터 결과: \\\"{{keyword}}\\\"를 포함하고 레벨이 \\\"{{level}}\\\"인 로그 {{count}}개",
|
||||
"searchOnly": "검색 결과: \\\"{{keyword}}\\\"를 포함하는 로그 {{count}}개",
|
||||
"levelOnly": "필터 결과: 레벨이 \\\"{{level}}\\\"인 로그 {{count}}개",
|
||||
"reset": "필터 초기화"
|
||||
},
|
||||
"clearLogs": "로그 삭제",
|
||||
"clearDays": "삭제 일수",
|
||||
"clearDaysDesc": "며칠 전 로그를 삭제할지 (0-365일, 0은 오늘)",
|
||||
"clearLevel": "로그 레벨",
|
||||
"clearLimit": "배치 제한",
|
||||
"clearLimitDesc": "배치 삭제 수량 제한 (100-10000건)",
|
||||
"clearPreview": "삭제 미리보기",
|
||||
"getStats": "통계 가져오기",
|
||||
"cutoffDate": "마감일",
|
||||
"willClear": "삭제 예정",
|
||||
"logsUnit": "개 로그",
|
||||
"clearWarning": "이 작업은 되돌릴 수 없습니다. 신중하게 진행해 주세요!",
|
||||
"clearing": "삭제 중...",
|
||||
"confirmClear": "삭제 확인",
|
||||
"clearSuccess": "삭제 완료! {{count}}개 로그 삭제됨",
|
||||
"clearFailed": "삭제 실패",
|
||||
"getStatsFailed": "삭제 통계 가져오기 실패",
|
||||
"clearLogsFailed": "로그 삭제 실패"
|
||||
},
|
||||
"common": {
|
||||
"refresh": "새로고침",
|
||||
"close": "닫기",
|
||||
"pagination": "{{current}}/{{total}} 페이지, 총 {{count}}개 항목"
|
||||
"error": "대기열 상태 로드 실패"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "메뉴 및 기능 검색...",
|
||||
@@ -1273,8 +1152,6 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
"basicInfo": "기본 정보",
|
||||
"amountInfo": "금액 정보",
|
||||
"timeInfo": "시간 정보",
|
||||
"commissionInfo": "수수료 정보",
|
||||
"commissionStatusActive": "활성",
|
||||
"addOrder": "주문 추가",
|
||||
"assignOrder": "주문 할당",
|
||||
"fields": {
|
||||
@@ -1288,12 +1165,7 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
"refundAmount": "환불 금액",
|
||||
"deductionAmount": "차감 금액",
|
||||
"createdAt": "생성 시간",
|
||||
"updatedAt": "업데이트 시간",
|
||||
"commissionStatus": "수수료 상태",
|
||||
"commissionAmount": "수수료 금액",
|
||||
"actualCommissionAmount": "실제 수수료",
|
||||
"inviteUser": "초대자",
|
||||
"inviteUserId": "초대자 ID"
|
||||
"updatedAt": "업데이트 시간"
|
||||
},
|
||||
"placeholders": {
|
||||
"email": "사용자 이메일을 입력해주세요",
|
||||
@@ -1410,17 +1282,6 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
},
|
||||
"error": {
|
||||
"saveFailed": "쿠폰 저장 실패"
|
||||
},
|
||||
"timeRange": {
|
||||
"quickSet": "빠른 설정",
|
||||
"presets": {
|
||||
"1week": "1주",
|
||||
"2weeks": "2주",
|
||||
"1month": "1개월",
|
||||
"3months": "3개월",
|
||||
"6months": "6개월",
|
||||
"1year": "1년"
|
||||
}
|
||||
}
|
||||
},
|
||||
"period": {
|
||||
@@ -1645,17 +1506,6 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
"tooltip": "이 노드를 구독할 수 있는 그룹",
|
||||
"empty": "--"
|
||||
},
|
||||
"loadStatus": {
|
||||
"title": "부하 상태",
|
||||
"tooltip": "서버 리소스 사용량",
|
||||
"noData": "데이터 없음",
|
||||
"details": "시스템 부하 세부정보",
|
||||
"cpu": "CPU 사용률",
|
||||
"memory": "메모리 사용량",
|
||||
"swap": "스왑 사용량",
|
||||
"disk": "디스크 사용량",
|
||||
"lastUpdate": "마지막 업데이트"
|
||||
},
|
||||
"type": "유형",
|
||||
"actions": "작업",
|
||||
"copyAddress": "연결 주소 복사",
|
||||
@@ -1698,9 +1548,7 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
},
|
||||
"rate": {
|
||||
"label": "요금",
|
||||
"error": "요금은 필수입니다",
|
||||
"error_numeric": "요금은 숫자여야 합니다",
|
||||
"error_gte_zero": "요금은 0보다 크거나 같아야 합니다"
|
||||
"error": "올바른 요금을 입력해주세요"
|
||||
},
|
||||
"code": {
|
||||
"label": "사용자 지정 노드 ID",
|
||||
@@ -1719,21 +1567,18 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
},
|
||||
"host": {
|
||||
"label": "노드 주소",
|
||||
"placeholder": "도메인 또는 IP를 입력해주세요",
|
||||
"error": "노드 주소는 필수입니다"
|
||||
"placeholder": "도메인 또는 IP를 입력해주세요"
|
||||
},
|
||||
"port": {
|
||||
"label": "연결 포트",
|
||||
"placeholder": "사용자 연결 포트",
|
||||
"tooltip": "사용자가 실제로 연결하는 포트로, 클라이언트 설정에 입력해야 하는 포트 번호입니다. 중계 또는 터널을 사용하는 경우 서버가 실제로 수신하는 포트와 다를 수 있습니다.",
|
||||
"sync": "서버 포트와 동기화",
|
||||
"error": "연결 포트는 필수입니다"
|
||||
"sync": "서버 포트와 동기화"
|
||||
},
|
||||
"server_port": {
|
||||
"label": "서버 포트",
|
||||
"placeholder": "서버 포트 입력",
|
||||
"error": "서버 포트는 필수입니다",
|
||||
"tooltip": "서버의 실제 수신 포트입니다."
|
||||
"placeholder": "서버 수신 포트",
|
||||
"tooltip": "서버가 실제로 수신하는 포트로, 서버에서 실제로 열린 포트입니다. 중계 또는 터널을 사용하는 경우 사용자 연결 포트와 다를 수 있습니다."
|
||||
},
|
||||
"parent": {
|
||||
"label": "상위 노드",
|
||||
@@ -1895,83 +1740,6 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
"empty": "사용 가능한 ALPN 프로토콜이 없습니다"
|
||||
}
|
||||
}
|
||||
},
|
||||
"socks": {
|
||||
"version": {
|
||||
"label": "프로토콜 버전",
|
||||
"placeholder": "SOCKS 버전 선택"
|
||||
},
|
||||
"tls": {
|
||||
"label": "TLS",
|
||||
"placeholder": "보안을 선택해주세요",
|
||||
"disabled": "비활성화",
|
||||
"enabled": "활성화"
|
||||
},
|
||||
"tls_settings": {
|
||||
"server_name": {
|
||||
"label": "서버 이름 표시(SNI)",
|
||||
"placeholder": "사용하지 않는 경우 비워두세요"
|
||||
},
|
||||
"allow_insecure": "안전하지 않은 연결 허용?"
|
||||
},
|
||||
"network": {
|
||||
"label": "전송 프로토콜",
|
||||
"placeholder": "전송 프로토콜 선택"
|
||||
}
|
||||
},
|
||||
"naive": {
|
||||
"tls_settings": {
|
||||
"server_name": {
|
||||
"label": "서버 이름 표시(SNI)",
|
||||
"placeholder": "사용하지 않는 경우 비워두세요"
|
||||
},
|
||||
"allow_insecure": "안전하지 않은 연결 허용?"
|
||||
},
|
||||
"tls": {
|
||||
"label": "TLS",
|
||||
"placeholder": "보안을 선택해주세요",
|
||||
"disabled": "비활성화",
|
||||
"enabled": "활성화",
|
||||
"server_name": {
|
||||
"label": "서버 이름 표시(SNI)",
|
||||
"placeholder": "노드 주소와 인증서가 다를 때 인증서 확인에 사용"
|
||||
},
|
||||
"allow_insecure": "안전하지 않은 연결 허용"
|
||||
}
|
||||
},
|
||||
"http": {
|
||||
"tls_settings": {
|
||||
"server_name": {
|
||||
"label": "서버 이름 표시(SNI)",
|
||||
"placeholder": "사용하지 않는 경우 비워두세요"
|
||||
},
|
||||
"allow_insecure": "안전하지 않은 연결 허용?"
|
||||
},
|
||||
"tls": {
|
||||
"label": "TLS",
|
||||
"placeholder": "보안을 선택해주세요",
|
||||
"disabled": "비활성화",
|
||||
"enabled": "활성화",
|
||||
"server_name": {
|
||||
"label": "서버 이름 표시(SNI)",
|
||||
"placeholder": "노드 주소와 인증서가 다를 때 인증서 확인에 사용"
|
||||
},
|
||||
"allow_insecure": "안전하지 않은 연결 허용"
|
||||
}
|
||||
},
|
||||
"mieru": {
|
||||
"transport": {
|
||||
"label": "전송 프로토콜",
|
||||
"placeholder": "전송 프로토콜 선택"
|
||||
},
|
||||
"multiplexing": {
|
||||
"label": "다중화",
|
||||
"placeholder": "다중화 수준 선택",
|
||||
"MULTIPLEXING_OFF": "비활성화",
|
||||
"MULTIPLEXING_LOW": "낮음",
|
||||
"MULTIPLEXING_MIDDLE": "중간",
|
||||
"MULTIPLEXING_HIGH": "높음"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -2028,7 +1796,6 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
"orders": "주문 내역",
|
||||
"invites": "초대 내역",
|
||||
"traffic_records": "트래픽 기록",
|
||||
"reset_traffic": "트래픽 재설정",
|
||||
"delete": "삭제",
|
||||
"delete_confirm_title": "사용자 삭제 확인",
|
||||
"delete_confirm_description": "이 작업은 사용자 {{email}}와 관련된 모든 데이터(주문, 쿠폰, 트래픽 기록, 지원 티켓 등)를 영구적으로 삭제합니다. 이 작업은 취소할 수 없습니다. 계속하시겠습니까?"
|
||||
@@ -2105,8 +1872,7 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
"generate_count_placeholder": "일괄 생성할 수량 입력",
|
||||
"cancel": "취소",
|
||||
"submit": "생성",
|
||||
"success": "생성 완료",
|
||||
"download_csv": "CSV 파일로 내보내기"
|
||||
"success": "생성 완료"
|
||||
}
|
||||
},
|
||||
"edit": {
|
||||
@@ -2165,7 +1931,6 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
"title": "작업",
|
||||
"send_email": "이메일 보내기",
|
||||
"export_csv": "CSV 내보내기",
|
||||
"traffic_reset_stats": "트래픽 재설정 통계",
|
||||
"batch_ban": "일괄 차단",
|
||||
"confirm_ban": {
|
||||
"title": "일괄 차단 확인",
|
||||
@@ -2193,117 +1958,6 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
|
||||
"required_fields": "모든 필수 항목을 입력해주세요"
|
||||
}
|
||||
},
|
||||
"traffic_reset": {
|
||||
"title": "트래픽 재설정",
|
||||
"description": "사용자 {{email}}의 트래픽 사용량 재설정",
|
||||
"tabs": {
|
||||
"reset": "트래픽 재설정",
|
||||
"history": "재설정 기록"
|
||||
},
|
||||
"user_info": "사용자 정보",
|
||||
"warning": {
|
||||
"title": "중요 안내",
|
||||
"irreversible": "트래픽 재설정 작업은 되돌릴 수 없습니다. 신중하게 진행해주세요",
|
||||
"reset_to_zero": "재설정 후 사용자의 업로드 및 다운로드 트래픽이 0으로 초기화됩니다",
|
||||
"logged": "모든 재설정 작업은 시스템 로그에 기록됩니다"
|
||||
},
|
||||
"reason": {
|
||||
"label": "재설정 사유",
|
||||
"placeholder": "트래픽 재설정 사유를 입력해주세요 (선택사항)",
|
||||
"optional": "이 필드는 선택사항이며 재설정 사유를 기록하는 데 사용됩니다"
|
||||
},
|
||||
"confirm_reset": "재설정 확인",
|
||||
"resetting": "재설정 중...",
|
||||
"reset_success": "트래픽 재설정 성공",
|
||||
"reset_failed": "트래픽 재설정 실패",
|
||||
"history": {
|
||||
"summary": "재설정 개요",
|
||||
"reset_count": "재설정 횟수",
|
||||
"last_reset": "마지막 재설정",
|
||||
"next_reset": "다음 재설정",
|
||||
"never": "재설정된 적 없음",
|
||||
"no_schedule": "예약된 재설정 없음",
|
||||
"records": "재설정 기록",
|
||||
"recent_records": "최근 10번의 재설정 기록",
|
||||
"no_records": "재설정 기록이 없습니다",
|
||||
"reset_time": "재설정 시간",
|
||||
"traffic_cleared": "삭제된 트래픽"
|
||||
},
|
||||
"stats": {
|
||||
"title": "트래픽 재설정 통계",
|
||||
"description": "시스템 트래픽 재설정 통계 정보 보기",
|
||||
"time_range": "통계 시간 범위",
|
||||
"total_resets": "총 재설정 횟수",
|
||||
"auto_resets": "자동 재설정",
|
||||
"manual_resets": "수동 재설정",
|
||||
"cron_resets": "예약 재설정",
|
||||
"in_period": "최근 {{days}}일",
|
||||
"breakdown": "재설정 유형별 분석",
|
||||
"breakdown_description": "다양한 재설정 작업 유형의 백분율 분석",
|
||||
"auto_percentage": "자동 재설정 비율",
|
||||
"manual_percentage": "수동 재설정 비율",
|
||||
"cron_percentage": "예약 재설정 비율",
|
||||
"days_options": {
|
||||
"week": "지난 주",
|
||||
"month": "지난 달",
|
||||
"quarter": "지난 분기",
|
||||
"year": "지난 해"
|
||||
}
|
||||
}
|
||||
},
|
||||
"traffic_reset_logs": {
|
||||
"title": "트래픽 재설정 로그",
|
||||
"description": "시스템의 모든 트래픽 재설정 작업에 대한 상세 기록 보기",
|
||||
"columns": {
|
||||
"id": "로그 ID",
|
||||
"user": "사용자",
|
||||
"reset_type": "재설정 유형",
|
||||
"trigger_source": "트리거 소스",
|
||||
"cleared_traffic": "삭제된 트래픽",
|
||||
"cleared": "삭제됨",
|
||||
"upload": "업로드",
|
||||
"download": "다운로드",
|
||||
"reset_time": "재설정 시간",
|
||||
"log_time": "로그 시간"
|
||||
},
|
||||
"filters": {
|
||||
"search_user": "사용자 이메일 검색...",
|
||||
"reset_type": "재설정 유형",
|
||||
"trigger_source": "트리거 소스",
|
||||
"all_types": "모든 유형",
|
||||
"all_sources": "모든 소스",
|
||||
"start_date": "시작 날짜",
|
||||
"end_date": "종료 날짜",
|
||||
"apply_date": "필터 적용",
|
||||
"reset": "필터 초기화",
|
||||
"filter_title": "필터 옵션",
|
||||
"filter_description": "특정 트래픽 재설정 기록을 찾기 위한 필터 조건을 설정하세요",
|
||||
"reset_types": {
|
||||
"monthly": "월별 재설정",
|
||||
"first_day_month": "매월 1일 재설정",
|
||||
"yearly": "연별 재설정",
|
||||
"first_day_year": "매년 1월 1일 재설정",
|
||||
"manual": "수동 재설정"
|
||||
},
|
||||
"trigger_sources": {
|
||||
"auto": "자동 트리거",
|
||||
"manual": "수동 트리거",
|
||||
"cron": "예약 작업"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"export": "로그 내보내기",
|
||||
"exporting": "내보내는 중...",
|
||||
"export_success": "내보내기 성공",
|
||||
"export_failed": "내보내기 실패"
|
||||
},
|
||||
"trigger_descriptions": {
|
||||
"manual": "관리자가 수동으로 실행한 트래픽 재설정",
|
||||
"cron": "시스템 예약 작업에 의한 자동 실행",
|
||||
"auto": "시스템이 조건에 따라 자동 트리거",
|
||||
"other": "기타 방법으로 트리거"
|
||||
}
|
||||
},
|
||||
"send_mail": {
|
||||
"title": "이메일 보내기",
|
||||
"description": "선택하거나 필터링된 사용자에게 이메일 보내기",
|
||||
|
||||
Vendored
+33
-1
@@ -196,6 +196,7 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
},
|
||||
"button": {
|
||||
"install": "安装",
|
||||
"upgrade": "升级",
|
||||
"config": "配置",
|
||||
"enable": "启用",
|
||||
"disable": "禁用",
|
||||
@@ -224,6 +225,11 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
"description": "确定要卸载此插件吗?卸载后插件数据将被清除。",
|
||||
"button": "卸载"
|
||||
},
|
||||
"upgrade": {
|
||||
"title": "升级插件",
|
||||
"description": "确定要升级此插件吗?升级过程中插件将暂时不可用。",
|
||||
"button": "升级"
|
||||
},
|
||||
"config": {
|
||||
"title": "配置",
|
||||
"description": "修改插件配置",
|
||||
@@ -237,6 +243,8 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
"messages": {
|
||||
"installSuccess": "插件安装成功",
|
||||
"installError": "插件安装失败",
|
||||
"upgradeSuccess": "插件升级成功",
|
||||
"upgradeError": "插件升级失败",
|
||||
"uninstallSuccess": "插件卸载成功",
|
||||
"uninstallError": "插件卸载失败",
|
||||
"enableSuccess": "插件启用成功",
|
||||
@@ -2106,8 +2114,31 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
"shadowsocks": {
|
||||
"cipher": {
|
||||
"label": "加密算法",
|
||||
"placeholder": "选择加密算法"
|
||||
"placeholder": "选择加密算法",
|
||||
"search_placeholder": "搜索或输入自定义加密方式...",
|
||||
"description": "选择预设加密方式或输入自定义加密方式",
|
||||
"preset_group": "预设加密方式",
|
||||
"custom_group": "自定义加密方式",
|
||||
"current_value": "当前值",
|
||||
"use_custom": "使用",
|
||||
"no_results": "未找到匹配的加密方式",
|
||||
"custom_hint": "你可以直接输入自定义的加密方式,如:aes-256-cfb",
|
||||
"custom_label": "自定义"
|
||||
},
|
||||
"plugin": {
|
||||
"label": "插件",
|
||||
"placeholder": "选择插件",
|
||||
"obfs_hint": "提示:配置格式如 obfs=http;obfs-host=www.bing.com;path=/",
|
||||
"v2ray_hint": "提示:WebSocket模式格式为 mode=websocket;host=mydomain.me;path=/;tls=true,QUIC模式格式为 mode=quic;host=mydomain.me"
|
||||
},
|
||||
"plugin_opts": {
|
||||
"label": "插件选项",
|
||||
"description": "按照 key=value;key2=value2 格式输入插件选项",
|
||||
"placeholder": "例如: mode=tls;host=bing.com"
|
||||
},
|
||||
"client_fingerprint": "客户端指纹",
|
||||
"client_fingerprint_placeholder": "选择客户端指纹",
|
||||
"client_fingerprint_description": "客户端伪装指纹,用于降低被识别风险",
|
||||
"obfs": {
|
||||
"label": "混淆",
|
||||
"placeholder": "选择混淆方式",
|
||||
@@ -2749,6 +2780,7 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
"three_yearly": "三年付",
|
||||
"onetime": "流量包",
|
||||
"reset_traffic": "重置包",
|
||||
"no_price": "无价格",
|
||||
"unit": {
|
||||
"month": "元/月",
|
||||
"quarter": "元/季",
|
||||
|
||||
Reference in New Issue
Block a user