add debug to system managment

This commit is contained in:
Hesabix 2025-08-04 22:44:32 +00:00
parent 11caf42da8
commit 82d39dbb42
11 changed files with 1785 additions and 178 deletions

View file

@ -21,6 +21,9 @@ framework:
#esi: true
#fragments: true
http_client:
default_options:
timeout: 30
php_errors:
log: true

View file

@ -40,6 +40,10 @@ services:
- '../src/Entity/'
- '../src/Kernel.php'
App\Controller\System\DebugController:
arguments:
$kernelLogsDir: '%kernel.logs_dir%'
doctrine.orm.default_attribute_driver:
class: Doctrine\ORM\Mapping\Driver\AttributeDriver
arguments:
@ -122,7 +126,37 @@ services:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
App\Cog\TicketService:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
$explore: '@App\Service\Explore'
$jdate: '@Jdate'
$registryMGR: '@registryMGR'
$sms: '@SMS'
$uploadDirectory: '%SupportFilesDir%'
App\Service\Explore: ~
App\AiTool\AccountingDocService:
arguments:
$em: '@doctrine.orm.entity_manager'
$cogAccountingDocService: '@App\Cog\AccountingDocService'
App\AiTool\TicketService:
arguments:
$em: '@doctrine.orm.entity_manager'
$cogTicketService: '@App\Cog\TicketService'
App\Service\AGI\AGIService:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
$registryMGR: '@registryMGR'
$log: '@Log'
$provider: '@Provider'
$promptService: '@App\Service\AGI\Promps\PromptService'
$httpClient: '@http_client'
$httpKernel: '@kernel'
$explore: '@App\Service\Explore'
$jdate: '@Jdate'
$sms: '@SMS'
$uploadDirectory: '%SupportFilesDir%'

View file

