merge: sync upstream/master preserving local changes
This commit is contained in:
@@ -63,7 +63,7 @@ class MailLinkService
|
||||
'subject' => __('Login to :name', [
|
||||
'name' => admin_setting('app_name', 'Notification Service')
|
||||
]),
|
||||
'template_name' => 'login',
|
||||
'template_name' => 'mailLogin',
|
||||
'template_value' => [
|
||||
'name' => admin_setting('app_name', 'Notification Service'),
|
||||
'link' => $link,
|
||||
|
||||
@@ -32,6 +32,9 @@ class DeviceStateService
|
||||
|
||||
$this->removeNodeDevices($nodeId, $userId);
|
||||
|
||||
// Normalize: strip port suffix and deduplicate
|
||||
$ips = array_values(array_unique(array_map([self::class, 'normalizeIP'], $ips)));
|
||||
|
||||
if (!empty($ips)) {
|
||||
$fields = [];
|
||||
foreach ($ips as $ip) {
|
||||
@@ -98,6 +101,7 @@ class DeviceStateService
|
||||
Redis::hdel($key, $field);
|
||||
}
|
||||
}
|
||||
$this->notifyUpdate($userId);
|
||||
}
|
||||
|
||||
return array_keys($oldDevices);
|
||||
@@ -166,6 +170,22 @@ class DeviceStateService
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip port from IP address: "1.2.3.4:12345" → "1.2.3.4", "[::1]:443" → "::1"
|
||||
*/
|
||||
private static function normalizeIP(string $ip): string
|
||||
{
|
||||
// [IPv6]:port
|
||||
if (preg_match('/^\[(.+)\]:\d+$/', $ip, $m)) {
|
||||
return $m[1];
|
||||
}
|
||||
// IPv4:port
|
||||
if (preg_match('/^(\d+\.\d+\.\d+\.\d+):\d+$/', $ip, $m)) {
|
||||
return $m[1];
|
||||
}
|
||||
return $ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* notify update (throttle control)
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Services;
|
||||
|
||||
use App\Jobs\SendEmailJob;
|
||||
use App\Models\MailLog;
|
||||
use App\Models\MailTemplate;
|
||||
use App\Models\User;
|
||||
use App\Utils\CacheKey;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
@@ -14,6 +14,7 @@ use Illuminate\Support\Str;
|
||||
class PluginManager
|
||||
{
|
||||
protected string $pluginPath;
|
||||
protected string $corePluginPath;
|
||||
protected array $loadedPlugins = [];
|
||||
protected bool $pluginsInitialized = false;
|
||||
protected array $configTypesCache = [];
|
||||
@@ -21,6 +22,7 @@ class PluginManager
|
||||
public function __construct()
|
||||
{
|
||||
$this->pluginPath = base_path('plugins');
|
||||
$this->corePluginPath = base_path('plugins-core');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,14 +33,42 @@ class PluginManager
|
||||
return 'Plugin\\' . Str::studly($pluginCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件的基础路径
|
||||
*/
|
||||
public function resolvePluginPath(string $pluginCode): ?string
|
||||
{
|
||||
$dirName = Str::studly($pluginCode);
|
||||
$corePath = $this->corePluginPath . '/' . $dirName;
|
||||
if (File::isDirectory($corePath)) {
|
||||
return $corePath;
|
||||
}
|
||||
$userPath = $this->pluginPath . '/' . $dirName;
|
||||
if (File::isDirectory($userPath)) {
|
||||
return $userPath;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getPluginPath(string $pluginCode): string
|
||||
{
|
||||
return $this->resolvePluginPath($pluginCode)
|
||||
?? $this->pluginPath . '/' . Str::studly($pluginCode);
|
||||
}
|
||||
|
||||
public function getUserPluginPath(string $pluginCode): string
|
||||
{
|
||||
return $this->pluginPath . '/' . Str::studly($pluginCode);
|
||||
}
|
||||
|
||||
public function isCorePlugin(string $pluginCode): bool
|
||||
{
|
||||
$dirName = Str::studly($pluginCode);
|
||||
return File::isDirectory($this->corePluginPath . '/' . $dirName);
|
||||
}
|
||||
|
||||
public function getPluginPaths(): array
|
||||
{
|
||||
return [$this->corePluginPath, $this->pluginPath];
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载插件类
|
||||
*/
|
||||
@@ -399,17 +429,19 @@ class PluginManager
|
||||
*/
|
||||
public function delete(string $pluginCode): bool
|
||||
{
|
||||
// 先卸载插件
|
||||
if (Plugin::where('code', $pluginCode)->exists()) {
|
||||
$this->uninstall($pluginCode);
|
||||
}
|
||||
|
||||
$pluginPath = $this->getPluginPath($pluginCode);
|
||||
if ($this->isCorePlugin($pluginCode)) {
|
||||
throw new \Exception('核心插件不允许删除');
|
||||
}
|
||||
|
||||
$pluginPath = $this->getUserPluginPath($pluginCode);
|
||||
if (!File::exists($pluginPath)) {
|
||||
throw new \Exception('插件不存在');
|
||||
}
|
||||
|
||||
// 删除插件目录
|
||||
File::deleteDirectory($pluginPath);
|
||||
|
||||
return true;
|
||||
@@ -527,7 +559,7 @@ class PluginManager
|
||||
throw new \Exception('插件配置文件格式错误');
|
||||
}
|
||||
|
||||
$targetPath = $this->pluginPath . '/' . Str::studly($config['code']);
|
||||
$targetPath = $this->getUserPluginPath($config['code']);
|
||||
if (File::exists($targetPath)) {
|
||||
$installedConfigPath = $targetPath . '/config.json';
|
||||
if (!File::exists($installedConfigPath)) {
|
||||
@@ -678,12 +710,27 @@ class PluginManager
|
||||
*/
|
||||
public static function installDefaultPlugins(): void
|
||||
{
|
||||
foreach (Plugin::PROTECTED_PLUGINS as $pluginCode) {
|
||||
if (!Plugin::where('code', $pluginCode)->exists()) {
|
||||
$pluginManager = app(self::class);
|
||||
$pluginManager->install($pluginCode);
|
||||
$pluginManager->enable($pluginCode);
|
||||
Log::info("Installed and enabled default plugin: {$pluginCode}");
|
||||
$pluginManager = app(self::class);
|
||||
$coreDir = base_path('plugins-core');
|
||||
|
||||
if (!File::isDirectory($coreDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (File::directories($coreDir) as $directory) {
|
||||
$configFile = $directory . '/config.json';
|
||||
if (!File::exists($configFile)) {
|
||||
continue;
|
||||
}
|
||||
$config = json_decode(File::get($configFile), true);
|
||||
$code = $config['code'] ?? null;
|
||||
if (!$code) {
|
||||
continue;
|
||||
}
|
||||
if (!Plugin::where('code', $code)->exists()) {
|
||||
$pluginManager->install($code);
|
||||
$pluginManager->enable($code);
|
||||
Log::info("Installed and enabled core plugin: {$code}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,15 +284,12 @@ class ServerService
|
||||
'trojan' => [
|
||||
...$baseConfig,
|
||||
'host' => $host,
|
||||
'server_name' => data_get($protocolSettings, 'tls_settings.server_name') ?? $protocolSettings['server_name'],
|
||||
'server_name' => data_get($protocolSettings, 'tls_settings.server_name'),
|
||||
'multiplex' => data_get($protocolSettings, 'multiplex'),
|
||||
'tls' => (int) $protocolSettings['tls'],
|
||||
'tls_settings' => match ((int) $protocolSettings['tls']) {
|
||||
2 => $protocolSettings['reality_settings'],
|
||||
default => array_merge($protocolSettings['tls_settings'] ?? [], [
|
||||
'server_name' => data_get($protocolSettings, 'tls_settings.server_name') ?? $protocolSettings['server_name'],
|
||||
'allow_insecure' => data_get($protocolSettings, 'tls_settings.allow_insecure', $protocolSettings['allow_insecure']),
|
||||
]),
|
||||
default => $protocolSettings['tls_settings'],
|
||||
},
|
||||
],
|
||||
'vless' => [
|
||||
|
||||
@@ -22,11 +22,11 @@ class TicketService
|
||||
'ticket_id' => $ticket->id,
|
||||
'message' => $message
|
||||
]);
|
||||
if ($userId !== $ticket->user_id) {
|
||||
$ticket->reply_status = Ticket::STATUS_OPENING;
|
||||
} else {
|
||||
$ticket->reply_status = Ticket::STATUS_CLOSED;
|
||||
}
|
||||
$isAdmin = $userId !== $ticket->user_id;
|
||||
$ticket->reply_status = $isAdmin
|
||||
? Ticket::REPLY_STATUS_REPLIED
|
||||
: Ticket::REPLY_STATUS_WAITING;
|
||||
$ticket->last_reply_user_id = $userId;
|
||||
if (!$ticketMessage || !$ticket->save()) {
|
||||
throw new \Exception();
|
||||
}
|
||||
@@ -40,33 +40,15 @@ class TicketService
|
||||
|
||||
public function replyByAdmin($ticketId, $message, $userId): void
|
||||
{
|
||||
$ticket = Ticket::where('id', $ticketId)
|
||||
->first();
|
||||
$ticket = Ticket::where('id', $ticketId)->first();
|
||||
if (!$ticket) {
|
||||
throw new ApiException('工单不存在');
|
||||
}
|
||||
$ticket->status = Ticket::STATUS_OPENING;
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
$ticketMessage = TicketMessage::create([
|
||||
'user_id' => $userId,
|
||||
'ticket_id' => $ticket->id,
|
||||
'message' => $message
|
||||
]);
|
||||
if ($userId !== $ticket->user_id) {
|
||||
$ticket->reply_status = Ticket::STATUS_OPENING;
|
||||
} else {
|
||||
$ticket->reply_status = Ticket::STATUS_CLOSED;
|
||||
}
|
||||
if (!$ticketMessage || !$ticket->save()) {
|
||||
throw new ApiException('工单回复失败');
|
||||
}
|
||||
DB::commit();
|
||||
HookManager::call('ticket.reply.admin.after', [$ticket, $ticketMessage]);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
throw $e;
|
||||
$ticketMessage = $this->reply($ticket, $message, $userId);
|
||||
if (!$ticketMessage) {
|
||||
throw new ApiException('工单回复失败');
|
||||
}
|
||||
HookManager::call('ticket.reply.admin.after', [$ticket, $ticketMessage]);
|
||||
$this->sendEmailNotify($ticket, $ticketMessage);
|
||||
}
|
||||
|
||||
@@ -81,7 +63,9 @@ class TicketService
|
||||
$ticket = Ticket::create([
|
||||
'user_id' => $userId,
|
||||
'subject' => $subject,
|
||||
'level' => $level
|
||||
'level' => $level,
|
||||
'reply_status' => Ticket::REPLY_STATUS_WAITING,
|
||||
'last_reply_user_id' => $userId,
|
||||
]);
|
||||
if (!$ticket) {
|
||||
throw new ApiException('工单创建失败');
|
||||
|
||||
Reference in New Issue
Block a user