398 lines
20 KiB
JavaScript
398 lines
20 KiB
JavaScript
require('dotenv').config();
|
||
const express = require('express');
|
||
const path = require('path');
|
||
|
||
const app = express();
|
||
const PORT = process.env.PORT || 3000;
|
||
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
|
||
const GEMINI_MODEL = 'gemini-2.5-flash-lite';
|
||
const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${encodeURIComponent(GEMINI_API_KEY)}`;
|
||
|
||
app.use(express.json({ limit: '2mb' }));
|
||
app.use(express.static(path.join(__dirname, 'public')));
|
||
|
||
// In-memory session store: sessionId -> { title, history, functionPoints, useCases, scope, actors }
|
||
const sessions = new Map();
|
||
|
||
function newId() {
|
||
return 'sess_' + Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
|
||
}
|
||
|
||
const SYSTEM_PROMPT = `أنت "محلل نظم ذكي" خبير في هندسة البرمجيات. مهمتك إجراء مقابلة تفاعلية مع المستخدم باللغة العربية الفصحى لاستخلاص متطلبات نظام برمجي بشكل كامل.
|
||
|
||
التزم بالمراحل التالية بالترتيب:
|
||
1) التمهيد: افهم اسم المشروع، نطاقه العام، وأهم المستخدمين والأهداف.
|
||
2) الاستكشاف (تأخذ وتعطي): اطرح سؤالاً واحداً فقط في كل رد، استوضح التفاصيل، استخرج كل وظيفة (Function Point).
|
||
3) صياغة حالات الاستخدام: حدد الـ Actors والسيناريوهات الرئيسية والاستثنائية.
|
||
4) التقييم: قيّم تعقيد كل وظيفة (بسيطة/متوسطة/معقدة).
|
||
5) عند اكتمال المتطلبات، أعلم المستخدم بأنه يستطيع توليد التقرير.
|
||
|
||
قواعد صارمة:
|
||
- سؤال واحد فقط في كل رد، قصير وواضح.
|
||
- لا تكرر ما قاله المستخدم.
|
||
- بعد كل رد للمستخدم، حدّث القائمة الداخلية للوظائف وحالات الاستخدام.
|
||
|
||
يجب أن تُخرج ردك دائماً كـ JSON صالح (بدون أي نص خارج الـ JSON، بدون أسوار كود) بالشكل التالي بالضبط:
|
||
{
|
||
"reply": "نص ردك العربي للمستخدم (سؤال واحد أو ملخص قصير)",
|
||
"stage": "intro|exploration|usecase|evaluation|done",
|
||
"scope": "وصف موجز لنطاق المشروع (يتطور مع الحوار)",
|
||
"actors": ["قائمة الـ Actors المستخرجين"],
|
||
"functionPoints": [
|
||
{ "id": "FP-01", "name": "اسم الوظيفة", "description": "وصف موجز", "complexity": "بسيطة|متوسطة|معقدة", "fpScore": 3 }
|
||
],
|
||
"useCases": [
|
||
{ "id": "UC-01", "title": "عنوان حالة الاستخدام", "actor": "الفاعل", "preconditions": "...", "mainFlow": ["خطوة 1","خطوة 2"], "alternateFlow": ["..."] }
|
||
],
|
||
"isComplete": false
|
||
}
|
||
|
||
قيّم fpScore: بسيطة=3، متوسطة=4، معقدة=6. أعد دائماً القائمة الكاملة المحدّثة (لا الفروقات فقط).`;
|
||
|
||
async function callGemini(history, userMessage) {
|
||
const contents = [
|
||
{ role: 'user', parts: [{ text: SYSTEM_PROMPT }] },
|
||
{ role: 'model', parts: [{ text: '{"reply":"تمام، أنا جاهز.","stage":"intro","scope":"","actors":[],"functionPoints":[],"useCases":[],"isComplete":false}' }] },
|
||
...history.flatMap(m => ([{
|
||
role: m.role === 'assistant' ? 'model' : 'user',
|
||
parts: [{ text: m.role === 'assistant' ? JSON.stringify(m.payload) : m.text }]
|
||
}])),
|
||
{ role: 'user', parts: [{ text: userMessage }] }
|
||
];
|
||
|
||
const res = await fetch(GEMINI_URL, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
contents,
|
||
generationConfig: { temperature: 0.7, responseMimeType: 'application/json' }
|
||
})
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const errTxt = await res.text();
|
||
throw new Error(`Gemini ${res.status}: ${errTxt.slice(0, 200)}`);
|
||
}
|
||
|
||
const data = await res.json();
|
||
const text = data?.candidates?.[0]?.content?.parts?.[0]?.text || '{}';
|
||
let parsed;
|
||
try { parsed = JSON.parse(text); }
|
||
catch { parsed = { reply: text, stage: 'exploration', scope: '', actors: [], functionPoints: [], useCases: [], isComplete: false }; }
|
||
return parsed;
|
||
}
|
||
|
||
app.post('/api/start', async (req, res) => {
|
||
try {
|
||
const { title } = req.body || {};
|
||
if (!title || !title.trim()) return res.status(400).json({ error: 'يرجى إدخال عنوان المشروع' });
|
||
const sessionId = newId();
|
||
const session = {
|
||
id: sessionId,
|
||
title: title.trim(),
|
||
history: [],
|
||
scope: '',
|
||
actors: [],
|
||
functionPoints: [],
|
||
useCases: [],
|
||
isComplete: false,
|
||
createdAt: new Date().toISOString()
|
||
};
|
||
sessions.set(sessionId, session);
|
||
|
||
const opener = `بدأنا مشروعاً جديداً بعنوان: "${title}". رجاءً عرّفني بالمجال العام للمشروع وأهم المستخدمين المستهدفين منه.`;
|
||
const payload = {
|
||
reply: opener,
|
||
stage: 'intro',
|
||
scope: '',
|
||
actors: [],
|
||
functionPoints: [],
|
||
useCases: [],
|
||
isComplete: false
|
||
};
|
||
session.history.push({ role: 'assistant', payload });
|
||
res.json({ sessionId, ...payload });
|
||
} catch (e) {
|
||
console.error(e);
|
||
res.status(500).json({ error: 'فشل بدء الجلسة' });
|
||
}
|
||
});
|
||
|
||
app.post('/api/message', async (req, res) => {
|
||
try {
|
||
const { sessionId, message } = req.body || {};
|
||
const session = sessions.get(sessionId);
|
||
if (!session) return res.status(404).json({ error: 'الجلسة غير موجودة' });
|
||
if (!message || !message.trim()) return res.status(400).json({ error: 'الرسالة فارغة' });
|
||
|
||
session.history.push({ role: 'user', text: message.trim() });
|
||
|
||
if (!GEMINI_API_KEY) {
|
||
const fake = {
|
||
reply: '(وضع تجريبي) يُرجى إضافة GEMINI_API_KEY في ملف .env لتفعيل الذكاء الاصطناعي.',
|
||
stage: 'exploration', scope: session.scope, actors: session.actors,
|
||
functionPoints: session.functionPoints, useCases: session.useCases, isComplete: false
|
||
};
|
||
session.history.push({ role: 'assistant', payload: fake });
|
||
return res.json(fake);
|
||
}
|
||
|
||
const payload = await callGemini(session.history.slice(0, -1), message.trim());
|
||
// update session state
|
||
session.scope = payload.scope || session.scope;
|
||
session.actors = payload.actors || session.actors;
|
||
session.functionPoints = payload.functionPoints || session.functionPoints;
|
||
session.useCases = payload.useCases || session.useCases;
|
||
session.isComplete = !!payload.isComplete;
|
||
session.history.push({ role: 'assistant', payload });
|
||
|
||
res.json(payload);
|
||
} catch (e) {
|
||
console.error('message error:', e.message);
|
||
res.status(500).json({ error: e.message || 'خطأ في المعالجة' });
|
||
}
|
||
});
|
||
|
||
app.get('/api/session/:id', (req, res) => {
|
||
const s = sessions.get(req.params.id);
|
||
if (!s) return res.status(404).json({ error: 'not found' });
|
||
res.json(s);
|
||
});
|
||
|
||
// ----- Report HTML builder -----
|
||
function buildReportHtml(s, { forPdf = false } = {}) {
|
||
const totalFP = (s.functionPoints || []).reduce((sum, f) => sum + (Number(f.fpScore) || 0), 0);
|
||
const ucp = computeUCP(s);
|
||
return `<!DOCTYPE html>
|
||
<html lang="ar" dir="rtl">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>تقرير متطلبات: ${escapeHtml(s.title)}</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;600;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
* { box-sizing: border-box; }
|
||
body { font-family: 'Cairo', sans-serif; margin: 0; padding: 40px; color: #1a1a2e; background: #fff; line-height: 1.8; }
|
||
h1 { color: #0f0f1e; border-bottom: 3px solid #c9a84c; padding-bottom: 12px; font-size: 28px; }
|
||
h2 { color: #2d3748; margin-top: 32px; border-right: 4px solid #c9a84c; padding-right: 12px; font-size: 20px; }
|
||
.meta { color: #666; font-size: 14px; margin-bottom: 24px; }
|
||
table { width: 100%; border-collapse: collapse; margin: 16px 0; }
|
||
th, td { border: 1px solid #ddd; padding: 10px; text-align: right; vertical-align: top; }
|
||
th { background: #f5f0e0; color: #1a1a2e; font-weight: 700; }
|
||
.uc { background: #fafbfc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 16px; margin: 12px 0; }
|
||
.uc-title { font-weight: 700; color: #0f0f1e; margin-bottom: 8px; }
|
||
.uc-row { margin: 4px 0; }
|
||
.uc-label { font-weight: 600; color: #4a5568; }
|
||
ol, ul { padding-right: 22px; }
|
||
.summary { background: linear-gradient(135deg, #0f0f1e, #1a1a3e); color: #fff; padding: 20px; border-radius: 8px; margin: 16px 0; }
|
||
.summary strong { color: #c9a84c; }
|
||
.dl-btn { position: fixed; top: 20px; left: 20px; background: #c9a84c; color: #1a1a2e; border: none; padding: 12px 24px; border-radius: 8px; font-family: inherit; font-weight: 700; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.15); text-decoration: none; display: inline-block; }
|
||
@media print { .dl-btn { display: none; } body { padding: 20px; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
${forPdf ? '' : `<button class="dl-btn" onclick="window.print()">⬇ حفظ كـ PDF</button>`}
|
||
<h1>وثيقة متطلبات النظام</h1>
|
||
<div class="meta">
|
||
<strong>المشروع:</strong> ${escapeHtml(s.title)} ·
|
||
<strong>التاريخ:</strong> ${new Date(s.createdAt).toLocaleDateString('ar-EG')}
|
||
</div>
|
||
|
||
<h2>1. مقدمة ونطاق النظام</h2>
|
||
<p>${escapeHtml(s.scope || 'لم يُحدَّد بعد.')}</p>
|
||
|
||
<h2>2. المستخدمون (Actors)</h2>
|
||
<ul>${(s.actors || []).map(a => `<li>${escapeHtml(a)}</li>`).join('') || '<li>—</li>'}</ul>
|
||
|
||
<h2>3. المتطلبات الوظيفية (Function Points)</h2>
|
||
<table>
|
||
<thead><tr><th>الرمز</th><th>الوظيفة</th><th>الوصف</th><th>التعقيد</th><th>FP</th></tr></thead>
|
||
<tbody>
|
||
${(s.functionPoints || []).map(f => `<tr>
|
||
<td>${escapeHtml(f.id || '')}</td>
|
||
<td>${escapeHtml(f.name || '')}</td>
|
||
<td>${escapeHtml(f.description || '')}</td>
|
||
<td>${escapeHtml(f.complexity || '')}</td>
|
||
<td>${escapeHtml(String(f.fpScore || ''))}</td>
|
||
</tr>`).join('') || '<tr><td colspan="5">—</td></tr>'}
|
||
</tbody>
|
||
</table>
|
||
|
||
<h2>4. توصيف حالات الاستخدام (Use Cases)</h2>
|
||
${(s.useCases || []).map(uc => `
|
||
<div class="uc">
|
||
<div class="uc-title">${escapeHtml(uc.id || '')} — ${escapeHtml(uc.title || '')}</div>
|
||
<div class="uc-row"><span class="uc-label">الفاعل:</span> ${escapeHtml(uc.actor || '')}</div>
|
||
<div class="uc-row"><span class="uc-label">الشروط المسبقة:</span> ${escapeHtml(uc.preconditions || '')}</div>
|
||
<div class="uc-row"><span class="uc-label">السيناريو الرئيسي:</span>
|
||
<ol>${(uc.mainFlow || []).map(s => `<li>${escapeHtml(s)}</li>`).join('')}</ol>
|
||
</div>
|
||
<div class="uc-row"><span class="uc-label">السيناريو البديل:</span>
|
||
<ul>${(uc.alternateFlow || []).map(s => `<li>${escapeHtml(s)}</li>`).join('') || '<li>—</li>'}</ul>
|
||
</div>
|
||
</div>
|
||
`).join('') || '<p>—</p>'}
|
||
|
||
<h2>5. التقدير الأولي للحجم</h2>
|
||
<div class="summary">
|
||
<div>عدد الوظائف: <strong>${(s.functionPoints || []).length}</strong></div>
|
||
<div>عدد حالات الاستخدام: <strong>${(s.useCases || []).length}</strong></div>
|
||
<div>إجمالي نقاط الوظائف (Total FP): <strong>${totalFP}</strong></div>
|
||
<div>نقاط حالات الاستخدام (UCP): <strong>${ucp.UCP}</strong> · الجهد التقديري: <strong>${ucp.effortHours} ساعة</strong> (~${ucp.effortDays} يوم عمل)</div>
|
||
</div>
|
||
|
||
{/* Function Points breakdown by complexity */}
|
||
${(() => {
|
||
const fps = s.functionPoints || [];
|
||
const simple = fps.filter(f => f.complexity === 'بسيطة').length;
|
||
const medium = fps.filter(f => f.complexity === 'متوسطة').length;
|
||
const complex = fps.filter(f => f.complexity === 'معقدة').length;
|
||
return `
|
||
<h3>تفصيل نقاط الوظائف حسب التعقيد:</h3>
|
||
<table style="width: 100%; margin: 16px 0; border-collapse: collapse;">
|
||
<thead>
|
||
<tr>
|
||
<th style="border: 1px solid #ddd; padding: 8px; text-align: right; background: #f5f0e0;">التعقيد</th>
|
||
<th style="border: 1px solid #ddd; padding: 8px; text-align: right; background: #f5f0e0;">العدد</th>
|
||
<th style="border: 1px solid #ddd; padding: 8px; text-align: right; background: #f5f0e0;">FP لكل وظيفة</th>
|
||
<th style="border: 1px solid #ddd; padding: 8px; text-align: right; background: #f5f0e0;">المجموع</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">بسيطة</td>
|
||
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">${simple}</td>
|
||
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">3</td>
|
||
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">${simple * 3}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">متوسطة</td>
|
||
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">${medium}</td>
|
||
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">4</td>
|
||
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">${medium * 4}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">معقدة</td>
|
||
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">${complex}</td>
|
||
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">6</td>
|
||
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;">${complex * 6}</td>
|
||
</tr>
|
||
<tr style="border-top: 2px solid #ddd;">
|
||
<td style="border: 1px solid #ddd; padding: 8px; text-align: right; font-weight: bold;">الإجمالي</td>
|
||
<td style="border: 1px solid #ddd; padding: 8px; text-align: right; font-weight: bold;">${fps.length}</td>
|
||
<td style="border: 1px solid #ddd; padding: 8px; text-align: right;"></td>
|
||
<td style="border: 1px solid #ddd; padding: 8px; text-align: right; font-weight: bold;">${totalFP}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
})()}
|
||
|
||
<h2>6. حساب نقاط حالات الاستخدام (Use Case Points)</h2>
|
||
<p style="color:#4a5568">يعتمد الحساب على منهجية Karner. تُصنَّف الـ Actors والـ Use Cases آلياً ثم تُطبَّق المعاملات الفنية والبيئية.</p>
|
||
|
||
<h3 style="color:#2d3748;margin-top:20px">6.1 وزن الـ Actors (UAW)</h3>
|
||
<table>
|
||
<thead><tr><th>الفاعل</th><th>التصنيف</th><th>المبرر</th><th>الوزن</th></tr></thead>
|
||
<tbody>
|
||
${ucp.actorRows.map(a => `<tr>
|
||
<td>${escapeHtml(a.name)}</td>
|
||
<td>${escapeHtml(a.type)}</td>
|
||
<td>${escapeHtml(a.reason)}</td>
|
||
<td>${a.weight}</td>
|
||
</tr>`).join('') || '<tr><td colspan="4">—</td></tr>'}
|
||
</tbody>
|
||
<tfoot><tr><th colspan="3" style="text-align:left">إجمالي UAW</th><th>${ucp.UAW}</th></tr></tfoot>
|
||
</table>
|
||
<p><em>القاعدة:</em> بسيط (واجهة برمجية API) = 1، متوسط (بروتوكول/سطر أوامر) = 2، معقد (مستخدم بشري عبر واجهة رسومية) = 3.</p>
|
||
|
||
<h3 style="color:#2d3748;margin-top:20px">6.2 وزن الـ Use Cases (UUCW)</h3>
|
||
<table>
|
||
<thead><tr><th>الرمز</th><th>عدد العمليات (Transactions)</th><th>التصنيف</th><th>الوزن</th></tr></thead>
|
||
<tbody>
|
||
${ucp.ucRows.map(u => `<tr>
|
||
<td>${escapeHtml(u.id)} — ${escapeHtml(u.title)}</td>
|
||
<td>${u.transactions}</td>
|
||
<td>${escapeHtml(u.type)}</td>
|
||
<td>${u.weight}</td>
|
||
</tr>`).join('') || '<tr><td colspan="4">—</td></tr>'}
|
||
</tbody>
|
||
<tfoot><tr><th colspan="3" style="text-align:left">إجمالي UUCW</th><th>${ucp.UUCW}</th></tr></tfoot>
|
||
</table>
|
||
<p><em>القاعدة:</em> بسيط (≤ 3 خطوات) = 5، متوسط (4–7 خطوات) = 10، معقد (> 7 خطوات) = 15.</p>
|
||
|
||
<h3 style="color:#2d3748;margin-top:20px">6.3 المعامل الفني (TCF)</h3>
|
||
<p>TCF = 0.6 + (0.01 × TFactor) ، حيث TFactor = مجموع 13 عاملاً فنياً (افتراضي = ${ucp.TFactor}).</p>
|
||
<p><strong>TCF = 0.6 + (0.01 × ${ucp.TFactor}) = ${ucp.TCF}</strong></p>
|
||
|
||
<h3 style="color:#2d3748;margin-top:20px">6.4 المعامل البيئي (ECF)</h3>
|
||
<p>ECF = 1.4 + (−0.03 × EFactor) ، حيث EFactor = مجموع 8 عوامل بيئية (افتراضي = ${ucp.EFactor}).</p>
|
||
<p><strong>ECF = 1.4 + (−0.03 × ${ucp.EFactor}) = ${ucp.ECF}</strong></p>
|
||
|
||
<h3 style="color:#2d3748;margin-top:20px">6.5 الحساب النهائي</h3>
|
||
<div class="summary">
|
||
<div>UUCP = UAW + UUCW = ${ucp.UAW} + ${ucp.UUCW} = <strong>${ucp.UUCP}</strong></div>
|
||
<div>UCP = UUCP × TCF × ECF = ${ucp.UUCP} × ${ucp.TCF} × ${ucp.ECF} = <strong>${ucp.UCP}</strong></div>
|
||
<div>الجهد التقديري = UCP × 20 ساعة/نقطة = <strong>${ucp.effortHours} ساعة عمل</strong> (~${ucp.effortDays} يوم بمعدل 8 ساعات).</div>
|
||
</div>
|
||
</body>
|
||
</html>`;
|
||
}
|
||
|
||
// Use Case Points (Karner) calculator
|
||
function computeUCP(s) {
|
||
// --- Actors classification ---
|
||
const actorRows = (s.actors || []).map(name => {
|
||
const n = String(name || '').toLowerCase();
|
||
let type, weight, reason;
|
||
if (/api|نظام خارجي|external|service|بروتوكول/.test(n)) {
|
||
type = 'بسيط'; weight = 1; reason = 'واجهة برمجية / نظام خارجي';
|
||
} else if (/cli|سطر أوامر|tcp|protocol|قاعدة بيانات|database/.test(n)) {
|
||
type = 'متوسط'; weight = 2; reason = 'تفاعل عبر بروتوكول أو سطر أوامر';
|
||
} else {
|
||
type = 'معقد'; weight = 3; reason = 'مستخدم بشري عبر واجهة رسومية';
|
||
}
|
||
return { name, type, weight, reason };
|
||
});
|
||
const UAW = actorRows.reduce((a, b) => a + b.weight, 0);
|
||
|
||
// --- Use cases classification by # of transactions (mainFlow steps) ---
|
||
const ucRows = (s.useCases || []).map(uc => {
|
||
const transactions = Array.isArray(uc.mainFlow) ? uc.mainFlow.length : 0;
|
||
let type, weight;
|
||
if (transactions <= 3) { type = 'بسيط'; weight = 5; }
|
||
else if (transactions <= 7) { type = 'متوسط'; weight = 10; }
|
||
else { type = 'معقد'; weight = 15; }
|
||
return { id: uc.id || '', title: uc.title || '', transactions, type, weight };
|
||
});
|
||
const UUCW = ucRows.reduce((a, b) => a + b.weight, 0);
|
||
|
||
const UUCP = UAW + UUCW;
|
||
const TFactor = 30; // معدل افتراضي للعوامل الفنية
|
||
const EFactor = 17; // معدل افتراضي للعوامل البيئية
|
||
const TCF = +(0.6 + 0.01 * TFactor).toFixed(2);
|
||
const ECF = +(1.4 + -0.03 * EFactor).toFixed(2);
|
||
const UCP = +(UUCP * TCF * ECF).toFixed(2);
|
||
const effortHours = +(UCP * 20).toFixed(1);
|
||
const effortDays = +(effortHours / 8).toFixed(1);
|
||
|
||
return { actorRows, ucRows, UAW, UUCW, UUCP, TFactor, EFactor, TCF, ECF, UCP, effortHours, effortDays };
|
||
}
|
||
|
||
// HTML preview
|
||
app.get('/report/:id', (req, res) => {
|
||
const s = sessions.get(req.params.id);
|
||
if (!s) return res.status(404).send('الجلسة غير موجودة');
|
||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||
res.send(buildReportHtml(s, { forPdf: false }));
|
||
});
|
||
|
||
// PDF: عبر طباعة المتصفح (window.print) من صفحة التقرير — لا حاجة لمكتبات ثقيلة.
|
||
|
||
function escapeHtml(s) {
|
||
return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||
}
|
||
|
||
app.listen(PORT, () => {
|
||
console.log(`✨ Smart System Analyst Agent running at http://localhost:${PORT}`);
|
||
if (!GEMINI_API_KEY) console.warn('⚠️ GEMINI_API_KEY غير مضبوط — ستعمل الواجهة في الوضع التجريبي.');
|
||
}); |