@ -3,13 +3,115 @@
namespace App\AiTool;
use App\Cog\TicketService as CogTicketService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class TicketService
{
public function __construct(
private readonly CogTicketService $cogTicketService
) {
private EntityManagerInterface $em;
private CogTicketService $cogTicketService;
public function __construct(EntityManagerInterface $em, CogTicketService $cogTicketService)
{
$this->em = $em;
$this->cogTicketService = $cogTicketService;
}
/**
* دریافت لیست تیکت‌ها برای ابزار هوش مصنوعی
*/
public function getTicketsListAi(array $params, $acc = null): array
{
$acc = $acc ?? ($params['acc'] ?? null);
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
// اینجا باید منطق دریافت لیست تیکت‌ها پیاده‌سازی شود
// فعلاً یک پیام موقت برمی‌گردانیم
return [
'error' => 'این قابلیت در حال توسعه است'
];
} catch (\Exception $e) {
return [
'error' => 'خطا در دریافت لیست تیکت‌ها: ' . $e->getMessage()
];
}
}
/**
* دریافت اطلاعات تیکت بر اساس کد
*/
public function getTicketInfoByCode($code, $acc): array
{
if (!$code) {
return [
'error' => 'کد تیکت الزامی است'
];
}
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
// اینجا باید منطق دریافت اطلاعات تیکت پیاده‌سازی شود
return [
'error' => 'این قابلیت در حال توسعه است'
];
} catch (\Exception $e) {
return [
'error' => 'خطا در دریافت اطلاعات تیکت: ' . $e->getMessage()
];
}
}
/**
* افزودن یا ویرایش تیکت برای ابزار هوش مصنوعی
*/
public function addOrUpdateTicketAi(array $params, $acc = null, $code = 0): array
{
$acc = $acc ?? ($params['acc'] ?? null);
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
// اینجا باید منطق افزودن/ویرایش تیکت پیاده‌سازی شود
return [
'error' => 'این قابلیت در حال توسعه است'
];
} catch (\Exception $e) {
return [
'error' => 'خطا در افزودن/ویرایش تیکت: ' . $e->getMessage()
];
}
}
/**
* پاسخ به تیکت برای ابزار هوش مصنوعی
*/
public function replyToTicketAi(array $params, $acc = null): array
{
$acc = $acc ?? ($params['acc'] ?? null);
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
// اینجا باید منطق پاسخ به تیکت پیاده‌سازی شود
return [
'error' => 'این قابلیت در حال توسعه است'
];
} catch (\Exception $e) {
return [
'error' => 'خطا در پاسخ به تیکت: ' . $e->getMessage()
];
}
}
/**

View file

@ -14,6 +14,7 @@ use App\Service\Notification;
use App\Service\Provider;
use App\Service\registryMGR;
use App\Service\SMS;
use App\AiTool\TicketService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse;

View file

@ -0,0 +1,684 @@
<?php
namespace App\Controller\System;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpKernel\KernelInterface;
#[Route('/api/admin/debug')]
class DebugController extends AbstractController
{
private string $logsDir;
private Filesystem $filesystem;
private string $environment;
public function __construct(string $kernelLogsDir, KernelInterface $kernel)
{
$this->logsDir = $kernelLogsDir;
$this->filesystem = new Filesystem();
$this->environment = $kernel->getEnvironment();
}
#[Route('/logs', name: 'debug_logs_list', methods: ['GET'])]
public function getLogs(Request $request): JsonResponse
{
try {
$page = (int) $request->query->get('page', 1);
$limit = (int) $request->query->get('limit', 50);
$search = (string) $request->query->get('search', '');
$level = (string) $request->query->get('level', '');
$date = (string) $request->query->get('date', '');
// Handle sorting parameters safely
$sortBy = 'timestamp';
$sortDesc = true;
// Get sortBy parameter safely
$sortByParam = $request->query->get('sortBy');
if (is_string($sortByParam) && !empty($sortByParam)) {
$sortBy = $sortByParam;
}
// Get sortDesc parameter safely
$sortDescParam = $request->query->get('sortDesc');
if (is_string($sortDescParam)) {
$sortDesc = $sortDescParam === 'true' || $sortDescParam === '1';
} elseif (is_bool($sortDescParam)) {
$sortDesc = $sortDescParam;
}
// محدود کردن تعداد آیتم‌ها برای جلوگیری از مصرف حافظه زیاد
$limit = min($limit, 100);
$logs = $this->parseLogFilesOptimized($page, $limit, $search, $level, $date, $sortBy, $sortDesc);
return $this->json([
'success' => true,
'data' => $logs['items'],
'total' => $logs['total'],
'page' => $page,
'limit' => $limit,
'totalPages' => ceil($logs['total'] / $limit),
'environment' => $this->environment
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در دریافت لاگ‌ها: ' . $e->getMessage()
], 500);
}
}
#[Route('/logs/{id}', name: 'debug_log_detail', methods: ['GET'])]
public function getLogDetail(string $id): JsonResponse
{
try {
$logDetail = $this->getLogDetailById($id);
if (!$logDetail) {
return $this->json([
'success' => false,
'message' => 'لاگ مورد نظر یافت نشد'
], 404);
}
return $this->json([
'success' => true,
'data' => $logDetail
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در دریافت جزئیات لاگ: ' . $e->getMessage()
], 500);
}
}
#[Route('/logs', name: 'debug_logs_delete', methods: ['DELETE'])]
public function deleteLogs(Request $request): JsonResponse
{
try {
$data = json_decode($request->getContent(), true);
$logIds = $data['ids'] ?? [];
$deleteAll = $data['deleteAll'] ?? false;
if ($deleteAll) {
$this->clearAllLogs();
return $this->json([
'success' => true,
'message' => 'تمام لاگ‌ها با موفقیت حذف شدند'
]);
}
if (empty($logIds)) {
return $this->json([
'success' => false,
'message' => 'هیچ لاگی برای حذف انتخاب نشده'
], 400);
}
$deletedCount = $this->deleteLogsByIds($logIds);
return $this->json([
'success' => true,
'message' => "{$deletedCount} لاگ با موفقیت حذف شد",
'deletedCount' => $deletedCount
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در حذف لاگ‌ها: ' . $e->getMessage()
], 500);
}
}
#[Route('/logs/export', name: 'debug_logs_export', methods: ['GET'])]
public function exportLogs(Request $request): JsonResponse
{
try {
$format = $request->query->get('format', 'json');
$date = $request->query->get('date', '');
$level = $request->query->get('level', '');
$logs = $this->getLogsForExport($date, $level);
if ($format === 'csv') {
$csvData = $this->convertToCsv($logs);
return new JsonResponse($csvData, 200, [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="logs_' . $this->environment . '_' . date('Y-m-d') . '.csv"'
]);
}
return $this->json([
'success' => true,
'data' => $logs,
'total' => count($logs),
'environment' => $this->environment
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در صادرات لاگ‌ها: ' . $e->getMessage()
], 500);
}
}
#[Route('/system-info', name: 'debug_system_info', methods: ['GET'])]
public function getSystemInfo(): JsonResponse
{
try {
$info = [
'environment' => $this->environment,
'php_version' => PHP_VERSION,
'symfony_version' => \Symfony\Component\HttpKernel\Kernel::VERSION,
'memory_usage' => memory_get_usage(true),
'memory_peak' => memory_get_peak_usage(true),
'disk_free_space' => disk_free_space($this->logsDir),
'disk_total_space' => disk_total_space($this->logsDir),
'log_files_count' => $this->getLogFilesCount(),
'log_files_size' => $this->getLogFilesSize(),
'last_error_log' => $this->getLastErrorLog(),
'server_info' => [
'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown',
'php_sapi' => php_sapi_name(),
'max_execution_time' => ini_get('max_execution_time'),
'memory_limit' => ini_get('memory_limit'),
]
];
return $this->json([
'success' => true,
'data' => $info
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در دریافت اطلاعات سیستم: ' . $e->getMessage()
], 500);
}
}
private function parseLogFilesOptimized(int $page, int $limit, string $search, string $level, string $date, string $sortBy = 'timestamp', bool $sortDesc = true): array
{
$finder = new Finder();
// فقط فایل‌های لاگ مربوط به محیط فعلی را پیدا کن
$finder->files()
->in($this->logsDir)
->name('*.log')
->filter(function ($file) {
// فایل‌های مربوط به محیط فعلی
$filename = $file->getFilename();
return strpos($filename, $this->environment . '.log') !== false ||
strpos($filename, $this->environment) !== false ||
$filename === 'dev.log' || $filename === 'prod.log' ||
$filename === 'test.log';
})
->sortByModifiedTime();
$allLogs = [];
$id = 1;
$maxLogs = 5000; // محدود کردن تعداد کل لاگ‌ها برای جلوگیری از مصرف حافظه
foreach ($finder as $file) {
// بررسی اندازه فایل قبل از خواندن
if ($file->getSize() > 50 * 1024 * 1024) { // فایل‌های بزرگتر از 50MB را رد کن
continue;
}
$handle = fopen($file->getPathname(), 'r');
if (!$handle) {
continue;
}
$lineNumber = 0;
while (($line = fgets($handle)) !== false && count($allLogs) < $maxLogs) {
$lineNumber++;
// محدود کردن تعداد خطوط خوانده شده
if ($lineNumber > 10000) {
break;
}
if (empty(trim($line))) continue;
$logEntry = $this->parseLogLine($line, $file->getFilename(), $id++);
if ($logEntry) {
// فیلتر بر اساس جستجو
if ($search && !$this->matchesSearch($logEntry, $search)) {
continue;
}
// فیلتر بر اساس سطح
if ($level && $logEntry['level'] !== $level) {
continue;
}
// فیلتر بر اساس تاریخ
if ($date && $logEntry['date'] !== $date) {
continue;
}
$allLogs[] = $logEntry;
}
// بررسی مصرف حافظه
if (memory_get_usage() > 100 * 1024 * 1024) { // بیش از 100MB
break 2;
}
}
fclose($handle);
}
// اعمال مرتب‌سازی
usort($allLogs, function($a, $b) use ($sortBy, $sortDesc) {
$aValue = $a[$sortBy] ?? '';
$bValue = $b[$sortBy] ?? '';
// برای تاریخ و زمان، از timestamp استفاده کن
if ($sortBy === 'timestamp') {
$aValue = strtotime($aValue);
$bValue = strtotime($bValue);
}
if ($sortDesc) {
return $aValue < $bValue ? 1 : -1;
} else {
return $aValue > $bValue ? 1 : -1;
}
});
$totalCount = count($allLogs);
$offset = ($page - 1) * $limit;
$items = array_slice($allLogs, $offset, $limit);
return [
'items' => $items,
'total' => $totalCount
];
}
private function parseLogLine(string $line, string $filename, int $id): ?array
{
// فرمت لاگ Symfony: [timestamp] level: message {"context"} []
$pattern = '/^\[([^\]]+)\]\s+([^:]+):\s+(.+?)(?:\s+\{([^}]*)\}\s+\[\])?$/';
if (preg_match($pattern, $line, $matches)) {
$timestamp = $matches[1];
$level = strtoupper(trim($matches[2]));
$message = trim($matches[3]);
$context = isset($matches[4]) ? $matches[4] : '';
// محدود کردن طول پیام
if (strlen($message) > 1000) {
$message = substr($message, 0, 1000) . '...';
}
// پردازش context اگر وجود داشته باشد
$extra = [];
if (!empty($context)) {
// تلاش برای پارس کردن context به عنوان JSON
$contextData = json_decode('{' . $context . '}', true);
if ($contextData) {
$extra = $contextData;
} else {
$extra = ['context' => $context];
}
}
return [
'id' => $id,
'timestamp' => $timestamp,
'date' => date('Y-m-d', strtotime($timestamp)),
'time' => date('H:i:s', strtotime($timestamp)),
'level' => $level,
'message' => $message,
'filename' => $filename,
'environment' => $this->environment,
'extra' => $extra,
'raw' => substr($line, 0, 500)
];
}
// اگر فرمت استاندارد تطبیق نکرد، تلاش برای فرمت‌های دیگر
$patterns = [
// فرمت JSON
'/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)\s+(\w+)\s+(.+)$/',
// فرمت استاندارد
'/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] (\w+): (.+)$/',
// فرمت ساده
'/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+(\w+)\s+(.+)$/'
];
foreach ($patterns as $pattern) {
if (preg_match($pattern, $line, $matches)) {
$timestamp = $matches[1];
$level = strtoupper($matches[2]);
$message = $matches[3];
// محدود کردن طول پیام
if (strlen($message) > 1000) {
$message = substr($message, 0, 1000) . '...';
}
// استخراج اطلاعات اضافی از JSON
$extra = [];
if (strpos($message, '{') === 0) {
$jsonData = json_decode($message, true);
if ($jsonData) {
$message = $jsonData['message'] ?? $message;
$extra = $jsonData;
}
}
return [
'id' => $id,
'timestamp' => $timestamp,
'date' => date('Y-m-d', strtotime($timestamp)),
'time' => date('H:i:s', strtotime($timestamp)),
'level' => $level,
'message' => $message,
'filename' => $filename,
'environment' => $this->environment,
'extra' => $extra,
'raw' => substr($line, 0, 500)
];
}
}
// اگر هیچ الگویی تطبیق نکرد، لاگ را با اطلاعات حداقلی برگردان
return [
'id' => $id,
'timestamp' => date('Y-m-d H:i:s'),
'date' => date('Y-m-d'),
'time' => date('H:i:s'),
'level' => 'UNKNOWN',
'message' => substr($line, 0, 500),
'filename' => $filename,
'environment' => $this->environment,
'extra' => [],
'raw' => substr($line, 0, 500)
];
}
private function matchesSearch(array $logEntry, string $search): bool
{
$search = strtolower($search);
return strpos(strtolower($logEntry['message']), $search) !== false ||
strpos(strtolower($logEntry['level']), $search) !== false ||
strpos(strtolower($logEntry['filename']), $search) !== false;
}
private function getLogDetailById(string $id): ?array
{
$finder = new Finder();
$finder->files()
->in($this->logsDir)
->name('*.log')
->filter(function ($file) {
$filename = $file->getFilename();
return strpos($filename, $this->environment . '.log') !== false ||
strpos($filename, $this->environment) !== false ||
$filename === 'dev.log' || $filename === 'prod.log' ||
$filename === 'test.log';
});
foreach ($finder as $file) {
if ($file->getSize() > 10 * 1024 * 1024) { // فایل‌های بزرگتر از 10MB را رد کن
continue;
}
$handle = fopen($file->getPathname(), 'r');
if (!$handle) {
continue;
}
$lineId = 1;
while (($line = fgets($handle)) !== false) {
if (empty(trim($line))) {
$lineId++;
continue;
}
if ($lineId == $id) {
fclose($handle);
return $this->parseLogLine($line, $file->getFilename(), $lineId);
}
$lineId++;
}
fclose($handle);
}
return null;
}
private function deleteLogsByIds(array $ids): int
{
$deletedCount = 0;
$finder = new Finder();
$finder->files()
->in($this->logsDir)
->name('*.log')
->filter(function ($file) {
$filename = $file->getFilename();
return strpos($filename, $this->environment . '.log') !== false ||
strpos($filename, $this->environment) !== false ||
$filename === 'dev.log' || $filename === 'prod.log' ||
$filename === 'test.log';
});
foreach ($finder as $file) {
if ($file->getSize() > 50 * 1024 * 1024) { // فایل‌های بزرگتر از 50MB را رد کن
continue;
}
$handle = fopen($file->getPathname(), 'r');
if (!$handle) {
continue;
}
$lines = [];
$lineId = 1;
$fileModified = false;
while (($line = fgets($handle)) !== false) {
if (!in_array($lineId, $ids)) {
$lines[] = $line;
} else {
$deletedCount++;
$fileModified = true;
}
$lineId++;
}
fclose($handle);
if ($fileModified) {
$this->filesystem->dumpFile($file->getPathname(), implode('', $lines));
}
}
return $deletedCount;
}
private function clearAllLogs(): void
{
$finder = new Finder();
$finder->files()
->in($this->logsDir)
->name('*.log')
->filter(function ($file) {
$filename = $file->getFilename();
return strpos($filename, $this->environment . '.log') !== false ||
strpos($filename, $this->environment) !== false ||
$filename === 'dev.log' || $filename === 'prod.log' ||
$filename === 'test.log';
});
foreach ($finder as $file) {
$this->filesystem->remove($file->getPathname());
}
}
private function getLogsForExport(string $date, string $level): array
{
$finder = new Finder();
$finder->files()
->in($this->logsDir)
->name('*.log')
->filter(function ($file) {
$filename = $file->getFilename();
return strpos($filename, $this->environment . '.log') !== false ||
strpos($filename, $this->environment) !== false ||
$filename === 'dev.log' || $filename === 'prod.log' ||
$filename === 'test.log';
});
$logs = [];
$id = 1;
$maxLogs = 1000; // محدود کردن تعداد لاگ‌های صادر شده
foreach ($finder as $file) {
if ($file->getSize() > 10 * 1024 * 1024) { // فایل‌های بزرگتر از 10MB را رد کن
continue;
}
$handle = fopen($file->getPathname(), 'r');
if (!$handle) {
continue;
}
while (($line = fgets($handle)) !== false && count($logs) < $maxLogs) {
if (empty(trim($line))) continue;
$logEntry = $this->parseLogLine($line, $file->getFilename(), $id++);
if ($logEntry) {
if ($date && $logEntry['date'] !== $date) continue;
if ($level && $logEntry['level'] !== $level) continue;
$logs[] = $logEntry;
}
}
fclose($handle);
}
return $logs;
}
private function convertToCsv(array $logs): string
{
$csv = "ID,Date,Time,Level,Message,Filename,Environment\n";
foreach ($logs as $log) {
$csv .= sprintf(
"%d,%s,%s,%s,%s,%s,%s\n",
$log['id'],
$log['date'],
$log['time'],
$log['level'],
str_replace(',', ';', $log['message']),
$log['filename'],
$log['environment']
);
}
return $csv;
}
private function getLogFilesCount(): int
{
$finder = new Finder();
$finder->files()
->in($this->logsDir)
->name('*.log')
->filter(function ($file) {
$filename = $file->getFilename();
return strpos($filename, $this->environment . '.log') !== false ||
strpos($filename, $this->environment) !== false ||
$filename === 'dev.log' || $filename === 'prod.log' ||
$filename === 'test.log';
});
return iterator_count($finder);
}
private function getLogFilesSize(): int
{
$size = 0;
$finder = new Finder();
$finder->files()
->in($this->logsDir)
->name('*.log')
->filter(function ($file) {
$filename = $file->getFilename();
return strpos($filename, $this->environment . '.log') !== false ||
strpos($filename, $this->environment) !== false ||
$filename === 'dev.log' || $filename === 'prod.log' ||
$filename === 'test.log';
});
foreach ($finder as $file) {
$size += $file->getSize();
}
return $size;
}
private function getLastErrorLog(): ?array
{
$finder = new Finder();
$finder->files()
->in($this->logsDir)
->name('*.log')
->filter(function ($file) {
$filename = $file->getFilename();
return strpos($filename, $this->environment . '.log') !== false ||
strpos($filename, $this->environment) !== false ||
$filename === 'dev.log' || $filename === 'prod.log' ||
$filename === 'test.log';
})
->sortByModifiedTime();
$lastError = null;
$lastTimestamp = 0;
foreach ($finder as $file) {
if ($file->getSize() > 5 * 1024 * 1024) { // فایل‌های بزرگتر از 5MB را رد کن
continue;
}
$handle = fopen($file->getPathname(), 'r');
if (!$handle) {
continue;
}
while (($line = fgets($handle)) !== false) {
if (empty(trim($line))) continue;
$logEntry = $this->parseLogLine($line, $file->getFilename(), 1);
if ($logEntry && in_array($logEntry['level'], ['ERROR', 'CRITICAL', 'ALERT', 'EMERGENCY'])) {
$timestamp = strtotime($logEntry['timestamp']);
if ($timestamp > $lastTimestamp) {
$lastTimestamp = $timestamp;
$lastError = $logEntry;
}
}
}
fclose($handle);
}
return $lastError;
}
}

View file

@ -9,6 +9,9 @@ use App\Service\registryMGR;
use App\Service\Log;
use App\Service\Provider;
use App\Service\AGI\Promps\PromptService;
use App\Service\Explore;
use App\Service\Jdate;
use App\Service\SMS;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;
@ -23,6 +26,10 @@ class AGIService
private $promptService;
private $httpClient;
private $httpKernel;
private $explore;
private $jdate;
private $sms;
private $uploadDirectory;
public function __construct(
EntityManagerInterface $entityManager,
@ -31,7 +38,11 @@ class AGIService
Provider $provider,
PromptService $promptService,
HttpClientInterface $httpClient,
HttpKernelInterface $httpKernel
HttpKernelInterface $httpKernel,
Explore $explore,
Jdate $jdate,
SMS $sms,
string $uploadDirectory
) {
$this->em = $entityManager;
$this->registryMGR = $registryMGR;
@ -40,6 +51,10 @@ class AGIService
$this->promptService = $promptService;
$this->httpClient = $httpClient;
$this->httpKernel = $httpKernel;
$this->explore = $explore;
$this->jdate = $jdate;
$this->sms = $sms;
$this->uploadDirectory = $uploadDirectory;
}
/**
@ -337,19 +352,19 @@ class AGIService
return $accountingDocService->searchRowsAi($params, $params['acc'] ?? null);
// ابزارهای مربوط به تیکت
case 'getTicketsList':
$cogTicketService = new \App\Cog\TicketService($this->em);
$cogTicketService = new \App\Cog\TicketService($this->em, $this->explore, $this->jdate, $this->registryMGR, $this->sms, $this->uploadDirectory);
$ticketService = new \App\AiTool\TicketService($this->em, $cogTicketService);
return $ticketService->getTicketsListAi($params, $params['acc'] ?? null);
case 'getTicketInfo':
$cogTicketService = new \App\Cog\TicketService($this->em);
$cogTicketService = new \App\Cog\TicketService($this->em, $this->explore, $this->jdate, $this->registryMGR, $this->sms, $this->uploadDirectory);
$ticketService = new \App\AiTool\TicketService($this->em, $cogTicketService);
return $ticketService->getTicketInfoByCode($params['code'] ?? null, $params['acc'] ?? null);
case 'addOrUpdateTicket':
$cogTicketService = new \App\Cog\TicketService($this->em);
$cogTicketService = new \App\Cog\TicketService($this->em, $this->explore, $this->jdate, $this->registryMGR, $this->sms, $this->uploadDirectory);
$ticketService = new \App\AiTool\TicketService($this->em, $cogTicketService);
return $ticketService->addOrUpdateTicketAi($params, $params['acc'] ?? null, $params['code'] ?? 0);
case 'replyToTicket':
$cogTicketService = new \App\Cog\TicketService($this->em);
$cogTicketService = new \App\Cog\TicketService($this->em, $this->explore, $this->jdate, $this->registryMGR, $this->sms, $this->uploadDirectory);
$ticketService = new \App\AiTool\TicketService($this->em, $cogTicketService);
return $ticketService->replyToTicketAi($params, $params['acc'] ?? null);
default:

View file

@ -86,6 +86,8 @@ class PromptService
switch ($key) {
case 'person':
return $this->personPromptService->getAllPersonPrompts();
case 'ticket':
return $this->ticketPromptService->getAllTicketPrompts();
// در آینده موارد بیشتر اضافه خواهند شد
// case 'accounting':
// return $this->accountingPromptService->getAllAccountingPrompts();

View file

@ -2,204 +2,292 @@
namespace App\Service\AGI\Promps;
use Doctrine\ORM\EntityManagerInterface;
class TicketService
{
private $em;
public function __construct(EntityManagerInterface $entityManager)
{
$this->em = $entityManager;
}
/**
* دریافت ابزارهای مربوط به تیکت‌ها
* دریافت تمام ابزارهای بخش تیکت‌ها برای function calling
* @return array
*/
public function getTools(): array
{
return [
[
'name' => 'analyze_ticket',
'description' => 'تحلیل و دسته‌بندی تیکت',
'parameters' => [
'type' => 'object',
'properties' => [
'ticket_body' => [
'type' => 'string',
'description' => 'متن تیکت'
]
],
'required' => ['ticket_body']
$tools = [];
// ابزار getTicketsList
$ticketsListPrompt = $this->getTicketsListPrompt();
$ticketsListData = json_decode($ticketsListPrompt, true);
if ($ticketsListData) {
$tools[] = [
'type' => 'function',
'function' => [
'name' => $ticketsListData['tool'],
'description' => $ticketsListData['description'],
'parameters' => $ticketsListData['parameters']
]
],
[
'name' => 'draft_ticket_response',
'description' => 'تهیه پیش‌نویس پاسخ تیکت',
'parameters' => [
'type' => 'object',
'properties' => [
'ticket_body' => [
'type' => 'string',
'description' => 'متن تیکت'
],
'ticket_title' => [
'type' => 'string',
'description' => 'عنوان تیکت'
],
'history' => [
'type' => 'array',
'description' => 'تاریخچه مکالمات قبلی',
'items' => [
'type' => 'object',
'properties' => [
'sender' => [
'type' => 'string',
'description' => 'فرستنده پیام'
],
'message' => [
'type' => 'string',
'description' => 'متن پیام'
]
]
]
]
],
'required' => ['ticket_body', 'ticket_title']
];
}
// ابزار getTicketInfo
$ticketInfoPrompt = $this->getTicketInfoPrompt();
$ticketInfoData = json_decode($ticketInfoPrompt, true);
if ($ticketInfoData) {
$tools[] = [
'type' => 'function',
'function' => [
'name' => $ticketInfoData['tool'],
'description' => $ticketInfoData['description'],
'parameters' => $ticketInfoData['parameters']
]
]
];
}
/**
* پرامپت برای بررسی متن تیکت و دسته‌بندی آن
*/
public function getTicketAnalysisPrompt(string $ticketBody): string
{
return <<<PROMPT
لطفاً این تیکت پشتیبانی را بررسی و دسته‌بندی کنید:
متن تیکت:
{$ticketBody}
لطفاً موارد زیر را مشخص کنید:
1. موضوع اصلی تیکت
2. اولویت (کم، متوسط، زیاد)
3. بخش مربوطه (مالی، فنی، عمومی)
4. پیشنهاد برای پاسخ
PROMPT;
}
/**
* پرامپت برای تولید پیش‌نویس پاسخ به تیکت
*/
public function getDraftResponsePrompt(string $ticketBody, string $ticketTitle, array $history = []): string
{
$historyText = '';
if (!empty($history)) {
$historyText = "تاریخچه مکالمات قبلی:\n";
foreach ($history as $message) {
$historyText .= sprintf(
"- %s: %s\n",
$message['sender'],
$message['message']
);
}
];
}
return <<<PROMPT
لطفاً یک پیش‌نویس پاسخ مناسب برای این تیکت پشتیبانی آماده کنید:
عنوان تیکت: {$ticketTitle}
متن تیکت:
{$ticketBody}
{$historyText}
لطفاً یک پاسخ حرفه‌ای و دقیق با در نظر گرفتن نکات زیر آماده کنید:
1. لحن مؤدبانه و حرفه‌ای
2. پاسخگویی به تمام نکات مطرح شده در تیکت
3. ارائه راهکارهای عملی
4. درخواست اطلاعات تکمیلی در صورت نیاز
PROMPT;
// ابزار addOrUpdateTicket
$addOrUpdatePrompt = $this->getAddOrUpdateTicketPrompt();
$addOrUpdateData = json_decode($addOrUpdatePrompt, true);
if ($addOrUpdateData) {
$tools[] = [
'type' => 'function',
'function' => [
'name' => $addOrUpdateData['tool'],
'description' => $addOrUpdateData['description'],
'parameters' => $addOrUpdateData['parameters']
]
];
}
// ابزار replyToTicket
$replyPrompt = $this->getReplyToTicketPrompt();
$replyData = json_decode($replyPrompt, true);
if ($replyData) {
$tools[] = [
'type' => 'function',
'function' => [
'name' => $replyData['tool'],
'description' => $replyData['description'],
'parameters' => $replyData['parameters']
]
];
}
return $tools;
}
/**
* پرامپت برای پیشنهاد اقدامات بعدی برای تیکت
* تولید تمام پرامپ‌های بخش تیکت‌ها
* @return string
*/
public function getNextActionPrompt(string $ticketBody, string $currentStatus, array $previousActions = []): string
public function getAllTicketPrompts(): string
{
$previousActionsText = '';
if (!empty($previousActions)) {
$previousActionsText = "اقدامات قبلی:\n";
foreach ($previousActions as $action) {
$previousActionsText .= "- {$action}\n";
}
}
return <<<PROMPT
لطفاً اقدامات بعدی مناسب برای این تیکت را پیشنهاد دهید:
متن تیکت:
{$ticketBody}
وضعیت فعلی: {$currentStatus}
{$previousActionsText}
لطفاً موارد زیر را مشخص کنید:
1. آیا نیاز به ارجاع به بخش دیگری هست؟
2. آیا نیاز به اطلاعات تکمیلی از کاربر هست؟
3. اولویت رسیدگی به این تیکت
4. پیشنهاد برای اقدام بعدی
PROMPT;
$prompts = [];
$prompts[] = $this->getTicketsListPrompt();
$prompts[] = $this->getTicketInfoPrompt();
$prompts[] = $this->getAddOrUpdateTicketPrompt();
$prompts[] = $this->getReplyToTicketPrompt();
return implode("\n\n", $prompts);
}
/**
* پرامپت برای خلاصه‌سازی تیکت و تاریخچه آن
* پرامپ برای دریافت لیست تیکت‌ها
*/
public function getTicketSummaryPrompt(array $ticketHistory): string
public function getTicketsListPrompt(): string
{
$historyText = '';
foreach ($ticketHistory as $entry) {
$historyText .= sprintf(
"- %s (%s): %s\n",
$entry['date'],
$entry['user'],
$entry['message']
);
return '{
"tool": "getTicketsList",
"description": "دریافت لیست تیکت‌های پشتیبانی با فیلتر و صفحه‌بندی",
"endpoint": "/api/ticket/list",
"method": "POST",
"parameters": {
"type": "object",
"properties": {
"page": {"type": "integer", "description": "شماره صفحه"},
"itemsPerPage": {"type": "integer", "description": "تعداد آیتم در هر صفحه"},
"search": {"type": "string", "description": "متن جست‌وجو (عنوان، کد، و غیره)"},
"status": {"type": "array", "items": {"type": "string"}, "description": "فیلتر وضعیت تیکت‌ها (اختیاری)"},
"priority": {"type": "array", "items": {"type": "string"}, "description": "فیلتر اولویت تیکت‌ها (اختیاری)"},
"sortBy": {"type": ["string", "null"], "description": "فیلد مرتب‌سازی (اختیاری)"},
"acc": {"type": "object", "description": "اطلاعات دسترسی (مورد نیاز برای backend)"}
},
"required": ["page", "itemsPerPage", "search"]
},
"output": {
"items": [
{
"id": "integer",
"code": "string",
"title": "string",
"body": "string",
"status": "string",
"priority": "string",
"dateSubmit": "integer",
"submitter": "object",
"main": "integer",
"fileName": "string|null"
}
],
"total": "integer",
"unfilteredTotal": "integer"
},
"examples": {
"input": {"page":1,"itemsPerPage":10,"search":"مشکل","status":["در حال پیگیری","بسته شده"],"priority":["کم","متوسط","زیاد"],"sortBy":null,"acc":{"bid":2,"user":2,"year":2,"access":true,"money":1,"ai":true}},
"output": {
"items": [
{
"id": 1,
"code": "TKT001",
"title": "مشکل در ورود به سیستم",
"body": "نمی‌توانم وارد سیستم شوم",
"status": "در حال پیگیری",
"priority": "متوسط",
"dateSubmit": 1703123456,
"submitter": {"id": 1, "name": "کاربر نمونه"},
"main": 0,
"fileName": null
}
return <<<PROMPT
لطفاً خلاصه‌ای از این تیکت و تاریخچه آن تهیه کنید:
تاریخچه تیکت:
{$historyText}
لطفاً موارد زیر را در خلاصه مشخص کنید:
1. موضوع اصلی و مشکل گزارش شده
2. اقدامات انجام شده
3. وضعیت فعلی
4. نکات مهم برای پیگیری
PROMPT;
],
"total": 1,
"unfilteredTotal": 5
}
}
}';
}
/**
* پرامپت برای دسته‌بندی خودکار تیکت‌ها
* پرامپ برای دریافت اطلاعات تیکت
*/
public function getTicketCategorizationPrompt(array $tickets): string
public function getTicketInfoPrompt(): string
{
$ticketsText = '';
foreach ($tickets as $ticket) {
$ticketsText .= sprintf(
"عنوان: %s\nمتن: %s\n\n",
$ticket['title'],
$ticket['body']
);
return '{
"tool": "getTicketInfo",
"description": "دریافت اطلاعات کامل یک تیکت بر اساس کد",
"endpoint": "/api/ticket/info/{code}",
"method": "GET",
"parameters": {
"type": "object",
"properties": {
"code": {"type": "string", "description": "کد تیکت (مثل TKT001, TKT002)"},
"acc": {"type": "object", "description": "اطلاعات دسترسی (مورد نیاز برای backend)"}
},
"required": ["code"]
},
"output": {
"id": "integer",
"code": "string",
"title": "string",
"body": "string",
"status": "string",
"priority": "string",
"dateSubmit": "integer",
"submitter": "object",
"main": "integer",
"fileName": "string|null",
"replies": [
{
"id": "integer",
"body": "string",
"dateSubmit": "integer",
"submitter": "object",
"fileName": "string|null"
}
]
},
"examples": {
"input": {"code": "TKT001"},
"output": {
"id": 1,
"code": "TKT001",
"title": "مشکل در ورود به سیستم",
"body": "نمی‌توانم وارد سیستم شوم",
"status": "در حال پیگیری",
"priority": "متوسط",
"dateSubmit": 1703123456,
"submitter": {"id": 1, "name": "کاربر نمونه"},
"main": 0,
"fileName": null,
"replies": [
{
"id": 2,
"body": "لطفاً مرورگر خود را پاک کنید و دوباره تلاش کنید",
"dateSubmit": 1703124000,
"submitter": {"id": 2, "name": "پشتیبان"},
"fileName": null
}
]
}
}
}';
}
return <<<PROMPT
لطفاً این تیکت‌ها را بر اساس موضوع و محتوا دسته‌بندی کنید:
/**
* پرامپ برای افزودن یا ویرایش تیکت
*/
public function getAddOrUpdateTicketPrompt(): string
{
return '{
"tool": "addOrUpdateTicket",
"description": "برای ویرایش یک تیکت ابتدا باید با ابزار جست‌وجوی تیکت (getTicketsList) تیکت مورد نظر را پیدا کنید. اگر چند نتیجه یافت شد، باید از کاربر بپرسید کدام را می‌خواهد ویرایش کند و کد (code) آن را دریافت کنید. سپس با ارسال کد و اطلاعات جدید به این ابزار، ویرایش انجام می‌شود. اگر code برابر 0 یا ارسال نشود، تیکت جدید ایجاد خواهد شد. افزودن تیکت جدید یا ویرایش تیکت موجود",
"endpoint": "/api/ticket/mod/{code}",
"method": "POST",
"parameters": {
"type": "object",
"properties": {
"title": {"type": "string", "description": "عنوان تیکت (مورد نیاز)"},
"body": {"type": "string", "description": "متن تیکت (مورد نیاز)"},
"priority": {"type": "string", "description": "اولویت تیکت (کم، متوسط، زیاد)"},
"status": {"type": "string", "description": "وضعیت تیکت (جدید، در حال پیگیری، بسته شده)"},
"code": {"type": ["integer", "string"], "description": "کد تیکت (0 برای جدید، در غیر این صورت برای ویرایش)"},
"acc": {"type": "object", "description": "اطلاعات دسترسی (مورد نیاز برای backend)"}
},
"required": ["title", "body"]
},
"output": {
"Success": "boolean",
"result": "integer",
"message": "string"
},
"examples": {
"input": {"title":"مشکل جدید","body":"نمی‌توانم فایل آپلود کنم","priority":"متوسط","status":"جدید","code":0,"acc":{"bid":2,"user":2,"year":2,"access":true,"money":1,"ai":true}},
"output": {"Success":true,"result":1,"message":"تیکت با موفقیت ایجاد شد"}
}
}';
}
تیکت‌ها:
{$ticketsText}
لطفاً برای هر تیکت موارد زیر را مشخص کنید:
1. دسته اصلی (مالی، فنی، پشتیبانی عمومی، آموزش)
2. زیر دسته
3. برچسب‌های پیشنهادی
4. اولویت پیشنهادی
PROMPT;
/**
* پرامپ برای پاسخ به تیکت
*/
public function getReplyToTicketPrompt(): string
{
return '{
"tool": "replyToTicket",
"description": "ارسال پاسخ به یک تیکت موجود",
"endpoint": "/api/ticket/reply",
"method": "POST",
"parameters": {
"type": "object",
"properties": {
"ticketCode": {"type": "string", "description": "کد تیکت (مورد نیاز)"},
"body": {"type": "string", "description": "متن پاسخ (مورد نیاز)"},
"status": {"type": "string", "description": "وضعیت جدید تیکت (اختیاری)"},
"acc": {"type": "object", "description": "اطلاعات دسترسی (مورد نیاز برای backend)"}
},
"required": ["ticketCode", "body"]
},
"output": {
"Success": "boolean",
"result": "integer",
"message": "string"
},
"examples": {
"input": {"ticketCode":"TKT001","body":"لطفاً مرورگر خود را پاک کنید و دوباره تلاش کنید","status":"در حال پیگیری","acc":{"bid":2,"user":2,"year":2,"access":true,"money":1,"ai":true}},
"output": {"Success":true,"result":1,"message":"پاسخ با موفقیت ارسال شد"}
}
}';
}
}

