نسخة كاملة من مستودع سيرو عند ecfe7568 لتكون أساس تطبيق «انطلق». نُسخ المتعقَّب في git فقط (12,509 ملفاً / 302 م.ب) بـ git archive، لا `cp -r` — فاستُثنيت تلقائياً مخلفات البناء (build · node_modules · .dart_tool · .gradle · Pods ≈ 10.7 غ.ب) وكل ما يستثنيه .gitignore. هذا الكوميت **بلا أي تعديل عمداً** حتى يكون كل ما يليه فرقاً مقروءاً مقابل سيرو الأصلي. سيرو نفسه لم يُمسّ. ⚠️ لا يبني بعد: `.env` و`lib/env/env.g.dart` غير متعقَّبين في سيرو (وهذا صحيح — أسرار لكل مستأجر). كل تطبيق فلاتر هنا يحتاج .env خاصاً بانطلق ثم توليد env.g.dart عبر build_runner. لا تُنسخ أسرار سيرو. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
380 lines
18 KiB
PHP
380 lines
18 KiB
PHP
<?php
|
|
// ============================================================
|
|
// core/Services/SiroGeminiService.php
|
|
// Siro AI Market Analysis & Marketing Content Generation Service
|
|
// ============================================================
|
|
|
|
declare(strict_types=1);
|
|
|
|
class SiroGeminiService {
|
|
private ?string $apiKey;
|
|
private string $baseUrl;
|
|
|
|
public function __construct() {
|
|
$this->apiKey = getenv('GEMINI_API_KEY') ?: null;
|
|
$this->baseUrl = "https://generativelanguage.googleapis.com/v1beta/models/";
|
|
}
|
|
|
|
/**
|
|
* Analyze market prices and generate target promotion response
|
|
*
|
|
* @param array $competitorPrices Array of competitor pricing information
|
|
* @param float $siroBasePrice Current Siro base pricing for this region/country
|
|
* @param string $regionName Name of the region/city
|
|
* @param string $countryCode Country code (e.g. SY, JO, EG, IQ)
|
|
* @param string $model Override AI model to use (default: gemini-1.5-flash)
|
|
* @return array|null Decoded JSON response from Gemini or null on failure
|
|
*/
|
|
public function analyzeMarketAndDraftCampaign(
|
|
array $competitorPrices,
|
|
float $siroBasePrice,
|
|
string $regionName,
|
|
string $countryCode,
|
|
string $model = 'gemini-flash-lite-latest'
|
|
): ?array {
|
|
if (!$this->apiKey) {
|
|
error_log("[SiroGeminiService] API Key is missing.");
|
|
return null;
|
|
}
|
|
|
|
$dialect = match (strtoupper($countryCode)) {
|
|
'SY' => 'السورية (الشامية)',
|
|
'JO' => 'الأردنية',
|
|
'EG' => 'المصرية',
|
|
'IQ' => 'العراقية',
|
|
default => 'العربية الفصحى البسيطة'
|
|
};
|
|
|
|
$prompt = "
|
|
أنت خبير تسويق ذكي ومحلل أسعار لتطبيق Siro لخدمات نقل الركاب.
|
|
قم بتحليل أسعار المنافسين في منطقة '$regionName' وصياغة حملة تسويقية وعرض ترويجي منافس.
|
|
|
|
بيانات الإدخال:
|
|
1. أسعار المنافسين الحالية: " . json_encode($competitorPrices, JSON_UNESCAPED_UNICODE) . "
|
|
2. سعر رحلة Siro الأساسي الحالي: $siroBasePrice
|
|
|
|
المطلوب:
|
|
1. دراسة الأسعار وتحديد هل توجد فرصة تسويقية واضحة لجذب الركاب (opportunity_detected: true/false).
|
|
2. تحديد معامل التخفيض المقترح أو قيمة خصم مناسبة (discount_value) والنسبة المئوية (discount_percentage).
|
|
3. كتابة رسالة تسويقية إعلانية جذابة وقصيرة جداً ومقنعة لإرسالها كإشعار (Push Notification) للركاب النشطين بالهجة $dialect.
|
|
4. كتابة رسالة استعادة جذابة ومغرية وقصيرة لإرسالها عبر SMS أو WhatsApp للركاب المنقطعين بالهجة $dialect مع ذكر كود الخصم المقترح.
|
|
5. اقتراح كود خصم مناسب للحملة (promo_code) ليكون سهل الحفظ ومناسباً للحدث.
|
|
|
|
الخرج المطلوب (يجب أن يكون JSON صالحاً تماماً وخالياً من أي شرح خارجي، باللغة العربية):
|
|
{
|
|
\"opportunity_detected\": true/false,
|
|
\"recommended_price\": 12000,
|
|
\"discount_percentage\": 15,
|
|
\"promo_code\": \"SIROGO15\",
|
|
\"push_title\": \"عنوان الإشعار (بحد أقصى 5 كلمات)\",
|
|
\"push_body\": \"محتوى الإشعار القصير والمثير للاهتمام (بحد أقصى 15 كلمة)\",
|
|
\"sms_body\": \"محتوى رسالة الاستعادة (بحد أقصى 20 كلمة)\"
|
|
}
|
|
";
|
|
|
|
return $this->callGemini($prompt, $model);
|
|
}
|
|
|
|
/**
|
|
* يُولّد إشعاراً مخصصاً لحدث دخول الجيوفينس بناءً على بيانات أسعار حقيقية.
|
|
*
|
|
* @param string $zoneName اسم منطقة السياج الجغرافي
|
|
* @param string $countryCode رمز الدولة (SY, JO, EG, IQ)
|
|
* @param float $savingsPct نسبة التوفير (مثال: 8.5)
|
|
* @param string $topCompetitor اسم أبرز منافس في المنطقة
|
|
* @param string $model
|
|
* @return array|null
|
|
*/
|
|
public function generateGeofenceMessage(
|
|
string $zoneName,
|
|
string $countryCode,
|
|
float $savingsPct,
|
|
string $topCompetitor = 'كريم',
|
|
string $model = 'gemini-flash-lite-latest'
|
|
): ?array {
|
|
if (!$this->apiKey) return null;
|
|
|
|
$dialect = match (strtoupper($countryCode)) {
|
|
'SY' => 'السورية الشامية',
|
|
'JO' => 'الأردنية',
|
|
'EG' => 'المصرية العامية',
|
|
'IQ' => 'العراقية',
|
|
default => 'العربية الفصحى'
|
|
};
|
|
|
|
$savingsFormatted = number_format($savingsPct, 1);
|
|
|
|
$prompt = "
|
|
أنت كاتب إشعارات تسويقية ذكية لتطبيق Siro لخدمات نقل الركاب.
|
|
المستخدم الآن موجود بالقرب من '$zoneName'.
|
|
سعر سيرو أقل بـ $savingsFormatted% من $topCompetitor وبقية التطبيقات في هذه المنطقة.
|
|
|
|
المطلوب: اكتب إشعاراً push قصيراً جداً (عنوان + جسم) باللهجة $dialect.
|
|
- العنوان: لا يتجاوز 5 كلمات، مثير للاهتمام
|
|
- الجسم: لا يتجاوز 12 كلمة، يذكر التوفير الفعلي ويحفّز على الطلب الآن
|
|
- اجعله طبيعياً كأنه يكتبه شخص حقيقي وليس روبوت
|
|
|
|
الخرج (JSON فقط، بدون أي شرح):
|
|
{
|
|
\"push_title\": \"...\",
|
|
\"push_body\": \"...\"
|
|
}
|
|
";
|
|
|
|
return $this->callGemini($prompt, $model);
|
|
}
|
|
|
|
/**
|
|
* تقرأ معادلات المنافسين المكتشفة أسبوعياً وتقدم استراتيجية تسويق (Weekly Advisor).
|
|
*
|
|
* @param array $formulas المصفوفة المستخرجة من competitor_secret_formulas
|
|
* @param string $model
|
|
* @return array|null
|
|
*/
|
|
public function analyzeCompetitorFormulas(
|
|
array $formulas,
|
|
string $model = 'gemini-flash-lite-latest'
|
|
): ?array {
|
|
if (!$this->apiKey) return null;
|
|
|
|
$formulasJson = json_encode($formulas, JSON_UNESCAPED_UNICODE);
|
|
|
|
$prompt = "
|
|
أنت المستشار التسويقي المالي لتطبيق 'سيرو' لنقل الركاب.
|
|
لقد قمنا بعمل هندسة عكسية لرحلات منافسينا واكتشفنا معادلات التسعير السرية الخاصة بهم لهذا الأسبوع.
|
|
|
|
البيانات المكتشفة:
|
|
$formulasJson
|
|
|
|
المطلوب منك (كمستشار أعمال خبير):
|
|
1. تحليل هذه الأرقام، وتوضيح أين تكمن نقاط قوة المنافسين وأين نقاط ضعفهم في التسعير (مثلاً من يركز على المسافات القصيرة برفع فتح العداد؟).
|
|
2. اقتراح 3 استراتيجيات تسويقية أو رسائل إعلانية (Push Notifications/Social Media) موجهة للركاب، تستغل نقاط ضعف المنافسين المكتشفة.
|
|
|
|
قم بصياغة تقريرك بتنسيق HTML مرتب وجاهز للعرض في لوحة الإدارة (بدون علامات ```html)، واستخدم ألوان خفيفة في العناوين <h3>.
|
|
تحدث بلغة عربية احترافية ومباشرة لصناع القرار.
|
|
";
|
|
|
|
// بما أن الإخراج سيكون نص HTML، سنستخدم API جيميناي العادي بدون تقييد JSON
|
|
$url = $this->baseUrl . "{$model}:generateContent?key={$this->apiKey}";
|
|
|
|
$postData = [
|
|
'contents' => [
|
|
[
|
|
'parts' => [
|
|
['text' => $prompt]
|
|
]
|
|
]
|
|
],
|
|
'generationConfig' => [
|
|
'temperature' => 0.7,
|
|
'maxOutputTokens' => 2000
|
|
]
|
|
];
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData));
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
|
|
|
|
$response = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($httpCode === 200) {
|
|
$data = json_decode($response, true);
|
|
$text = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
|
return ['status' => 'success', 'html_report' => $text];
|
|
} else {
|
|
error_log("[SiroGeminiService] HTTP $httpCode in analyzeCompetitorFormulas: $response");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper: يُرسل prompt لـ Gemini ويُعيد JSON مُفكَّك
|
|
*/
|
|
public function callGemini(string $prompt, string $model): ?array {
|
|
$apiUrl = $this->baseUrl . $model . ":generateContent?key=" . $this->apiKey;
|
|
|
|
$payload = [
|
|
'contents' => [
|
|
[
|
|
'parts' => [
|
|
['text' => $prompt]
|
|
]
|
|
]
|
|
]
|
|
];
|
|
|
|
$ch = curl_init($apiUrl);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_POST => true,
|
|
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
|
CURLOPT_POSTFIELDS => json_encode($payload),
|
|
CURLOPT_TIMEOUT => 30,
|
|
CURLOPT_CONNECTTIMEOUT => 5
|
|
]);
|
|
|
|
$response = curl_exec($ch);
|
|
$curlErr = curl_error($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($curlErr) {
|
|
error_log("[SiroGeminiService] Curl Error: $curlErr");
|
|
return null;
|
|
}
|
|
|
|
if ($httpCode !== 200) {
|
|
error_log("[SiroGeminiService] HTTP Error $httpCode. Response: $response");
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
$responseData = json_decode($response, true);
|
|
$rawText = $responseData['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
|
// تنظيف أي علامات كود ماركداون محتملة من الموديل
|
|
$cleanJson = trim(preg_replace('/```json|```/', '', $rawText));
|
|
|
|
$decoded = json_decode($cleanJson, true);
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
error_log("[SiroGeminiService] JSON Decode Error: " . json_last_error_msg() . " | Raw text: $rawText");
|
|
return null;
|
|
}
|
|
return $decoded;
|
|
} catch (Exception $e) {
|
|
error_log("[SiroGeminiService] Exception: " . $e->getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public function evaluatePostsForReporting(array $posts, string $model = 'gemini-flash-lite-latest'): ?string {
|
|
if (!$this->apiKey || empty($posts)) return null;
|
|
|
|
$postsJson = json_encode($posts, JSON_UNESCAPED_UNICODE);
|
|
|
|
$prompt = "
|
|
أنت محلل بيانات استخباراتية للسوق لتطبيق 'سيرو' لنقل الركاب.
|
|
إليك مجموعة من المنشورات والتعليقات التي جمعها الروبوت الخاص بنا من مجموعات فيسبوك اليوم:
|
|
$postsJson
|
|
|
|
المطلوب منك:
|
|
1. قراءة جميع هذه المنشورات واستخراج أي شكاوى، أسئلة، أو نقاشات تتعلق بـ (تطبيقات النقل الذكي، أسعار المحروقات، باقات الإنترنت، مشاكل السيارات).
|
|
ملاحظة هامة عن بنية البيانات:
|
|
- أي نص يبدأ بـ [FEED]: هو عبارة عن المنشور الأصلي (البوست).
|
|
- أي نص يبدأ بـ [COMMENT]: هو تعليق تابع للمنشور الذي يسبقه مباشرة.
|
|
2. تلخيص أهم هذه النقاشات في تقرير قصير ومفيد (News Report). يرجى التمييز بين المنشور الأصلي والتعليقات عليه لفهم السياق.
|
|
3. تجاهل المنشورات العشوائية أو الشخصية التي لا تفيد السوق.
|
|
|
|
هام جداً:
|
|
- يجب أن يكون التقرير باللغة العربية بالكامل.
|
|
- يجب أن تغلف التقرير بالكامل بوسم <div dir=\"rtl\" align=\"right\"> في البداية و </div> في النهاية لضمان القراءة من اليمين لليسار.
|
|
- استخدم <h3> للعناوين و <ul> للقوائم داخل التقرير.
|
|
- لا تقم بتضمين علامات ```html في المخرجات.
|
|
إذا لم يكن هناك أي شيء مفيد، اكتب فقط: <div dir=\"rtl\" align=\"right\"><p>لا توجد بيانات مفيدة في هذه الدفعة.</p></div>
|
|
";
|
|
|
|
$url = $this->baseUrl . "{$model}:generateContent?key={$this->apiKey}";
|
|
|
|
$postData = [
|
|
'contents' => [
|
|
[
|
|
'parts' => [
|
|
['text' => $prompt]
|
|
]
|
|
]
|
|
],
|
|
'generationConfig' => [
|
|
'temperature' => 0.5,
|
|
'maxOutputTokens' => 1500
|
|
]
|
|
];
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData));
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
|
|
|
|
$response = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($httpCode === 200) {
|
|
$data = json_decode($response, true);
|
|
$text = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
|
return trim(preg_replace('/```html|```/', '', $text));
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public function evaluateTelegramForReporting(array $messages, string $model = 'gemini-flash-lite-latest'): ?string {
|
|
if (!$this->apiKey || empty($messages)) return null;
|
|
|
|
$messagesJson = json_encode($messages, JSON_UNESCAPED_UNICODE);
|
|
|
|
$prompt = "
|
|
أنت محلل بيانات استخباراتية للسوق لتطبيق 'سيرو' لنقل الركاب.
|
|
إليك مجموعة من الرسائل التي جمعها الروبوت الخاص بنا من قنوات ومجموعات السائقين على تليغرام اليوم:
|
|
$messagesJson
|
|
|
|
المطلوب منك:
|
|
1. قراءة جميع هذه الرسائل واستخراج أي شكاوى، أسئلة، أو نقاشات تتعلق بـ (تطبيقات النقل الذكي مثل كريم، أوبر، جيني، يلاغو، تكسي إف، أسعار المحروقات، مشاكل التطبيقات، الإضرابات).
|
|
ملاحظة هامة عن بنية البيانات:
|
|
- كل رسالة تبدأ بـ [TELEGRAM]: هي عبارة عن رسالة مرسلة في مجموعة تليغرام.
|
|
2. تلخيص أهم هذه النقاشات في تقرير قصير ومفيد (News Report).
|
|
3. تجاهل رسائل الانضمام للمجموعات، الملصقات، الإعلانات العشوائية، أو الرسائل القصيرة جداً التي لا تحتوي على معنى مفيد للسوق.
|
|
|
|
هام جداً:
|
|
- يجب أن يكون التقرير باللغة العربية بالكامل.
|
|
- يجب أن تغلف التقرير بالكامل بوسم <div dir=\"rtl\" align=\"right\"> في البداية و </div> في النهاية لضمان القراءة من اليمين لليسار.
|
|
- استخدم <h3> للعناوين و <ul> للقوائم داخل التقرير.
|
|
- لا تقم بتضمين علامات ```html في المخرجات.
|
|
إذا لم يكن هناك أي شيء مفيد، اكتب فقط: <div dir=\"rtl\" align=\"right\"><p>لا توجد بيانات مفيدة في هذه الدفعة.</p></div>
|
|
";
|
|
|
|
$url = $this->baseUrl . "{$model}:generateContent?key={$this->apiKey}";
|
|
|
|
$postData = [
|
|
'contents' => [
|
|
[
|
|
'parts' => [
|
|
['text' => $prompt]
|
|
]
|
|
]
|
|
],
|
|
'generationConfig' => [
|
|
'temperature' => 0.5,
|
|
'maxOutputTokens' => 1500
|
|
]
|
|
];
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData));
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
|
|
|
|
$response = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
if ($httpCode !== 200) {
|
|
echo "\n[GEMINI API ERROR] HTTP Code: $httpCode\n";
|
|
echo "Response: $response\n\n";
|
|
return null;
|
|
}
|
|
|
|
$data = json_decode($response, true);
|
|
$text = $data['candidates'][0]['content']['parts'][0]['text'] ?? '';
|
|
return trim(preg_replace('/```html|```/', '', $text));
|
|
return null;
|
|
}
|
|
}
|