الدرس التعليمي: ابنِ تطبيقك الأول
في هذا الدرس التعليمي، ستبني برنامجاً مصغراً عداد الكلمات يقرأ سجل المحادثة النشط، يعد الكلمات حسب الدور (المستخدم مقابل المساعد)، ويعرض الإحصائيات. بنهاية الدرس، ستعرف كيفية إنشاء واختبار وتغليف ونشر برنامج مصغر.
ما ستتعلمه:
- إنشاء
manifest.json - كتابة HTML التطبيق مع SDK
window.ais - استخدام
ais.chat.getHistory()لقراءة الرسائل - استخدام
ais.storageللاحتفاظ بالبيانات عبر الجلسات - الاختبار محلياً بالتحميل الجانبي
- التغليف كحزمة
.ais - النشر في سجل المجتمع
الوقت المطلوب: حوالي 15 دقيقة.
الخطوة 1: إنشاء البيان
كل برنامج مصغر يحتاج manifest.json يصف التطبيق وأذوناته ونقطة دخوله. أنشئ دليلاً جديداً وأضف هذا الملف:
mkdir word-counter
cd word-counter
أنشئ manifest.json:
{
"name": "word-counter",
"version": "1.0.0",
"abi": 1,
"type": "mini-program",
"title": "عداد الكلمات",
"description": "عد الكلمات في سجل محادثاتك حسب الدور",
"author": { "name": "اسمك" },
"entry": "index.html",
"base_url": "https://localhost:8080/",
"permissions": ["storage", "chat:read", "ui:toast"],
"keywords": ["إحصائيات", "أداة مساعدة"]
}
الحقول الرئيسية:
| الحقل | القيمة | لماذا |
|---|---|---|
name | word-counter | معرف فريد (أحرف صغيرة، واصلات فقط) |
abi | 1 | مطلوب -- يطابق ABI المنصة الحالية |
type | mini-program | يخبر المنصة أن هذا تطبيق iframe معزل |
entry | index.html | ملف HTML للتحميل |
base_url | https://localhost:8080/ | أين تُستضاف الأصول (حدّث للإنتاج) |
permissions | ["storage", "chat:read", "ui:toast"] | نحتاج لقراءة المحادثة وحفظ الإحصائيات |
إذن storage ممنوح دائماً تلقائياً، لكن من الممارسات الجيدة إدراجه صراحة ليعرف المستخدمون أن تطبيقك يخزن بيانات.
الخطوة 2: إنشاء HTML
أنشئ index.html في نفس الدليل. هذا هو تطبيقك بالكامل -- HTML و CSS و JavaScript في ملف واحد.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family:
system-ui,
-apple-system,
sans-serif;
padding: 20px;
color: #e0e0e0;
background: #1a1a2e;
min-height: 100vh;
}
h1 {
font-size: 22px;
margin-bottom: 16px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.stat-card {
background: #16213e;
border: 1px solid #333;
border-radius: 8px;
padding: 16px;
text-align: center;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #58a6ff;
font-variant-numeric: tabular-nums;
}
.stat-label {
font-size: 14px;
color: #888;
margin-top: 4px;
}
.actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
min-height: 48px;
padding: 10px 20px;
font-size: 16px;
border: 1px solid #444;
border-radius: 6px;
background: #2a2a4e;
color: #e0e0e0;
cursor: pointer;
}
button:hover {
background: #3a3a5e;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background: #1a73e8;
border-color: #1a73e8;
}
.btn-primary:hover {
background: #1557b0;
}
.btn-close {
background: none;
border-color: #666;
color: #888;
}
#last-updated {
margin-top: 16px;
font-size: 14px;
color: #666;
}
</style>
</head>
<body>
<h1>عداد الكلمات</h1>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="total-words">--</div>
<div class="stat-label">إجمالي الكلمات</div>
</div>
<div class="stat-card">
<div class="stat-value" id="user-words">--</div>
<div class="stat-label">كلماتك</div>
</div>
<div class="stat-card">
<div class="stat-value" id="ai-words">--</div>
<div class="stat-label">كلمات الذكاء الاصطناعي</div>
</div>
<div class="stat-card">
<div class="stat-value" id="msg-count">--</div>
<div class="stat-label">الرسائل</div>
</div>
</div>
<div class="actions">
<button class="btn-primary" id="analyze">تحليل المحادثة</button>
<button class="btn-close" id="close">إغلاق</button>
</div>
<div id="last-updated"></div>
<script>
// مساعد: عد الكلمات في نص
function countWords(text) {
if (!text || typeof text !== "string") return 0;
return text.trim().split(/\s+/).filter(Boolean).length;
}
// تنسيق الأرقام بفواصل (1234 -> "1,234")
function fmt(n) {
return n.toLocaleString();
}
ais.ready(async function () {
// تعيين عنوان اللوحة
ais.ui.setTitle("عداد الكلمات");
// محاولة استعادة آخر إحصائيات محفوظة
var saved = await ais.storage.get("last-stats");
if (saved) {
showStats(saved);
}
// زر التحليل
document
.getElementById("analyze")
.addEventListener("click", async function () {
this.disabled = true;
this.textContent = "جاري التحليل...";
try {
// جلب حتى 500 رسالة من سجل المحادثة
var messages = await ais.chat.getHistory(500);
var userWords = 0;
var aiWords = 0;
var msgCount = messages.length;
for (var i = 0; i < messages.length; i++) {
var msg = messages[i];
var words = countWords(msg.content);
if (msg.role === "user") {
userWords += words;
} else if (msg.role === "assistant") {
aiWords += words;
}
}
var stats = {
total: userWords + aiWords,
user: userWords,
ai: aiWords,
messages: msgCount,
timestamp: Date.now(),
};
showStats(stats);
// حفظ الإحصائيات للمرة القادمة
await ais.storage.set("last-stats", stats);
ais.ui.toast("تم تحليل " + msgCount + " رسالة!");
} catch (err) {
ais.ui.toast("خطأ: " + err.message);
}
this.disabled = false;
this.textContent = "تحليل المحادثة";
});
// زر الإغلاق
document.getElementById("close").addEventListener("click", function () {
ais.close();
});
});
function showStats(stats) {
document.getElementById("total-words").textContent = fmt(stats.total);
document.getElementById("user-words").textContent = fmt(stats.user);
document.getElementById("ai-words").textContent = fmt(stats.ai);
document.getElementById("msg-count").textContent = fmt(stats.messages);
if (stats.timestamp) {
var date = new Date(stats.timestamp);
document.getElementById("last-updated").textContent =
"آخر تحليل: " + date.toLocaleString();
}
}
</script>
</body>
</html>
ما يفعله هذا الكود
ais.ready()-- ينتظر اتصال جسر SDK قبل تشغيل أي منطق.ais.storage.get('last-stats')-- يستعيد الإحصائيات المحفوظة سابقاً ليرى المستخدم البيانات فوراً عند الإطلاق.ais.chat.getHistory(500)-- يجلب حتى 500 رسالة من المحادثة النشطة.- عد الكلمات -- يمر عبر الرسائل، يقسم المحتوى على المسافات ويجمع حسب الدور.
ais.storage.set('last-stats', stats)-- يحفظ النتائج للمرة القادمة.ais.ui.toast()-- يظهر إشعاراً عند اكتمال التحليل.ais.close()-- يعود لعرض المحادثة عندما ينقر المستخدم إغلاق.
الخطوة 3: الاختبار محلياً
تحتاج خادم HTTP محلي لخدمة البيان وملف HTML. استخدم أي أداة تفضلها:
# Python 3
cd word-counter
python3 -m http.server 8080
# أو Node.js
npx serve -p 8080
# أو PHP
php -S localhost:8080
الآن ثبّت التطبيق في المنصة:
- افتح aiscouncil.net وسجّل الدخول
- انقر أيقونة التطبيقات في الشريط الجانبي الأيسر
- في قسم التحميل الجانبي، الصق:
http://localhost:8080/manifest.json - انقر تثبيت
- راجع الأذونات (storage، chat:read، ui:toast) وانقر سماح
- انقر فتح على بطاقة التطبيق المثبت
لأسرع دورة تطوير، استخدم رفع HTML بدلاً من التحميل الجانبي عبر URL. ارفع index.html مباشرة -- لا حاجة لخادم. تنشئ المنصة بياناً اصطناعياً تلقائياً. يمكنك إلغاء التثبيت وإعادة الرفع في كل مرة تجري تغييرات.
استكشاف الأخطاء وإصلاحها
| المشكلة | الحل |
|---|---|
| "فشل جلب البيان" | تأكد من أن خادمك المحلي يعمل ويخدم رؤوس CORS. جرب python3 -m http.server 8080 الذي يخدم CORS آمن. |
| التطبيق يظهر صفحة بيضاء فارغة | تحقق من وحدة تحكم المتصفح للأخطاء. المشكلة الأكثر شيوعاً هي استدعاء طرق ais.* قبل ais.ready(). |
| "PermissionDenied: chat:read" | بيانك لا يتضمن chat:read في مصفوفة الأذونات. حدّث البيان وأعد التثبيت. |
| التطبيق لا يُحدّث بعد تغييرات الكود | ألغِ تثبيت التطبيق أولاً (انقر زر X على البطاقة)، ثم أعد التثبيت. HTML المدخل يُخزّن مؤقتاً وقت التثبيت. |
الخطوة 4: أضف بعض التحسينات
لنضف ميزة: عد الكلمات في الوقت الفعلي مع وصول رسائل جديدة.
أضف هذا الكود داخل callback ais.ready()، بعد معالج زر الإغلاق:
// الاشتراك في الرسائل الجديدة للعد في الوقت الفعلي
ais.chat.onMessage(function (msg) {
// إعادة قراءة الإحصائيات المحفوظة وإضافة كلمات الرسالة الجديدة
ais.storage.get("last-stats").then(function (stats) {
if (!stats) return;
var words = countWords(msg.content);
if (msg.role === "user") stats.user += words;
else if (msg.role === "assistant") stats.ai += words;
stats.total = stats.user + stats.ai;
stats.messages++;
stats.timestamp = Date.now();
showStats(stats);
ais.storage.set("last-stats", stats);
});
});
الآن تتحدث العدادات مباشرة أثناء محادثة المستخدم دون الحاجة للنقر "تحليل" مرة أخرى.
الخطوة 5: التغليف كحزمة .ais
حزمة .ais هي أرشيف ZIP يحتوي على بيانك وجميع ملفات التطبيق. تستخرج المنصة الـ ZIP، تقرأ البيان، وتضمن جميع الأصول (CSS، JS، الصور) في HTML المدخل.
لتطبيق ملف واحد مثل تطبيقنا، الحزمة بسيطة:
cd word-counter
zip -r ../word-counter.ais manifest.json index.html
هذا ينشئ word-counter.ais في الدليل الأصل.
اختبار الحزمة
- في المنصة، اذهب إلى التطبيقات وانقر رفع تطبيق
- اختر
word-counter.ais - راجع الأذونات ووافق
- يُثبّت التطبيق من الحزمة مع جميع الأصول مضمنة
الحزم مكتفية ذاتياً. المستخدم لا يحتاج وصولاً شبكياً لـ base_url الأصلي -- كل شيء مضمن وقت التثبيت. هذا يجعل الحزم مثالية للتوزيع دون اتصال والمشاركة.
الحزم متعددة الملفات
إذا كان تطبيقك لديه ملفات CSS أو JavaScript أو صور منفصلة، أدرجها جميعاً في الـ ZIP:
zip -r ../word-counter.ais manifest.json index.html style.css app.js icon.png
تضمن المنصة تلقائياً:
<link rel="stylesheet" href="style.css">تصبح<style>...</style><script src="app.js"></script>تصبح<script>...</script><img src="icon.png">تصبح<img src="data:image/png;base64,...">
الخطوة 6: النشر في السجل
عندما يكون تطبيقك جاهزاً لاستخدامه الآخرون، انشره في سجل المجتمع.
1. استضف ملفاتك
ارفع بيانك و HTML المدخل إلى CDN عام. GitHub Pages مجاني وسهل:
# في مستودع GitHub الخاص بك (مثلاً، github.com/yourname/word-counter)
# ادفع manifest.json و index.html إلى الفرع الرئيسي
# فعّل GitHub Pages في إعدادات المستودع (المصدر: الفرع الرئيسي، الجذر)
ستكون ملفاتك متاحة في:
https://yourname.github.io/word-counter/manifest.jsonhttps://yourname.github.io/word-counter/index.html
حدّث base_url في بيانك ليطابق:
"base_url": "https://yourname.github.io/word-counter/"
2. انسخ مستودع aiscouncil
اذهب إلى github.com/nicholasgasior/bcz وانقر Fork.
3. أضف إدخال حزمتك
حرّر registry/packages.json وأضف إدخالاً لمصفوفة packages:
{
"name": "word-counter",
"type": "mini-program",
"version": "1.0.0",
"manifest": "https://yourname.github.io/word-counter/manifest.json",
"tier": "community",
"category": "utilities",
"description": "عد الكلمات في سجل محادثاتك حسب الدور",
"icon": "https://yourname.github.io/word-counter/icon.png",
"added": "2026-02-19",
"price": 0,
"currency": "USD",
"seller": null
}
4. التحقق
شغّل سكربت التحقق للتحقق من إدخالك:
python3 registry/validate.py packages
إذا نجح التحقق، أنت جاهز للإرسال.
5. أرسل طلب سحب
ادفع تغييراتك إلى نسختك وأنشئ PR مقابل المستودع الرئيسي. إذا نجح التحقق الآلي، يمكن دمج الـ PR وسيظهر تطبيقك في قسم متجر التطبيقات في المنصة.
راجع النشر في السجل للتفاصيل الكاملة حول التسعير وإعداد البائع ومستويات التحقق.
نصائح وأفضل الممارسات
التصميم
- المظهر الداكن افتراضياً -- معظم مستخدمي المنصة يستخدمون الوضع الداكن. صمّم لخلفيات داكنة (
#1a1a2eأو مشابه) بنص فاتح (#e0e0e0). - أهداف لمس بحد أدنى 48 بكسل -- يجب أن تكون الأزرار والعناصر التفاعلية بارتفاع 48 بكسل على الأقل لإمكانية الوصول والتفاعل الصديق لـ VLM.
- حجم خط بحد أدنى 14 بكسل -- يجب أن يكون كل النص 14 بكسل على الأقل للقراءة.
- تخطيط مستجيب -- عرض لوحة التطبيقات يختلف. استخدم CSS Grid أو Flexbox مع
auto-fitللتكيف.
الأداء
- خزّن النتائج مؤقتاً في التخزين -- استخدم
ais.storageلحفظ النتائج المحسوبة. استعدها عند الإطلاق ليرى المستخدم البيانات فوراً. - حدّد طلبات سجل المحادثة --
ais.chat.getHistory(500)عادة كافٍ. تجنب طلب سجل غير محدود. - لا استطلاع -- استخدم
ais.chat.onMessage()للتحديثات في الوقت الفعلي بدلاً من استدعاءgetHistoryبشكل متكرر.
الأمان
- اطلب أذونات دنيا -- أدرج فقط الأذونات التي يستخدمها تطبيقك فعلاً. أذونات أقل تعني مستخدمين أكثر سيثقون ويثبتون تطبيقك.
- تحقق من جميع المدخلات -- البيانات من
ais.chat.getHistory()تحتوي محتوى من إنشاء المستخدم. نظّف قبل الإدراج في DOM. - لا تخزن بيانات حساسة --
ais.storageغير مشفر. لا تخزن أبداً كلمات مرور أو رموز أو مفاتيح API (إلا إذا كان تطبيقك لديهsecrets:syncويتعامل صراحة مع نقل الاعتمادات).
التوافق
- تحقق من
ais.platform.abi-- إذا كان تطبيقك يعتمد على ميزات SDK محددة، تحقق من إصدار ABI وأظهر رسالة مفيدة إذا كانت المنصة أقدم. - غلف استدعاءات SDK في try/catch -- أخطاء الأذونات واختلافات إصدار المنصة يمكن أن تسبب رفض. تعامل معها بلطف.
- اختبر مع مثال hello-world -- المنصة تأتي مع برنامج مصغر نموذجي يمكنك استخدامه كمرجع.