חנות התוספים כתוכנה עצמאית לווינדוס: קובץ הרצה אחד, שמסנכרן את קטלוג התוספים מ-otzaria.org אל תיקייה שצמודה לו, מזהה איזו גרסה של אוצריא מותקנת במחשב, ומתקין את התוסף שתואם לה.
חנות התוספים.exe ← זה כל מה שמפיצים
Data\ ← נוצרת לידו בהרצה הראשונה
catalog.json הקטלוג הממוראה
plugins\<id>\... קובצי ה-.otzplugin, האייקונים וצילומי המסך
logs\store-<תאריך>.log
state.json מה זוהה על ההתקנה של אוצריא במחשב הזה
מרגע שהסנכרון הסתיים התוכנה עובדת בלי אינטרנט: הקטלוג והקבצים יושבים
ב-Data\, והתקנה אינה נוגעת ברשת. זה מה שמאפשר להעתיק את התיקייה לכונן
נייד ולהתקין ממנה במחשב מנותק.
ולמי שאין לו רשת גם בפעם הראשונה מתפרסמת חבילה מלאה — אותו exe עם כל התוספים כבר בתוכו.
0.5MB, קובץ אחד. אין מתקין, אין DLL לצד ה-exe, ואין תיקיית חילוץ. זה מושג בשלושה דברים:
| ה-loader של WebView2 | מקושר סטטית → אין WebView2Loader.dll |
| ה-CRT של Visual C++ | מקושר סטטית (/MT) → אין vcruntime140.dll |
| קובצי הממשק | דחוסים (LZMS) כ-resource בתוך ה-exe → אין תיקיית ממשק |
מנוע התצוגה עצמו הוא ה-WebView2 Runtime שכבר נמצא במחשב (מותקן מראש בווינדוס 11, ומגיע עם Edge בווינדוס 10), ולכן הוא אינו נספר בגודל.
להשוואה: הגרסה הקודמת נבנתה ב-Flutter ושקלה 11.8MB — היא נשאה איתה
מנוע רינדור שלם (flutter_windows.dll לבדו 21MB לא-דחוס), ופרסה 29MB
לתיקיית app-files\ בהרצה הראשונה כדי להריץ משם קובץ הרצה שני.
מי שמתקין במחשב מנותק צריך שהתוספים כבר יהיו אצלו. לכן מתפרסמת, לצד ה-exe הרזה, חבילה מלאה: אותו קובץ הרצה בדיוק, עם מראה שלמה של כל התוספים משורשרת לסופו.
Otzaria-Plugin-Store-Full.exe ~96MB, הרצה אחת
↓
חנות התוספים.exe 0.52MB — עותק של עצמו בלי המטען
Data\ כל התוספים, מוכנים
הרצה ראשונה פורסת את Data\, כותבת עותק רזה של עצמה, מפעילה אותו
ויוצאת. הרזה שואל אם למחוק את קובץ ההתקנה — ולא מוחק מעצמו: אותה
חבילה עשויה להיות מיועדת גם למחשב שני, ומחיקה שקטה של 96MB שהורדו הייתה
מאלצת הורדה מחדש.
החילוץ מחליף את plugins\ ואת catalog.json במלואם, ולא ממזג.
המראה היא נגזרת של (קטלוג, גרסאות היעד) ולא נתוני משתמש — זה בדיוק מה
ש-pruneUnusedFiles כבר עושה, רק בבת אחת. state.json ו-logs\, שהם
מקומיים למכונה, אינם נגעים.
המטען אינו נדחס: .otzplugin הוא ZIP והתמונות webp/png, ו-LZMS עליהם
מחזיר אחוזים בודדים תמורת פרישה של 96MB לזיכרון. שרשור לסוף ה-PE משאיר
את הבינארי שפורסם בייט-לבייט — כך שהעותק הרזה שנכתב בחילוץ הוא בדיוק
ה-exe שנבדק — ומאפשר חילוץ בזרימה בלי הקצאה גדולה אחת. RCDATA היה מחייב
בנייה מחדש של ה-exe סביב המטען, כלומר בינארי אחר מזה שפורסם.
הפורמט מוגדר ב-native/tools/pack_overlay.mjs ונקרא ב-native/src/overlay.cpp.
native/
src/ ה-host ב-C++: חלון, WebView2, וגשר לקבצים/רשת/רג'יסטרי
web/ הממשק וכל הלוגיקה — HTML/CSS/JS, בלי bundler ובלי npm
test/ בדיקות `node --test`
tools/ כלי בנייה ופיתוח, וכלי האריזה של החבילה המלאה
third_party/ ה-SDK של WebView2 (מובא מ-nuget, אינו בגיט)
build.ps1 הבנייה כולה
VERSION מספר הגרסה — מקום אחד
tool/
set_version.sh מציב את הגרסה (מריץ אותו ה-CI)
make_app_icon.ps1 בונה את ה-ico ואת האייקון שבשורת הכותרת ממקור אחד
ה-host הוא מעטפת טיפשה. כל הלוגיקה — המודלים, בחירת הבילד התואם,
הסנכרון, פירוק הכותרות, קריאת ה-manifest — יושבת ב-native/web/js/.
ה-host נותן חלון, ומגיש קבצים, רשת, רג'יסטרי ודיאלוג שמירה.
זה מכוון: הלוגיקה בצד ה-JS נבדקת ב-node --test בלי חלון ובלי ווינדוס,
ומה שנשאר ב-C++ הוא קריאות מערכת שאין בהן החלטות.
לכן גם אין פרסר JSON ב-C++: JS → host הוא שדות מופרדים ב-U+001F
(תו שאינו חוקי בנתיב ואינו מופיע בכתובת, ולכן אין מה לברוח ואין מה
לטעות בפרסור), ו-host → JS הוא מחרוזת JSON שנבנית עם escaping בלבד.
דורש Visual Studio (או Build Tools) עם כלי C++. אין תלות ב-CMake, ואין צורך להתקין SDK של שפה כלשהי.
.\native\build.ps1 # בנייה
.\native\build.ps1 -Run # בנייה והרצה
.\native\build.ps1 -Clean # מאפס את native\build\
.\native\build.ps1 -Debug # עם סמלי ניפוי, בלי אופטימיזציההסקריפט מביא בעצמו את ה-SDK של WebView2 מ-nuget (גרסה נעוצה ב-
native/tools/fetch_webview2.ps1), אורז את קובצי הממשק, וצורב את משאב
הגרסה.
cd native && node --testבלי שום תלות npm — node --test הוא המובנה. הבדיקות מכסות את הלוגיקה
הטהורה: השוואת גרסאות, בחירת הבילד התואם, פירוק כותרות ההורדה, המודלים,
התאריך העברי, וקורא ה-ZIP.
קורא ה-ZIP נבדק מול .otzplugin אמיתי ב-native/test/fixtures/,
שנוצר בכותב ZIP חיצוני (System.IO.Compression של .NET) — ולא בקוד
שלנו, כדי שהבדיקה לא תהיה מעגלית.
# מריץ, מצלם את החלון, ומדפיס את היומן.
# -CropHeight שומר רק את הפיקסלים העליונים (אימות המסגרת בלבד).
.\native\tools\screenshot.ps1 -Out shot.png -CropHeight 300# מביא את הקטלוג האמיתי מהאתר עם התמונות ובלי קובצי ההתקנה — כדי לראות
# את המסכים עם נתונים אמיתיים בלי להוריד מאות MB.
node native/tools/seed_catalog.mjs native/build/Data# בונה מראה מלאה בהרצת הסנכרון האמיתי, ואורז אותה לחבילה — בדיוק מה
# שהג'וב עושה, כדי שאפשר יהיה לשחזר תקלה בלי GitHub.
node native/tools/pack_mirror.mjs Data
node native/tools/pack_overlay.mjs "native/build/חנות התוספים.exe" Data Full.exe
node native/tools/unpack_overlay.mjs Full.exe verify # ואז diff -r Data verify# מחלץ מחדש את האייקונים מגופני Fluent UI אל native/web/js/ui/icons.js.
# להריץ רק כשמוסיפים אייקון (ראו ICONS שבסקריפט).
node native/tools/extract_icons.mjs22 האייקונים שבממשק הם הווקטורים המדויקים של
Fluent UI System Icons
(MIT) — אותם שגרסת ה-Flutter הציגה. extract_icons.mjs הוא פרסר TTF
מינימלי (cmap, loca, glyf, כולל גליפים מורכבים) שמוציא מהגופנים רק
את מה שבשימוש: 15KB במקום 4.5MB של שני גופנים.
ערכות הצבעים ב-native/web/css/tokens.css הן הפלט המדויק של
ColorScheme.fromSeed של Material 3 מגרסת ה-Flutter (זרעים #2C1B02
לבהיר ו-#9C27B0 לכהה), שנשפך לקובץ לפני שה-Flutter הוסר.
הגרסה היא מספר שלם אחד (1, 2, 3…) ויושבת ב-native/VERSION בלבד.
כל דחיפה ל-main שכל הבדיקות בה עברו מקדמת את המספר, מתייגת vN,
ומפרסמת release עם ה-exe — ראו .github/workflows/ci.yml.
התוכנה בודקת פעם אחת בהרצה אם יצא release חדש ומציגה על כך שורה. אין עדכון עצמי: הסיפור מסתיים בכתובת שנפתחת בדפדפן.
רץ יומית, וגם כשריצת CI הצליחה ב-main (exe חדש), וגם ביד. אינו
מקדם גרסה ואינו מתייג v<N>: התג הוא bundle מתגלגל, תמיד אותו
אחד, מתעדכן במקום.
שתי סיבות, שתיהן ב-store_release_client.js:
isStoreReleaseTagפוסל כל תג שאינו^v?\d+$, ולכןbundleנעלם מבדיקת העדכון — אחרת כל אריזה הייתה מודיעה למשתמשים על "גרסה חדשה" כשהקוד זהה.fetchLatestStableקורא דף אחד של 30 releases. פרסום חדש בכל ריצה היה דוחף אחרי כמה חודשים את כל ה-v<N>מחוץ לדף, והתראת העדכון הייתה מפסיקה לעבוד בשקט.
הג'וב זורע את עצמו מהחבילה הקודמת לפני שהוא מסנכרן, וכך #plan
מוריד מ-otzaria.org רק את הדלתא: עדכון של תוסף אחד עולה קובץ אחד ולא
96MB. בדרך אגב זה מריץ ב-CI בדיוק את המסלול האינקרמנטלי שכונן של משתמש
עובר בעדכון, ולא "מראה ריקה" שאיש אינו חי בה.
מה נחשב שינוי נקבע ב-mirror_changed.mjs, בהשוואת התוצאה ולא
בגיבוב של ה-API. downloadCount, הדירוגים) מנוטרלים
מההשוואה: נמדד שסנכרון שדילג על כל 38 התוספים ולא הוריד דבר עדיין
הפיק קטלוג שונה, וכל ההבדל היה מונה ההורדות של תוסף אחד — כלומר בלי
הנטרול הג'וב היה מפרסם 96MB בכל ריצה.
unpack_overlay.mjs --slim) ומשווה
בייטים.
- תיקיית הנתונים תמיד צמודה לקובץ ההרצה ואינה ניתנת לשינוי — זו
הדרישה של עבודה מכונן נייד. כשאי אפשר לכתוב בה אבל יש בה קטלוג,
התוכנה רצה במצב קריאה (ההתקנות כותבות למחשב) והיומן עובר ל-
%LOCALAPPDATA%. - המאגר הפנימי של ה-WebView2 יושב ב-
%LOCALAPPDATA%ולא ב-Data\. נמדד שהוא 9.5MB ב-189 קבצים אחרי הרצה אחת ו-14MB אחרי שמונה, ומתוכם ~5MB הם מטמון שיידרים שנגזר מכרטיס הגרפי של המחשב — כלומר חסר תוקף במחשב אחר. גם מחיקה בסיום נבדקה ואינה עובדת: ה-WebView2 מריץ תהליכי-בן ששורדים את סגירת החלון ומחזיקים מנעולים על הקבצים. - שם קובץ ההרצה אינו מכיל "אוצריא" בכוונה: זיהוי ההתקנה של אוצריא
עובד לפי השם, ו-exe בשם כזה שיושב ב-
C:\אוצריאהיה נבחר שם כהתקנה של אוצריא עצמה.⚠️ הזיהוי מתאים גם ל-otzariaהלטיני, ולכן גםOtzaria-Plugin-Store-Full.exe— קובץ ההתקנה של החבילה, שהמשתמש עשוי לבחור להשאיר — נמצא ב-OUR_OWN_EXE_NAMESשב-otzaria_probe.js. selfPathאינו מכסה אותו: אחרי הפריסה מי שרץ הוא ה-exe הרזה. - ההתקנה נמסרת לאוצריא דרך
otzaria://plugin/install-local— ולא בפרישת ה-ZIP בעצמנו, כי אוצריא מנהלת רישום פנימי לתוספים המותקנים ופרישה ידנית עוקפת אותו. כשנתיב ההתקנה ידוע ה-URL נמסר לקובץ ההרצה עצמו ולא למערכת ההפעלה, כי התקנה ניידת אינה רושמת את הסכימה בכלל.
GPL-3.0 — ראו LICENSE.