Update Saqel Platform: 2026-08-28 18:08:03
This commit is contained in:
@@ -46,17 +46,58 @@ class CurriculumController
|
|||||||
];
|
];
|
||||||
file_put_contents($processingDir . '/' . $taskId . '.json', json_encode($taskState, JSON_UNESCAPED_UNICODE));
|
file_put_contents($processingDir . '/' . $taskId . '.json', json_encode($taskState, JSON_UNESCAPED_UNICODE));
|
||||||
|
|
||||||
// Launch Background Worker
|
// Prepare task paths
|
||||||
$scriptPath = realpath(__DIR__ . '/../../scripts/curriculum_worker.php');
|
$scriptPath = realpath(__DIR__ . '/../../scripts/curriculum_worker.php');
|
||||||
$logPath = $processingDir . '/' . $taskId . '.log';
|
$logPath = $processingDir . '/' . $taskId . '.log';
|
||||||
$cmd = "php " . escapeshellarg($scriptPath) . " " . escapeshellarg($taskId) . " > " . escapeshellarg($logPath) . " 2>&1 &";
|
|
||||||
exec($cmd);
|
|
||||||
|
|
||||||
|
// Send processing response immediately
|
||||||
$response->json([
|
$response->json([
|
||||||
'status' => 'processing',
|
'status' => 'processing',
|
||||||
'task_id' => $taskId,
|
'task_id' => $taskId,
|
||||||
'message' => 'تم استلام الكتاب، وجاري المعالجة المعمقة بواسطة الذكاء الاصطناعي. يرجى الانتظار...'
|
'message' => 'تم استلام الكتاب، وجاري المعالجة المعمقة بواسطة الذكاء الاصطناعي. يرجى الانتظار...'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Terminate request and flush buffers so the client receives the JSON immediately
|
||||||
|
if (function_exists('fastcgi_finish_request')) {
|
||||||
|
fastcgi_finish_request();
|
||||||
|
} else {
|
||||||
|
// Fallback for non-FPM
|
||||||
|
ob_end_flush();
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run worker natively in the same PHP process (in the background from client's perspective)
|
||||||
|
try {
|
||||||
|
// Capture all output to log file
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
// Execute the script logic natively by setting argv
|
||||||
|
$argv = ['curriculum_worker.php', $taskId];
|
||||||
|
$_SERVER['argv'] = $argv;
|
||||||
|
|
||||||
|
require $scriptPath;
|
||||||
|
|
||||||
|
$output = ob_get_clean();
|
||||||
|
file_put_contents($logPath, $output, FILE_APPEND);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$err = "[Exception] " . $e->getMessage() . " in " . $e->getFile() . ":" . $e->getLine();
|
||||||
|
file_put_contents($logPath, $err . "
|
||||||
|
", FILE_APPEND);
|
||||||
|
|
||||||
|
// Update state file to error
|
||||||
|
$stateFile = $processingDir . '/' . $taskId . '.json';
|
||||||
|
if (file_exists($stateFile)) {
|
||||||
|
$st = json_decode(file_get_contents($stateFile), true);
|
||||||
|
$st['status'] = 'error';
|
||||||
|
$st['message'] = 'تعطل النظام أثناء التحليل (Crash). يرجى مراجعة سجل الأخطاء.';
|
||||||
|
file_put_contents($stateFile, json_encode($st, JSON_UNESCAPED_UNICODE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure script stops here so it doesn't return anything else
|
||||||
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,9 +123,34 @@ class CurriculumController
|
|||||||
$response->json($state);
|
$response->json($state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Upload Log
|
||||||
|
*/
|
||||||
|
public function getUploadLog(Request $request, Response $response): void
|
||||||
|
{
|
||||||
|
$taskId = $request->getQueryParams()['task_id'] ?? '';
|
||||||
|
if (empty($taskId)) {
|
||||||
|
$response->status(400)->json(['status' => 'error', 'message' => 'رقم المهمة مفقود']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$processingDir = __DIR__ . '/../../storage/curriculum/processing';
|
||||||
|
$logFile = $processingDir . '/' . $taskId . '.log';
|
||||||
|
|
||||||
|
if (!file_exists($logFile)) {
|
||||||
|
$response->status(404)->json(['status' => 'error', 'message' => 'سجل الأخطاء غير موجود']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$logContent = file_get_contents($logFile);
|
||||||
|
$response->json(['status' => 'success', 'log' => $logContent]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Complete Live Tree
|
* Get Complete Live Tree
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public function getTree(Request $request, Response $response): void
|
public function getTree(Request $request, Response $response): void
|
||||||
{
|
{
|
||||||
$response->json([
|
$response->json([
|
||||||
|
|||||||
@@ -450,8 +450,43 @@ class CurriculumStudio
|
|||||||
</div>
|
</div>
|
||||||
<p id="upload_progress_text" style="color: var(--text-secondary); font-size: 13px; font-weight: bold;">جاري التهيئة...</p>
|
<p id="upload_progress_text" style="color: var(--text-secondary); font-size: 13px; font-weight: bold;">جاري التهيئة...</p>
|
||||||
<p id="upload_progress_percent" style="color: var(--accent-gold); font-size: 12px; margin-top: 5px;">0%</p>
|
<p id="upload_progress_percent" style="color: var(--accent-gold); font-size: 12px; margin-top: 5px;">0%</p>
|
||||||
|
|
||||||
|
<button id="btn_view_logs" type="button" style="margin-top: 15px; background: transparent; border: 1px solid var(--border-color); color: var(--text-muted); padding: 5px 12px; border-radius: 6px; font-size: 11px; cursor: pointer; transition: all 0.2s;" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='var(--text-muted)'" onclick="toggleUploadLogs()">🔍 عرض سجل السيرفر (Logs)</button>
|
||||||
|
<div id="upload_logs_container" style="display: none; margin-top: 15px; width: 100%; text-align: left;">
|
||||||
|
<textarea id="upload_logs_textarea" readonly style="width: 100%; height: 120px; background: #000; color: #0f0; font-family: monospace; font-size: 11px; border: 1px solid #333; border-radius: 4px; padding: 8px; resize: none; direction: ltr;"></textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentUploadTaskId = null;
|
||||||
|
|
||||||
|
function toggleUploadLogs() {
|
||||||
|
const container = document.getElementById('upload_logs_container');
|
||||||
|
if (container.style.display === 'none') {
|
||||||
|
container.style.display = 'block';
|
||||||
|
fetchUploadLogs();
|
||||||
|
} else {
|
||||||
|
container.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchUploadLogs() {
|
||||||
|
if (!currentUploadTaskId) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/curriculum/upload-log?task_id=${currentUploadTaskId}`);
|
||||||
|
const data = await res.json();
|
||||||
|
const textarea = document.getElementById('upload_logs_textarea');
|
||||||
|
if (data.status === 'success') {
|
||||||
|
textarea.value = data.log || 'No logs generated yet...';
|
||||||
|
textarea.scrollTop = textarea.scrollHeight;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% { transform: scale(1); opacity: 1; }
|
0% { transform: scale(1); opacity: 1; }
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ $router->get('/curriculum-studio', function ($request, $response) {
|
|||||||
|
|
||||||
// Real Ministry Curriculum PDF Ingestion & Live Tree API
|
// Real Ministry Curriculum PDF Ingestion & Live Tree API
|
||||||
$router->post('/api/curriculum/upload-pdf', [\App\Controllers\CurriculumController::class, 'uploadPdf']);
|
$router->post('/api/curriculum/upload-pdf', [\App\Controllers\CurriculumController::class, 'uploadPdf']);
|
||||||
|
$router->get('/api/curriculum/upload-status', [\App\Controllers\CurriculumController::class, 'getUploadStatus']);
|
||||||
|
$router->get('/api/curriculum/upload-log', [\App\Controllers\CurriculumController::class, 'getUploadLog']);
|
||||||
$router->get('/api/curriculum/tree', [\App\Controllers\CurriculumController::class, 'getTree']);
|
$router->get('/api/curriculum/tree', [\App\Controllers\CurriculumController::class, 'getTree']);
|
||||||
$router->get('/api/curriculum/lesson', [\App\Controllers\CurriculumController::class, 'getLessonContent']);
|
$router->get('/api/curriculum/lesson', [\App\Controllers\CurriculumController::class, 'getLessonContent']);
|
||||||
$router->post('/api/curriculum/save-lesson', [\App\Controllers\CurriculumController::class, 'saveLessonContent']);
|
$router->post('/api/curriculum/save-lesson', [\App\Controllers\CurriculumController::class, 'saveLessonContent']);
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
<?php
|
<?php
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
set_error_handler(function($errno, $errstr, $errfile, $errline) {
|
||||||
|
echo "[PHP ERROR] $errstr in $errfile on line $errline
|
||||||
|
";
|
||||||
|
});
|
||||||
|
|
||||||
|
set_exception_handler(function($e) {
|
||||||
|
echo "[PHP EXCEPTION] " . $e->getMessage() . " in " . $e->getFile() . " on line " . $e->getLine() . "
|
||||||
|
";
|
||||||
|
});
|
||||||
|
|
||||||
require_once __DIR__ . '/../vendor/autoload.php';
|
require_once __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
@@ -7,7 +20,7 @@ use App\Core\Env;
|
|||||||
|
|
||||||
$taskId = $argv[1] ?? '';
|
$taskId = $argv[1] ?? '';
|
||||||
if (empty($taskId)) {
|
if (empty($taskId)) {
|
||||||
die("Task ID required\n");
|
echo "Task ID required\n"; return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$processingDir = __DIR__ . '/../storage/curriculum/processing';
|
$processingDir = __DIR__ . '/../storage/curriculum/processing';
|
||||||
@@ -15,7 +28,7 @@ $stateFile = $processingDir . '/' . $taskId . '.json';
|
|||||||
$pdfFile = $processingDir . '/' . $taskId . '.pdf';
|
$pdfFile = $processingDir . '/' . $taskId . '.pdf';
|
||||||
|
|
||||||
if (!file_exists($stateFile) || !file_exists($pdfFile)) {
|
if (!file_exists($stateFile) || !file_exists($pdfFile)) {
|
||||||
die("Files missing\n");
|
echo "Files missing\n"; return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateState($stateFile, $status, $progress, $message, $extra = []) {
|
function updateState($stateFile, $status, $progress, $message, $extra = []) {
|
||||||
@@ -27,6 +40,11 @@ function updateState($stateFile, $status, $progress, $message, $extra = []) {
|
|||||||
$state = array_merge($state, $extra);
|
$state = array_merge($state, $extra);
|
||||||
}
|
}
|
||||||
file_put_contents($stateFile, json_encode($state, JSON_UNESCAPED_UNICODE));
|
file_put_contents($stateFile, json_encode($state, JSON_UNESCAPED_UNICODE));
|
||||||
|
|
||||||
|
// Log to file too
|
||||||
|
$logFile = str_replace('.json', '.log', $stateFile);
|
||||||
|
$timestamp = date('Y-m-d H:i:s');
|
||||||
|
file_put_contents($logFile, "[$timestamp] [$status] [$progress%] $message\n", FILE_APPEND);
|
||||||
}
|
}
|
||||||
|
|
||||||
$state = json_decode(file_get_contents($stateFile), true);
|
$state = json_decode(file_get_contents($stateFile), true);
|
||||||
@@ -38,7 +56,7 @@ $apiKeys = array_values(array_filter(array_map('trim', explode(',', $rawKeys))))
|
|||||||
|
|
||||||
if (empty($apiKeys)) {
|
if (empty($apiKeys)) {
|
||||||
updateState($stateFile, 'error', 0, '⚠️ لم يتم العثور على مفتاح الذكاء الاصطناعي (GEMINI_API_KEY) في ملف .env.');
|
updateState($stateFile, 'error', 0, '⚠️ لم يتم العثور على مفتاح الذكاء الاصطناعي (GEMINI_API_KEY) في ملف .env.');
|
||||||
exit;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Round Robin Logic
|
// Round Robin Logic
|
||||||
@@ -53,7 +71,7 @@ updateState($stateFile, 'uploading_to_ai', 15, "تم تفعيل Round Robin. ج
|
|||||||
|
|
||||||
if (empty($geminiKey)) {
|
if (empty($geminiKey)) {
|
||||||
updateState($stateFile, 'error', 0, '⚠️ لم يتم العثور على مفتاح الذكاء الاصطناعي (GEMINI_API_KEY) في ملف .env. النظام يحتاج إلى الـ AI لقراءة وتفريغ الكتب (بما فيها الكتب المصورة والرياضيات) بشكل حقيقي.');
|
updateState($stateFile, 'error', 0, '⚠️ لم يتم العثور على مفتاح الذكاء الاصطناعي (GEMINI_API_KEY) في ملف .env. النظام يحتاج إلى الـ AI لقراءة وتفريغ الكتب (بما فيها الكتب المصورة والرياضيات) بشكل حقيقي.');
|
||||||
exit;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateState($stateFile, 'uploading_to_ai', 20, 'جاري رفع الكتاب إلى محرك الذكاء الاصطناعي (Gemini Vision) لقراءته بالكامل...');
|
updateState($stateFile, 'uploading_to_ai', 20, 'جاري رفع الكتاب إلى محرك الذكاء الاصطناعي (Gemini Vision) لقراءته بالكامل...');
|
||||||
@@ -77,7 +95,7 @@ curl_close($ch);
|
|||||||
|
|
||||||
if ($uploadCode !== 200) {
|
if ($uploadCode !== 200) {
|
||||||
updateState($stateFile, 'error', 0, 'فشل رفع الملف إلى الذكاء الاصطناعي. كود الخطأ: ' . $uploadCode . ' التفاصيل: ' . $uploadRes);
|
updateState($stateFile, 'error', 0, 'فشل رفع الملف إلى الذكاء الاصطناعي. كود الخطأ: ' . $uploadCode . ' التفاصيل: ' . $uploadRes);
|
||||||
exit;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$uploadData = json_decode($uploadRes, true);
|
$uploadData = json_decode($uploadRes, true);
|
||||||
@@ -86,7 +104,7 @@ $fileNameGemini = $uploadData['file']['name'] ?? '';
|
|||||||
|
|
||||||
if (empty($fileUri)) {
|
if (empty($fileUri)) {
|
||||||
updateState($stateFile, 'error', 0, 'لم يتم الحصول على URI من الذكاء الاصطناعي.');
|
updateState($stateFile, 'error', 0, 'لم يتم الحصول على URI من الذكاء الاصطناعي.');
|
||||||
exit;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateState($stateFile, 'analyzing', 40, 'الكتاب الآن في عقل الذكاء الاصطناعي... جاري المعالجة البصرية واستخراج المعادلات والنصوص...');
|
updateState($stateFile, 'analyzing', 40, 'الكتاب الآن في عقل الذكاء الاصطناعي... جاري المعالجة البصرية واستخراج المعادلات والنصوص...');
|
||||||
@@ -109,7 +127,7 @@ for ($i = 0; $i < $maxRetries; $i++) {
|
|||||||
|
|
||||||
if (!$isReady) {
|
if (!$isReady) {
|
||||||
updateState($stateFile, 'error', 0, 'انتهى وقت الانتظار أثناء معالجة الذكاء الاصطناعي لملف الـ PDF.');
|
updateState($stateFile, 'error', 0, 'انتهى وقت الانتظار أثناء معالجة الذكاء الاصطناعي لملف الـ PDF.');
|
||||||
exit;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateState($stateFile, 'analyzing', 60, 'جاري تفريغ المحتوى وتشكيل دروس المارك داون والمصادر الملحقة...');
|
updateState($stateFile, 'analyzing', 60, 'جاري تفريغ المحتوى وتشكيل دروس المارك داون والمصادر الملحقة...');
|
||||||
@@ -186,7 +204,7 @@ curl_close($ch);
|
|||||||
|
|
||||||
if ($code !== 200 || empty($res)) {
|
if ($code !== 200 || empty($res)) {
|
||||||
updateState($stateFile, 'error', 0, 'فشل توليد المحتوى من الذكاء الاصطناعي. قد يكون الملف ضخماً جداً أو الـ API Key غير صالح.');
|
updateState($stateFile, 'error', 0, 'فشل توليد المحتوى من الذكاء الاصطناعي. قد يكون الملف ضخماً جداً أو الـ API Key غير صالح.');
|
||||||
exit;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$jsonResponse = json_decode($res, true);
|
$jsonResponse = json_decode($res, true);
|
||||||
@@ -195,7 +213,7 @@ $parsedStructure = json_decode($text, true);
|
|||||||
|
|
||||||
if (empty($parsedStructure) || empty($parsedStructure['units'])) {
|
if (empty($parsedStructure) || empty($parsedStructure['units'])) {
|
||||||
updateState($stateFile, 'error', 0, 'فشل الذكاء الاصطناعي في إرجاع بنية JSON صالحة.');
|
updateState($stateFile, 'error', 0, 'فشل الذكاء الاصطناعي في إرجاع بنية JSON صالحة.');
|
||||||
exit;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateState($stateFile, 'generating', 80, 'الذكاء الاصطناعي انتهى من القراءة! جاري بناء الملفات وحفظها على السيرفر...');
|
updateState($stateFile, 'generating', 80, 'الذكاء الاصطناعي انتهى من القراءة! جاري بناء الملفات وحفظها على السيرفر...');
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
<?php
|
||||||
|
echo PHP_BINARY;
|
||||||
Reference in New Issue
Block a user