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 #esi: true
#fragments: true #fragments: true
http_client:
default_options:
timeout: 30
php_errors: php_errors:
log: true log: true

View file

@ -40,6 +40,10 @@ services:
- '../src/Entity/' - '../src/Entity/'
- '../src/Kernel.php' - '../src/Kernel.php'
App\Controller\System\DebugController:
arguments:
$kernelLogsDir: '%kernel.logs_dir%'
doctrine.orm.default_attribute_driver: doctrine.orm.default_attribute_driver:
class: Doctrine\ORM\Mapping\Driver\AttributeDriver class: Doctrine\ORM\Mapping\Driver\AttributeDriver
arguments: arguments:
@ -122,7 +126,37 @@ services:
arguments: arguments:
$entityManager: '@doctrine.orm.entity_manager' $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: App\AiTool\AccountingDocService:
arguments: arguments:
$em: '@doctrine.orm.entity_manager' $em: '@doctrine.orm.entity_manager'
$cogAccountingDocService: '@App\Cog\AccountingDocService' $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; namespace App\AiTool;
use App\Cog\TicketService as CogTicketService; use App\Cog\TicketService as CogTicketService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
class TicketService class TicketService
{ {
public function __construct( private EntityManagerInterface $em;
private readonly CogTicketService $cogTicketService 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\Provider;
use App\Service\registryMGR; use App\Service\registryMGR;
use App\Service\SMS; use App\Service\SMS;
use App\AiTool\TicketService;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\BinaryFileResponse; 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\Log;
use App\Service\Provider; use App\Service\Provider;
use App\Service\AGI\Promps\PromptService; use App\Service\AGI\Promps\PromptService;
use App\Service\Explore;
use App\Service\Jdate;
use App\Service\SMS;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\HttpKernelInterface;
@ -23,6 +26,10 @@ class AGIService
private $promptService; private $promptService;
private $httpClient; private $httpClient;
private $httpKernel; private $httpKernel;
private $explore;
private $jdate;
private $sms;
private $uploadDirectory;
public function __construct( public function __construct(
EntityManagerInterface $entityManager, EntityManagerInterface $entityManager,
@ -31,7 +38,11 @@ class AGIService
Provider $provider, Provider $provider,
PromptService $promptService, PromptService $promptService,
HttpClientInterface $httpClient, HttpClientInterface $httpClient,
HttpKernelInterface $httpKernel HttpKernelInterface $httpKernel,
Explore $explore,
Jdate $jdate,
SMS $sms,
string $uploadDirectory
) { ) {
$this->em = $entityManager; $this->em = $entityManager;
$this->registryMGR = $registryMGR; $this->registryMGR = $registryMGR;
@ -40,6 +51,10 @@ class AGIService
$this->promptService = $promptService; $this->promptService = $promptService;
$this->httpClient = $httpClient; $this->httpClient = $httpClient;
$this->httpKernel = $httpKernel; $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); return $accountingDocService->searchRowsAi($params, $params['acc'] ?? null);
// ابزارهای مربوط به تیکت // ابزارهای مربوط به تیکت
case 'getTicketsList': 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); $ticketService = new \App\AiTool\TicketService($this->em, $cogTicketService);
return $ticketService->getTicketsListAi($params, $params['acc'] ?? null); return $ticketService->getTicketsListAi($params, $params['acc'] ?? null);
case 'getTicketInfo': 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); $ticketService = new \App\AiTool\TicketService($this->em, $cogTicketService);
return $ticketService->getTicketInfoByCode($params['code'] ?? null, $params['acc'] ?? null); return $ticketService->getTicketInfoByCode($params['code'] ?? null, $params['acc'] ?? null);
case 'addOrUpdateTicket': 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); $ticketService = new \App\AiTool\TicketService($this->em, $cogTicketService);
return $ticketService->addOrUpdateTicketAi($params, $params['acc'] ?? null, $params['code'] ?? 0); return $ticketService->addOrUpdateTicketAi($params, $params['acc'] ?? null, $params['code'] ?? 0);
case 'replyToTicket': 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); $ticketService = new \App\AiTool\TicketService($this->em, $cogTicketService);
return $ticketService->replyToTicketAi($params, $params['acc'] ?? null); return $ticketService->replyToTicketAi($params, $params['acc'] ?? null);
default: default:

View file

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

View file

