40012-vm/server.js
2026-05-15 15:21:46 +00:00

398 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)} &nbsp; · &nbsp;
<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> &nbsp;·&nbsp; الجهد التقديري: <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، متوسط (47 خطوات) = 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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
app.listen(PORT, () => {
console.log(`✨ Smart System Analyst Agent running at http://localhost:${PORT}`);
if (!GEMINI_API_KEY) console.warn('⚠️ GEMINI_API_KEY غير مضبوط — ستعمل الواجهة في الوضع التجريبي.');
});