View file

@ -186,6 +186,14 @@ const router = createRouter({
'login': true
}
},
{
path: 'manager/debug',
component: () => import('../views/user/manager/debug/debug.vue'),
meta: {
'title': 'دیباگ سیستم',
'login': true
}
},
{
path: 'manager/changes/mod/:id',
component: () => import('../views/user/manager/reportchange/mod.vue'),

View file

@ -0,0 +1,669 @@
<template>
<v-container fluid>
<v-row>
<v-col cols="12">
<v-card>
<v-card-title class="d-flex justify-space-between align-center">
<div>
<v-icon class="mr-2">mdi-bug</v-icon>
مدیریت دیباگ سیستم
<v-chip
v-if="environment"
:color="getEnvironmentColor(environment)"
size="small"
class="ml-2"
>
{{ environment.toUpperCase() }}
</v-chip>
</div>
<div class="d-flex align-center">
<v-btn
color="error"
variant="outlined"
:disabled="selectedLogs.length === 0"
prepend-icon="mdi-delete"
class="mr-2"
@click="showDeleteDialog = true"
>
حذف انتخاب شده ({{ selectedLogs.length }})
</v-btn>
<v-btn
color="warning"
variant="outlined"
prepend-icon="mdi-delete-sweep"
@click="showDeleteAllDialog = true"
>
حذف همه
</v-btn>
</div>
</v-card-title>
<!-- فیلترها -->
<v-card-text>
<v-row>
<v-col cols="12" md="3">
<v-text-field
v-model="filters.search"
label="جستجو"
prepend-icon="mdi-magnify"
clearable
@update:model-value="debouncedLoadLogs"
/>
</v-col>
<v-col cols="12" md="2">
<v-select
v-model="filters.level"
label="سطح لاگ"
:items="logLevels"
clearable
@update:model-value="loadLogs"
/>
</v-col>
<v-col cols="12" md="2">
<v-text-field
v-model="filters.date"
label="تاریخ"
type="date"
clearable
@update:model-value="loadLogs"
/>
</v-col>
<v-col cols="12" md="2">
<v-select
v-model="pagination.limit"
label="تعداد در صفحه"
:items="[10, 25, 50, 100]"
@update:model-value="loadLogs"
/>
</v-col>
</v-row>
</v-card-text>
<!-- اطلاعات سیستم -->
<v-card-text v-if="systemInfo">
<v-alert
type="info"
variant="tonal"
class="mb-4"
>
<div>
<strong>اطلاعات سیستم:</strong>
محیط: <v-chip :color="getEnvironmentColor(systemInfo.environment)" size="small">{{ systemInfo.environment.toUpperCase() }}</v-chip> |
فایلهای لاگ: {{ systemInfo.log_files_count }} |
حجم کل: {{ formatBytes(systemInfo.log_files_size) }} |
حافظه استفاده شده: {{ formatBytes(systemInfo.memory_usage) }}
</div>
</v-alert>
</v-card-text>
<!-- جدول لاگها -->
<v-data-table
v-model="selectedLogs"
:headers="headers"
:items="logs"
:loading="loading"
:items-per-page="pagination.limit"
:page="pagination.page"
:total-items="pagination.total"
:sort-by="sortBy"
:sort-desc="sortDesc"
show-select
item-key="id"
class="elevation-1"
@update:options="handleTableUpdate"
>
<template v-slot:item.level="{ item }">
<v-chip
:color="getLevelColor(item.level)"
size="small"
variant="flat"
>
{{ item.level }}
</v-chip>
</template>
<template v-slot:item.timestamp="{ item }">
<div>
<div class="text-body-2">{{ item.date }}</div>
<div class="text-caption text-grey">{{ item.time }}</div>
</div>
</template>
<template v-slot:item.message="{ item }">
<div class="text-truncate" style="max-width: 300px;">
{{ item.message }}
</div>
</template>
<template v-slot:item.environment="{ item }">
<v-chip
:color="getEnvironmentColor(item.environment)"
size="small"
variant="flat"
>
{{ item.environment.toUpperCase() }}
</v-chip>
</template>
<template v-slot:item.actions="{ item }">
<v-btn
size="small"
color="primary"
variant="text"
@click="viewLogDetail(item)"
prepend-icon="mdi-eye"
>
مشاهده
</v-btn>
</template>
</v-data-table>
<!-- صفحهبندی -->
<v-card-actions class="justify-center">
<v-pagination
v-model="pagination.page"
:length="pagination.totalPages"
:total-visible="7"
@update:model-value="loadLogs"
/>
</v-card-actions>
</v-card>
</v-col>
</v-row>
<!-- دیالوگ جزئیات لاگ -->
<v-dialog v-model="showDetailDialog" max-width="800px">
<v-card>
<v-card-title>
<v-icon class="mr-2">mdi-file-document</v-icon>
جزئیات لاگ
<v-chip
v-if="selectedLog?.environment"
:color="getEnvironmentColor(selectedLog.environment)"
size="small"
class="ml-2"
>
{{ selectedLog.environment.toUpperCase() }}
</v-chip>
</v-card-title>
<v-card-text>
<v-row v-if="selectedLog">
<v-col cols="12" md="6">
<v-list>
<v-list-item>
<template v-slot:prepend>
<v-icon>mdi-calendar</v-icon>
</template>
<v-list-item-title>تاریخ</v-list-item-title>
<v-list-item-subtitle>{{ selectedLog.date }} {{ selectedLog.time }}</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template v-slot:prepend>
<v-icon>mdi-alert-circle</v-icon>
</template>
<v-list-item-title>سطح</v-list-item-title>
<v-list-item-subtitle>
<v-chip :color="getLevelColor(selectedLog.level)" size="small">
{{ selectedLog.level }}
</v-chip>
</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template v-slot:prepend>
<v-icon>mdi-file</v-icon>
</template>
<v-list-item-title>فایل</v-list-item-title>
<v-list-item-subtitle>{{ selectedLog.filename }}</v-list-item-subtitle>
</v-list-item>
<v-list-item v-if="selectedLog.environment">
<template v-slot:prepend>
<v-icon>mdi-server</v-icon>
</template>
<v-list-item-title>محیط</v-list-item-title>
<v-list-item-subtitle>
<v-chip :color="getEnvironmentColor(selectedLog.environment)" size="small">
{{ selectedLog.environment.toUpperCase() }}
</v-chip>
</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-col>
<v-col cols="12" md="6">
<v-textarea
v-model="selectedLog.message"
label="پیام"
readonly
rows="4"
variant="outlined"
/>
</v-col>
<v-col cols="12" v-if="selectedLog.extra && Object.keys(selectedLog.extra).length > 0">
<v-expansion-panels>
<v-expansion-panel>
<v-expansion-panel-title>
اطلاعات اضافی
</v-expansion-panel-title>
<v-expansion-panel-text>
<pre class="text-body-2">{{ JSON.stringify(selectedLog.extra, null, 2) }}</pre>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-col>
<v-col cols="12">
<v-textarea
v-model="selectedLog.raw"
label="متن خام"
readonly
rows="6"
variant="outlined"
/>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn color="primary" @click="showDetailDialog = false">
بستن
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- دیالوگ حذف انتخاب شده -->
<v-dialog v-model="showDeleteDialog" max-width="400px">
<v-card>
<v-card-title class="text-h6">
<v-icon class="mr-2" color="error">mdi-delete</v-icon>
حذف لاگهای انتخاب شده
</v-card-title>
<v-card-text>
آیا از حذف {{ selectedLogs.length }} لاگ انتخاب شده اطمینان دارید؟
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="showDeleteDialog = false">انصراف</v-btn>
<v-btn color="error" @click="deleteSelectedLogs" :loading="deleting">
حذف
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- دیالوگ حذف همه -->
<v-dialog v-model="showDeleteAllDialog" max-width="400px">
<v-card>
<v-card-title class="text-h6">
<v-icon class="mr-2" color="warning">mdi-delete-sweep</v-icon>
حذف تمام لاگها
</v-card-title>
<v-card-text>
آیا از حذف تمام لاگهای سیستم اطمینان دارید؟ این عملیات غیرقابل بازگشت است.
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="showDeleteAllDialog = false">انصراف</v-btn>
<v-btn color="warning" @click="deleteAllLogs" :loading="deleting">
حذف همه
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- اسنکبار -->
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="snackbar.timeout"
>
{{ snackbar.message }}
<template v-slot:actions>
<v-btn
color="white"
variant="text"
@click="snackbar.show = false"
>
بستن
</v-btn>
</template>
</v-snackbar>
</v-container>
</template>
<script>
import { ref, reactive, onMounted, computed } from 'vue'
import axios from 'axios'
export default {
name: 'Debug',
setup() {
const loading = ref(false)
const loadingSystemInfo = ref(false)
const deleting = ref(false)
const logs = ref([])
const selectedLogs = ref([])
const selectedLog = ref(null)
const showDetailDialog = ref(false)
const showDeleteDialog = ref(false)
const showDeleteAllDialog = ref(false)
const systemInfo = ref(null)
const environment = ref('')
const filters = reactive({
search: '',
level: '',
date: ''
})
const pagination = reactive({
page: 1,
limit: 50,
total: 0,
totalPages: 0
})
const snackbar = reactive({
show: false,
message: '',
color: 'success',
timeout: 3000
})
// متغیرهای مرتبسازی
const sortBy = ref(['timestamp'])
const sortDesc = ref([true])
const headers = [
{ title: 'تاریخ', key: 'timestamp', sortable: true },
{ title: 'سطح', key: 'level', sortable: true },
{ title: 'پیام', key: 'message', sortable: false },
{ title: 'فایل', key: 'filename', sortable: true },
{ title: 'محیط', key: 'environment', sortable: true },
{ title: 'عملیات', key: 'actions', sortable: false }
]
const logLevels = [
'DEBUG',
'INFO',
'WARNING',
'ERROR',
'CRITICAL',
'ALERT',
'EMERGENCY'
]
// Debounce برای جستجو
let searchTimeout = null
const debouncedLoadLogs = () => {
if (searchTimeout) {
clearTimeout(searchTimeout)
}
searchTimeout = setTimeout(() => {
pagination.page = 1
loadLogs()
}, 500)
}
const getLevelColor = (level) => {
const colors = {
'DEBUG': 'grey',
'INFO': 'blue',
'WARNING': 'orange',
'ERROR': 'red',
'CRITICAL': 'red-darken-2',
'ALERT': 'red-darken-3',
'EMERGENCY': 'red-darken-4'
}
return colors[level] || 'grey'
}
const getEnvironmentColor = (env) => {
const colors = {
'dev': 'green',
'prod': 'red',
'test': 'orange'
}
return colors[env] || 'blue'
}
const formatBytes = (bytes) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
const loadLogs = async () => {
try {
loading.value = true
// Get current sorting values
const currentSortBy = sortBy.value[0] || 'timestamp'
const currentSortDesc = sortDesc.value[0] || true
// Create simple params object
const params = {
page: pagination.page,
limit: pagination.limit,
search: filters.search || '',
level: filters.level || '',
date: filters.date || '',
sortBy: currentSortBy,
sortDesc: currentSortDesc
}
console.log('Sending params:', params)
const response = await axios.get('/api/admin/debug/logs', { params })
if (response.data.success) {
logs.value = response.data.data
pagination.total = response.data.total
pagination.totalPages = response.data.totalPages
environment.value = response.data.environment
} else {
showSnackbar('خطا در دریافت لاگ‌ها', 'error')
}
} catch (error) {
console.error('Error loading logs:', error)
showSnackbar('خطا در دریافت لاگ‌ها', 'error')
} finally {
loading.value = false
}
}
const loadSystemInfo = async () => {
try {
loadingSystemInfo.value = true
const response = await axios.get('/api/admin/debug/system-info')
if (response.data.success) {
systemInfo.value = response.data.data
environment.value = response.data.data.environment
}
} catch (error) {
console.error('Error loading system info:', error)
} finally {
loadingSystemInfo.value = false
}
}
const handleTableUpdate = (options) => {
try {
console.log('Table update options:', options)
let shouldReload = false
// بررسی تغییرات صفحهبندی
if (options && options.page !== undefined && options.page !== pagination.page) {
pagination.page = options.page
shouldReload = true
}
// بررسی تغییرات مرتبسازی
if (options && options.sortBy && Array.isArray(options.sortBy) && options.sortBy.length > 0) {
const newSortBy = options.sortBy[0]
// بررسی وجود sortDesc و مقدار آن
const newSortDesc = options.sortDesc && Array.isArray(options.sortDesc) && options.sortDesc.length > 0
? options.sortDesc[0]
: true
console.log('Sorting changed:', { newSortBy, newSortDesc, currentSortBy: sortBy.value[0], currentSortDesc: sortDesc.value[0] })
if (newSortBy !== sortBy.value[0] || newSortDesc !== sortDesc.value[0]) {
sortBy.value = [newSortBy]
sortDesc.value = [newSortDesc]
shouldReload = true
}
}
if (shouldReload) {
loadLogs()
}
} catch (error) {
console.error('Error in handleTableUpdate:', error)
}
}
const viewLogDetail = async (log) => {
try {
const response = await axios.get(`/api/admin/debug/logs/${log.id}`)
if (response.data.success) {
selectedLog.value = response.data.data
showDetailDialog.value = true
} else {
showSnackbar('خطا در دریافت جزئیات لاگ', 'error')
}
} catch (error) {
console.error('Error loading log detail:', error)
showSnackbar('خطا در دریافت جزئیات لاگ', 'error')
}
}
const deleteSelectedLogs = async () => {
try {
deleting.value = true
const logIds = selectedLogs.value.map(log => log.id)
const response = await axios.delete('/api/admin/debug/logs', {
data: { ids: logIds }
})
if (response.data.success) {
showSnackbar(response.data.message, 'success')
selectedLogs.value = []
pagination.page = 1
loadLogs()
loadSystemInfo()
} else {
showSnackbar('خطا در حذف لاگ‌ها', 'error')
}
} catch (error) {
console.error('Error deleting logs:', error)
showSnackbar('خطا در حذف لاگ‌ها', 'error')
} finally {
deleting.value = false
showDeleteDialog.value = false
}
}
const deleteAllLogs = async () => {
try {
deleting.value = true
const response = await axios.delete('/api/admin/debug/logs', {
data: { deleteAll: true }
})
if (response.data.success) {
showSnackbar(response.data.message, 'success')
selectedLogs.value = []
pagination.page = 1
loadLogs()
loadSystemInfo()
} else {
showSnackbar('خطا در حذف لاگ‌ها', 'error')
}
} catch (error) {
console.error('Error deleting all logs:', error)
showSnackbar('خطا در حذف لاگ‌ها', 'error')
} finally {
deleting.value = false
showDeleteAllDialog.value = false
}
}
const showSnackbar = (message, color = 'success') => {
snackbar.message = message
snackbar.color = color
snackbar.show = true
}
onMounted(() => {
loadLogs()
loadSystemInfo()
})
return {
loading,
loadingSystemInfo,
deleting,
logs,
selectedLogs,
selectedLog,
showDetailDialog,
showDeleteDialog,
showDeleteAllDialog,
systemInfo,
environment,
filters,
pagination,
snackbar,
headers,
logLevels,
getLevelColor,
getEnvironmentColor,
formatBytes,
loadLogs,
loadSystemInfo,
handleTableUpdate,
viewLogDetail,
deleteSelectedLogs,
deleteAllLogs,
showSnackbar,
debouncedLoadLogs,
sortBy,
sortDesc
}
}
}
</script>
<style scoped>
.v-data-table {
border-radius: 8px;
}
.v-chip {
font-weight: 500;
}
pre {
background-color: #f5f5f5;
padding: 12px;
border-radius: 4px;
overflow-x: auto;
font-family: 'Courier New', monospace;
font-size: 12px;
}
</style>

View file

@ -113,6 +113,7 @@ export default defineComponent({
{ text: 'تاریخچه سیستم', url: '/profile/manager/logs/list', icon: 'mdi-history', visible: true },
{ text: 'کیف پول', url: '/profile/manager/wallet/list', icon: 'mdi-wallet', visible: true },
{ text: 'اطلاعیه‌ها', url: '/profile/manager/statments/list', icon: 'mdi-bell', visible: true },
{ text: 'دیباگ سیستم', url: '/profile/manager/debug', icon: 'mdi-bug', visible: true },
],
adminSettings: [
{ text: 'پیامک', url: '/profile/manager/system/sms/settings', icon: 'mdi-message-alert', visible: true },