@ -2,204 +2,292 @@
namespace App\Service\AGI\Promps; namespace App\Service\AGI\Promps;
use Doctrine\ORM\EntityManagerInterface;
class TicketService class TicketService
{ {
private $em;
public function __construct(EntityManagerInterface $entityManager)
{
$this->em = $entityManager;
}
/** /**
* دریافت ابزارهای مربوط به تیکت‌ها * دریافت تمام ابزارهای بخش تیکت‌ها برای function calling
* @return array * @return array
*/ */
public function getTools(): array public function getTools(): array
{ {
return [ $tools = [];
[
'name' => 'analyze_ticket', // ابزار getTicketsList
'description' => 'تحلیل و دسته‌بندی تیکت', $ticketsListPrompt = $this->getTicketsListPrompt();
'parameters' => [ $ticketsListData = json_decode($ticketsListPrompt, true);
'type' => 'object', if ($ticketsListData) {
'properties' => [ $tools[] = [
'ticket_body' => [ 'type' => 'function',
'type' => 'string', 'function' => [
'description' => 'متن تیکت' 'name' => $ticketsListData['tool'],
] 'description' => $ticketsListData['description'],
], 'parameters' => $ticketsListData['parameters']
'required' => ['ticket_body']
]
],
[
'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);
public function getTicketAnalysisPrompt(string $ticketBody): string if ($ticketInfoData) {
{ $tools[] = [
return <<<PROMPT 'type' => 'function',
لطفاً این تیکت پشتیبانی را بررسی و دسته‌بندی کنید: 'function' => [
'name' => $ticketInfoData['tool'],
'description' => $ticketInfoData['description'],
'parameters' => $ticketInfoData['parameters']
]
];
}
متن تیکت: // ابزار addOrUpdateTicket
{$ticketBody} $addOrUpdatePrompt = $this->getAddOrUpdateTicketPrompt();
$addOrUpdateData = json_decode($addOrUpdatePrompt, true);
if ($addOrUpdateData) {
$tools[] = [
'type' => 'function',
'function' => [
'name' => $addOrUpdateData['tool'],
'description' => $addOrUpdateData['description'],
'parameters' => $addOrUpdateData['parameters']
]
];
}
لطفاً موارد زیر را مشخص کنید: // ابزار replyToTicket
1. موضوع اصلی تیکت $replyPrompt = $this->getReplyToTicketPrompt();
2. اولویت (کم، متوسط، زیاد) $replyData = json_decode($replyPrompt, true);
3. بخش مربوطه (مالی، فنی، عمومی) if ($replyData) {
4. پیشنهاد برای پاسخ $tools[] = [
PROMPT; 'type' => 'function',
'function' => [
'name' => $replyData['tool'],
'description' => $replyData['description'],
'parameters' => $replyData['parameters']
]
];
}
return $tools;
} }
/** /**
* پرامپت برای تولید پیش‌نویس پاسخ به تیکت * تولید تمام پرامپ‌های بخش تیکت‌ها
* @return string
*/ */
public function getDraftResponsePrompt(string $ticketBody, string $ticketTitle, array $history = []): string public function getAllTicketPrompts(): string
{ {
$historyText = ''; $prompts = [];
if (!empty($history)) { $prompts[] = $this->getTicketsListPrompt();
$historyText = "تاریخچه مکالمات قبلی:\n"; $prompts[] = $this->getTicketInfoPrompt();
foreach ($history as $message) { $prompts[] = $this->getAddOrUpdateTicketPrompt();
$historyText .= sprintf( $prompts[] = $this->getReplyToTicketPrompt();
"- %s: %s\n", return implode("\n\n", $prompts);
$message['sender'],
$message['message']
);
}
}
return <<<PROMPT
لطفاً یک پیش‌نویس پاسخ مناسب برای این تیکت پشتیبانی آماده کنید:
عنوان تیکت: {$ticketTitle}
متن تیکت:
{$ticketBody}
{$historyText}
لطفاً یک پاسخ حرفه‌ای و دقیق با در نظر گرفتن نکات زیر آماده کنید:
1. لحن مؤدبانه و حرفه‌ای
2. پاسخگویی به تمام نکات مطرح شده در تیکت
3. ارائه راهکارهای عملی
4. درخواست اطلاعات تکمیلی در صورت نیاز
PROMPT;
} }
/** /**
* پرامپت برای پیشنهاد اقدامات بعدی برای تیکت * پرامپ برای دریافت لیست تیکت‌ها
*/ */
public function getNextActionPrompt(string $ticketBody, string $currentStatus, array $previousActions = []): string public function getTicketsListPrompt(): string
{ {
$previousActionsText = ''; return '{
if (!empty($previousActions)) { "tool": "getTicketsList",
$previousActionsText = "اقدامات قبلی:\n"; "description": "دریافت لیست تیکت‌های پشتیبانی با فیلتر و صفحه‌بندی",
foreach ($previousActions as $action) { "endpoint": "/api/ticket/list",
$previousActionsText .= "- {$action}\n"; "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
}
],
"total": 1,
"unfilteredTotal": 5
} }
} }
}';
return <<<PROMPT
لطفاً اقدامات بعدی مناسب برای این تیکت را پیشنهاد دهید:
متن تیکت:
{$ticketBody}
وضعیت فعلی: {$currentStatus}
{$previousActionsText}
لطفاً موارد زیر را مشخص کنید:
1. آیا نیاز به ارجاع به بخش دیگری هست؟
2. آیا نیاز به اطلاعات تکمیلی از کاربر هست؟
3. اولویت رسیدگی به این تیکت
4. پیشنهاد برای اقدام بعدی
PROMPT;
} }
/** /**
* پرامپت برای خلاصه‌سازی تیکت و تاریخچه آن * پرامپ برای دریافت اطلاعات تیکت
*/ */
public function getTicketSummaryPrompt(array $ticketHistory): string public function getTicketInfoPrompt(): string
{ {
$historyText = ''; return '{
foreach ($ticketHistory as $entry) { "tool": "getTicketInfo",
$historyText .= sprintf( "description": "دریافت اطلاعات کامل یک تیکت بر اساس کد",
"- %s (%s): %s\n", "endpoint": "/api/ticket/info/{code}",
$entry['date'], "method": "GET",
$entry['user'], "parameters": {
$entry['message'] "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"
} }
]
return <<<PROMPT },
لطفاً خلاصه‌ای از این تیکت و تاریخچه آن تهیه کنید: "examples": {
"input": {"code": "TKT001"},
تاریخچه تیکت: "output": {
{$historyText} "id": 1,
"code": "TKT001",
لطفاً موارد زیر را در خلاصه مشخص کنید: "title": "مشکل در ورود به سیستم",
1. موضوع اصلی و مشکل گزارش شده "body": "نمی‌توانم وارد سیستم شوم",
2. اقدامات انجام شده "status": "در حال پیگیری",
3. وضعیت فعلی "priority": "متوسط",
4. نکات مهم برای پیگیری "dateSubmit": 1703123456,
PROMPT; "submitter": {"id": 1, "name": "کاربر نمونه"},
"main": 0,
"fileName": null,
"replies": [
{
"id": 2,
"body": "لطفاً مرورگر خود را پاک کنید و دوباره تلاش کنید",
"dateSubmit": 1703124000,
"submitter": {"id": 2, "name": "پشتیبان"},
"fileName": null
}
]
}
}
}';
} }
/** /**
* پرامپت برای دسته‌بندی خودکار تیکت‌ها * پرامپ برای افزودن یا ویرایش تیکت
*/ */
public function getTicketCategorizationPrompt(array $tickets): string public function getAddOrUpdateTicketPrompt(): string
{ {
$ticketsText = ''; return '{
foreach ($tickets as $ticket) { "tool": "addOrUpdateTicket",
$ticketsText .= sprintf( "description": "برای ویرایش یک تیکت ابتدا باید با ابزار جست‌وجوی تیکت (getTicketsList) تیکت مورد نظر را پیدا کنید. اگر چند نتیجه یافت شد، باید از کاربر بپرسید کدام را می‌خواهد ویرایش کند و کد (code) آن را دریافت کنید. سپس با ارسال کد و اطلاعات جدید به این ابزار، ویرایش انجام می‌شود. اگر code برابر 0 یا ارسال نشود، تیکت جدید ایجاد خواهد شد. افزودن تیکت جدید یا ویرایش تیکت موجود",
"عنوان: %s\nمتن: %s\n\n", "endpoint": "/api/ticket/mod/{code}",
$ticket['title'], "method": "POST",
$ticket['body'] "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":"تیکت با موفقیت ایجاد شد"}
}
}';
} }
return <<<PROMPT /**
لطفاً این تیکت‌ها را بر اساس موضوع و محتوا دسته‌بندی کنید: * پرامپ برای پاسخ به تیکت
*/
تیکت‌ها: public function getReplyToTicketPrompt(): string
{$ticketsText} {
return '{
لطفاً برای هر تیکت موارد زیر را مشخص کنید: "tool": "replyToTicket",
1. دسته اصلی (مالی، فنی، پشتیبانی عمومی، آموزش) "description": "ارسال پاسخ به یک تیکت موجود",
2. زیر دسته "endpoint": "/api/ticket/reply",
3. برچسب‌های پیشنهادی "method": "POST",
4. اولویت پیشنهادی "parameters": {
PROMPT; "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 'login': true
} }
}, },
{
path: 'manager/debug',
component: () => import('../views/user/manager/debug/debug.vue'),
meta: {
'title': 'دیباگ سیستم',
'login': true
}
},
{ {
path: 'manager/changes/mod/:id', path: 'manager/changes/mod/:id',
component: () => import('../views/user/manager/reportchange/mod.vue'), 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/logs/list', icon: 'mdi-history', visible: true },
{ text: 'کیف پول', url: '/profile/manager/wallet/list', icon: 'mdi-wallet', 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/statments/list', icon: 'mdi-bell', visible: true },
{ text: 'دیباگ سیستم', url: '/profile/manager/debug', icon: 'mdi-bug', visible: true },
], ],
adminSettings: [ adminSettings: [
{ text: 'پیامک', url: '/profile/manager/system/sms/settings', icon: 'mdi-message-alert', visible: true }, { text: 'پیامک', url: '/profile/manager/system/sms/settings', icon: 'mdi-message-alert', visible: true },