برمجة JavaScript
يدعم Chute برمجة JavaScript لتعديل الطلبات/الاستجابات المتقدم، مطابقة القواعد المخصصة، تحليل DNS، والمهام المجدولة. تعمل السكريبتات على JavaScriptCore في منصات Apple وعلى QuickJS في Android، وتتبع واجهة برمجة السكريبتات المتوافقة مع Surge.
تعرف السكريبتات في القسم [Script] من ملف الإعدادات.
الإعدادات
[Script]
MyScript = type=http-request, script-path=/path/to/script.js, pattern=^https?://example\.com, requires-body=true, max-size=65536, timeout=10, argument=myArg
CronJob = type=cron, script-path=/path/to/cron.js, cron-expression=*/30 * * * *
معاملات السكريبت
| المعامل | مطلوب | الافتراضي | الوصف |
|---|---|---|---|
type |
لا | http-request |
نوع تشغيل السكريبت (انظر أدناه) |
script-path |
نعم | — | مسار ملف محلي أو رابط HTTP(S) لسكريبت JS |
pattern |
لا | (مطابقة الكل) | نمط رابط بتعبير نمطي لتصفية وقت تشغيل السكريبت. يُطابَق في أي موضع من رابط الطلب الكامل — https://… بعد فك التشفير، وhttp://… لـ HTTP العادي — ولا يُطابَق أبداً مع المسار وحده، لذا لا يطابق نمطٌ مثل ^/api |
requires-body |
لا | اكتشاف تلقائي | فرض استلام السكريبت لمحتوى الطلب/الاستجابة الكامل |
max-size |
لا | 131072 (128KB) | الحد الأقصى لحجم المحتوى بالبايت للسكريبتات التي تصل للمحتوى. إذا تجاوز المحتوى المجمّع هذا الحجم، يُتخطى السكريبت لذلك الطلب (لا يتم اقتطاع المحتوى). وفي HTTP/1.x — HTTP العادي وHTTP/1.1 المفكوك تشفيره — لا يخزّن Chute للسكريبتات أكثر من 131072 بايت مهما كانت هذه القيمة، فلا يمكنها هناك إلا خفض الحد؛ أما رسالة HTTP/2 المفكوك تشفيرها فتُسلَّم كاملة ولا تُقارن إلا بـ max-size |
timeout |
لا | 5.0 ثانية | مهلة لكل تنفيذ، تُحسب من بدايته: تحميل المصدر (وتنزيله إن كان السكريبت بعيداً) وتشغيل الشيفرة وانتظار المؤقتات والطلبات كلها تتقاسمها. القيم فوق 30 ثانية تُخفض إلى 30 |
argument |
لا | — | نص وسيط مخصص متاح كـ $argument في JS |
full-header-mode |
لا | false | عند true يصبح $request.headers و$response.headers مصفوفتين من {field, value}، عنصر لكل سطر ترويسة (صيغة Surge)، بدلاً من كائنين. يسري على سكريبتات http-request وhttp-request-before-send وhttp-response؛ ويقبل $done() الصيغتين في الحالتين. انظر الترويسات ذات الأسطر المتعددة |
debug |
لا | false | محجوز؛ لا تأثير له حالياً |
cron-expression |
لا | — | تعبير جدولة cron (لنوع cron فقط)؛ ويُقبل cronexp= (وتهجئة Shadowrocket cronexpr=) بوصفه المفتاح نفسه |
event-name |
لا | (كل الأحداث) | الحدث الذي يستمع إليه سكريبت event — network-changed أو engine-started أو profile-reloaded؛ وبلا اسم يعمل السكريبت على الثلاثة جميعاً (راجع سكريبت الحدث) |
wake-system |
لا | false | محجوز؛ يُحلَّل لكن لا تأثير له حالياً |
enable |
لا | true | تفعيل أو تعطيل هذا السكريبت |
script-update-interval |
لا | 0 | استطلاع تحديث السكريبتات البعيدة (انظر نموذج التنفيذ) |
binary-body-mode |
لا | — | يُقبل للتوافق مع Surge (ويُكتب أيضاً binary-mode) ويُحفظ عند حفظ الملف، لكنه لا يغيّر شيئاً: البايتات الخام موجودة دائماً في .bodyBytes |
engine |
لا | — | يُقبل ويُحفظ لكنه يُتجاهل: كل سكريبت يعمل على المحرك الخاص بمنصته |
أنواع السكريبتات
| نص النوع | التعداد | الوصف |
|---|---|---|
http-request |
HTTP Request | اعتراض وتعديل طلبات HTTP قبل إرسالها إلى الخادم الأعلى |
http-response |
HTTP Response | اعتراض وتعديل استجابات HTTP قبل العميل |
http-request-before-send |
HTTP Request Before Send | تعديل الطلب قبيل إرساله إلى الخادم الأعلى |
rule |
Rule | منطق مطابقة قواعد مخصص |
dns |
DNS | تحليل DNS مخصص |
cron |
Cron | سكريبتات مجدولة/موقوتة |
event |
Event | معالجات أحداث النظام (مثال: network-changed) |
generic |
Generic | يُقبل للتوافق مع Surge؛ ولا يرتبط بالطلبات أو القواعد أو DNS أو الأحداث، لذا لا يشغّله Chute من تلقاء نفسه. شغّله عبر تشغيل (Run) في قائمة السكريبتات في Chute iOS أو Chute Android، أو عبر POST /api/scripts/run في HTTP Control API |
يُبلَّغ عن قيمة type= غير المعروفة في السجل، وتُعامل معاملة http-request.
اكتشاف المحتوى التلقائي: في سكريبتات
http-requestوhttp-response، المصدر الذي يحتوي على$request.bodyأو$response.bodyيحصل على المحتوى تلقائياً حتىmax-size. استخدمrequires-body=trueلفرض هذا السلوك. أما السكريبت الذي لا يقرأ إلا.bodyBytesأو.rawBodyفلا يُكتشف، وسكريبتاتhttp-request-before-sendلا تُكتشف أبداً: فهذه تحتاج إلىrequires-body=true.
مرجع JavaScript API
تعمل السكريبتات في بيئة JavaScript معزولة — JavaScriptCore في منصات Apple، وQuickJS في Android — مع توفر الكائنات العامة التالية.
$request (للقراءة فقط)
متوفر في: http-request, http-response, http-request-before-send, rule, dns
| الخاصية | النوع | الوصف |
|---|---|---|
.url |
String | رابط الطلب الكامل؛ يُكتب المنفذ ما لم يكن المنفذ الافتراضي للمخطط، ويوضع مضيف IPv6 بين قوسين معقوفين. وإذا كان هدف الطلب نفسه رابطاً مطلقاً استُعمل كما كُتب، والهدف الذي لا يبدأ بـ / تُضاف إليه /، وإن لم يُعرف المضيف كانت القيمة المسار وحده. وفي سكريبت rule هي مضيف الوجهة، وفي سكريبت dns النطاق المُستعلَم عنه |
.method |
String | طريقة HTTP (GET, POST, إلخ) أو QUERY لـ DNS |
.headers |
Object أو Array | ترويسات الطلب، كل اسم مع قيمته؛ وتُدمج قيم الترويسة ذات الأسطر المتعددة في نص واحد (انظر الترويسات ذات الأسطر المتعددة). وهي مصفوفة من {field, value} مع full-header-mode=true. وتُقرأ الأسماء أياً كانت حالة أحرفها (انظر أسماء الترويسات) |
.body |
String أو null | محتوى الطلب (مفكوك UTF-8) |
.bodyBytes |
Bytes أو null | بايتات محتوى الطلب الخام (انظر الملاحظة أدناه) |
.hostname |
String | اسم المضيف الهدف |
.destPort |
Number | منفذ الوجهة |
.processPath |
String | مسار العملية الطالبة (macOS؛ وعلى Android مسار ملف APK للتطبيق، لاتصالات TCP التي تلتقطها VPN) |
.userAgent |
String | قيمة ترويسة User-Agent |
.sourceIP |
String | عنوان IP المصدر |
.listenPort |
Number | منفذ استماع البروكسي |
.requestId |
String | معرف طلب فريد |
.dnsResult |
String | عنوان IP المحلل |
.srcPort |
Number | منفذ المصدر |
.protocol |
String | البروتوكول المكتشف: http, https, tcp, dns |
$response (للقراءة فقط)
متوفر في: http-response
| الخاصية | النوع | الوصف |
|---|---|---|
.status |
Number | رمز حالة HTTP |
.headers |
Object أو Array | ترويسات الاستجابة، كل اسم مع قيمته؛ وتُدمج قيم الترويسة ذات الأسطر المتعددة في نص واحد (انظر الترويسات ذات الأسطر المتعددة). وهي مصفوفة من {field, value} مع full-header-mode=true. وتُقرأ الأسماء أياً كانت حالة أحرفها (انظر أسماء الترويسات) |
.body |
String أو null | محتوى الاستجابة (مفكوك UTF-8) |
.bodyBytes |
Bytes أو null | بايتات محتوى الاستجابة الخام (انظر الملاحظة أدناه) |
.rawBody |
Bytes أو null | اسم بديل لـ .bodyBytes |
يحمل
.bodyو.bodyBytesو.rawBodyالمحتوى نفسه: فقبل أن يراه السكريبت يُزال تقطيع chunked ويُفكّ ترميزContent-Encodingمن نوع gzip أو deflate. يحمل.bodyBytesو.rawBodyكائن بايتات. مرّره إلى استدعاء يأخذ بايتات —$utils.ungzip()، ونتيجته كائن بايتات أيضاً، أوbodyداخل$done()، الذي يأخذه بوصفه المحتوى الخام. وطبيعة هذا الكائن تتوقف على المحرك: غلاف معتم لا.lengthله ولا فهرسة على منصات Apple، وUint8Arrayعلى Android، لذا لا ينبغي للسكريبت المعدّ لكل المنصات أن يعتمد على.lengthأو الفهرسة. ولقراءة المحتوى استخدم.body، وهي البايتات نفسها مفكوكةً بـ UTF-8.
$done(value) — معالج الإكمال
يجب استدعاؤه مرة واحدة بالضبط في نهاية السكريبت للإشارة إلى الاكتمال. أما السكريبت الذي ينتهي دون أن يستدعيه فيُكمَل تمريراً في الحال، ما لم يكن هناك مؤقّت setTimeout أو طلب $httpClient معلّق. ويبدأ حساب timeout= لحظة بدء التنفيذ: تحميل المصدر وتشغيل الشيفرة وذلك الانتظار كلها تُحسب منه.
$done({}) // تمرير — لا تعديلات
$done() // إجهاض الاتصال
$done({matched: true}) // نتيجة مطابقة القاعدة (لسكريبتات القواعد فقط)
$done({address: "1.2.3.4"}) // نتيجة DNS (لسكريبتات DNS فقط)
في سكريبت http-request أو http-request-before-send أو http-response، يؤدي استدعاء $done() بلا وسيط — أو بقيمة ليست كائناً — إلى إجهاض الاتصال: لا يرسل Chute شيئاً بدلاً من الطلب أو الاستجابة، ولا يصل الطلب المُجهَض إلى الخادم أبداً. وما كان في طريقه إلى العميل يُكتب أولاً، ثم يُغلق الاتصال فوراً. وفي HTTP/2 لا يُعاد تعيين إلا دفق ذلك الطلب (RST_STREAM برمز الخطأ CANCEL) ولا يُرسل شيء بدلاً منه؛ ويستمر الاتصال والطلبات الأخرى عليه كالمعتاد.
تُفحص الترويسات وurl التي يعيدها السكريبت قبل استخدامها. وتُسقط الترويسة، مع تحذير في السجل، في الحالتين التاليتين:
- إذا احتوى اسمها أو قيمتها على فاصل سطر (CR أو LF) أو محرف NUL:
[JS] Dropped header <name>: a header name or value cannot contain a line break؛ - إذا لم يكن اسمها اسم حقل HTTP صالحاً (RFC 9110 §5.1). والاسم الصالح يتألف من محرف واحد أو أكثر من الأحرف أو الأرقام أو أيٍّ من
!#$%&'*+-.^_`|~، لذا يُسقط الاسم الفارغ، أو الذي يحتوي على مسافة أو نقطتين (:) أو حرف من خارج ASCII:[JS] Dropped header <name>: not a valid header name.
ويُطبَّق باقي النتيجة. وتشمل الفحوص نفسها الترويسات المعطاة بصيغة المصفوفة وترويسات response الذي يعيده سكريبت الطلب. أما url الذي يحتوي على فاصل سطر أو NUL أو مسافة فيُتجاهل، مع تحذير كذلك. ويُرسَل مسار url واستعلامه كما كُتبا: فلا تُفكّ رموز الهروب مثل %20.
قيم إرجاع سكريبت طلب HTTP:
$done({
url: "https://new.example.com/path", // إعادة كتابة الرابط
headers: {"X-Custom": "value"}, // تعديل الترويسات
body: "new request body", // تعديل المحتوى
response: { // إرجاع استجابة اصطناعية (تخطي الخادم الأعلى)
status: 200,
headers: {"Content-Type": "text/html"},
body: "<html>Blocked</html>"
}
})
عند توفير response، يتم اختصار الطلب: يرجع Chute الاستجابة الاصطناعية مباشرة إلى العميل، ولا يُرسَل الطلب إلى الخادم الأعلى. ويصح ذلك أينما عالج Chute الطلب، بما في ذلك HTTP/2، وسواء عمل السكريبت على الترويسة أو على المحتوى كاملاً أو بوصفه http-request-before-send. وفي HTTP/1.x يحمل سطر الحالة عبارة السبب القياسية للرمز (HTTP/1.1 404 Not Found) ويُغلق الاتصال بعد خروج الاستجابة؛ أما في HTTP/2 فلا يُجاب إلا دفق ذلك الطلب. وstatus رقم بين 200 و599، أو نص يبقى عدداً صحيحاً كهذا بعد حذف المسافات المحيطة به ("404")؛ وأي قيمة أخرى، أو غيابها، تعطي 200. ويُحسب Content-Length من body مهما قالت headers. هذا مفيد للحظر، محاكاة APIs، أو إرجاع محتوى مخزن مؤقتاً.
الإجابات التي يولّدها Chute بنفسه — Map Local، و
responseالسكريبت، وإجابات إعادة كتابة الرابط وسياسات REJECT وصفحة الخطأ الخاصة بها — تتبع HTTP: فالإجابة عن طلب HEAD هي الترويسة وحدها، مع Content-Length الذي كان سيحصل عليه طلب GET، والإجابة ذات الحالة 204 أو 205 أو 304 لا تحمل محتوى ولا Content-Length.
قيم إرجاع سكريبت استجابة HTTP:
$done({
status: 200, // تعديل رمز الحالة
headers: {"X-Custom": "value"}, // تعديل ترويسات الاستجابة
body: "new response body", // تعديل محتوى الاستجابة
url: "https://other.example.com" // إعادة توجيه (302 افتراضياً)
})
يستبدل body محتوى الاستجابة ولا شيء غيره: يبقى سطر الحالة والترويسات كما أرسلها الخادم، وتُدمج فيها status وheaders المُعادة، ويُسقط محتوى الخادم الأصلي. وإذا عمل السكريبت على الترويسة وحدها، تعمل بعده قواعد إعادة كتابة المحتوى وسكريبتات http-response التي تتلقى المحتوى كالمعتاد، على المحتوى الجديد. والاستجابة لطلب HEAD، أو التي حالتها 1xx أو 204 أو 205 أو 304، لا محتوى لها، فيُتجاهل body المُعاد فيها، بينما تظل status وheaders سارية.
وstatus رقم بين 200 و599، أو نص يبقى عدداً صحيحاً كهذا بعد حذف المسافات المحيطة به ("301"). وتُتجاهل أي قيمة أخرى، بما فيها true وfalse، وتحتفظ الاستجابة بحالتها. وحين يغيّر status الرمز، يحمل سطر الحالة عبارة السبب القياسية للرمز الجديد. وإذا جعل status الحالة 204 أو 205 أو 304، تُرسل الاستجابة بلا محتوى — لا محتوى الخادم ولا body المُعاد — وبلا ترويسة طول؛ وفي HTTP/1.x يُغلق الاتصال بعد خروج الترويسة.
عند توفير url، يجيب Chute بإعادة توجيه إليه بدلاً من الاستجابة الأصلية. وحالتها هي status المُعادة — 301 أو 307 أو 308 مثلاً — أو 302 إن لم تُعطَ أو جرى تجاهلها؛ وتُدمج headers المُعادة كالمعتاد، ويكون Location هو url. ومن دون body لا محتوى لإعادة التوجيه، ولا يُرسَل محتوى الخادم أبداً؛ وفي HTTP/2 لا تُرسَل مقطورات (trailers) الخادم أيضاً. يجب إرجاع url مع واحد على الأقل من status / headers / body؛ النتيجة التي تحتوي على url فقط تُعامل كتمرير.
قيم إرجاع سكريبت DNS:
$done({address: "1.2.3.4"}) // IP واحد
$done({addresses: ["1.2.3.4", "5.6.7.8"]}) // عدة IPs
$done({address: "10.0.0.1", ttl: 300}) // مع TTL مخصص (بالثواني، الافتراضي 60)
$done({server: "1.1.1.1"}) // تحليل DNS عبر هذا الخادم
$done({servers: ["tls://dns.google", "8.8.8.8#disable-ipv6"]})
لا يقدّم
server/serversإجابةً، بل يختار الخادم الذي يُجرى عبره تحليل DNS: كل مدخل هو مدخل خادم DNS عادي — عنوان أوtls://أوhttps://، مع خيارات#المعتادة — وتُسأل جميعها معاً، وتفوز أول إجابة. وإذا لم يكن أي مدخل صالحاً للاستخدام، يفشل البحث بدل الرجوع إلى المجموعة المُعدّة.
الترويسات ذات الأسطر المتعددة
قد تظهر الترويسة الواحدة على عدة أسطر: فالاستجابة التي تضبط ملفَّي تعريف ارتباط (cookies) تحمل سطرَي Set-Cookie. وتستطيع سكريبتات http-request وhttp-request-before-send وhttp-response قراءة كل سطر وكتابة عدة أسطر.
القراءة
افتراضياً، $request.headers و$response.headers كائنان يربطان كل اسم ترويسة بنص واحد. وإذا كان للترويسة عدة أسطر، دُمجت قيمها بالترتيب: قيم Cookie بـ "; "، وقيم كل ترويسة أخرى (ومنها Set-Cookie) بـ ", ".
$response.headers["Set-Cookie"] // "a=1; Path=/, b=2; Path=/"
$request.headers["Cookie"] // "a=1; b=2"
يحتوي تاريخ Expires في ملف تعريف الارتباط على فاصلة، لذا لا يمكن تقسيم نص Set-Cookie المدموج إلى ملفات تعريف ارتباط منفصلة بشكل موثوق. ولقراءة كل سطر على حدة، أضف full-header-mode=true إلى سطر السكريبت. عندئذ يصبح $request.headers و$response.headers مصفوفتين فيهما كائن {field, value} لكل سطر، وهي الصيغة التي يستخدمها Surge. وتحتفظ الأسطر ذات الاسم الواحد بترتيبها.
// full-header-mode=true
const cookies = $response.headers
.filter(h => h.field.toLowerCase() === "set-cookie")
.map(h => h.value) // ["a=1; Path=/", "b=2; Path=/"]
والأمر نفسه في HTTP/2: فإذا قسّم العميل ملف تعريف الارتباط على عدة حقول cookie، قرأها السكريبت ترويسةَ Cookie واحدة مدموجة بـ "; "، وكل set-cookie سطر مستقل.
الكتابة
لا تغيّر headers في $done() إلا الترويسات التي تسمّيها؛ وتبقى كل ترويسة أخرى كما هي. وتسري القواعد نفسها على headers داخل response الذي يجيب به سكريبت الطلب.
| القيمة | الأثر |
|---|---|
| نص | تحصل الترويسة على سطر واحد بالضبط بهذه القيمة؛ وتُستبدل كل أسطرها الموجودة |
| مصفوفة نصوص | سطر لكل قيمة، بالترتيب، بدلاً من أسطر الترويسة. {"Set-Cookie": ["a=1", "b=2"]} يرسل سطرَي Set-Cookie |
[] |
تُحذف الترويسة |
أي شيء آخر (رقم، null، كائن، مصفوفة فيها عنصر ليس نصاً) |
يُتجاهل: وتبقى الترويسة كما هي |
والنص المدموج الذي يعيده السكريبت كما قرأه تماماً يُبقي أسطر الترويسة الأصلية. لذا فإن const h = $response.headers; h["X-A"] = "1"; $done({headers: h}) لا يغيّر إلا X-A، ويخرج سطرا Set-Cookie سطرين كما كانا. وأي نص آخر يستبدل كل أسطر الترويسة بسطر واحد. ولإضافة ملف تعريف ارتباط، اكتب القائمة كاملة مصفوفةً: اقرأ الأسطر الموجودة في وضع full-header، ثم أعِد {"Set-Cookie": [...existing, "c=3"]}.
ويحمل الطلب سطر Cookie واحداً: فالمصفوفة المعطاة لـ Cookie في headers سكريبت http-request أو http-request-before-send تُدمج بـ "; "، فيرسل {"Cookie": ["a=1", "b=2"]} الترويسة Cookie: a=1; b=2.
ويقبل $done() أيضاً headers مصفوفةً من كائنات {field, value}، مع full-header-mode أو بدونه:
$done({headers: [{field: "Set-Cookie", value: "a=1"},
{field: "Set-Cookie", value: "b=2"},
{field: "X-A", value: "1"}]})
تُجمَع العناصر حسب الاسم، دون تمييز لحالة الأحرف، وتستبدل كل مجموعة أسطر تلك الترويسة بالترتيب المعطى. وتبقى الترويسات التي لا تسمّيها المصفوفة كما هي، فإغفال ترويسة لا يحذفها؛ ولحذف ترويسة استخدم صيغة الكائن مع []. ويُتجاهل العنصر الذي ليس له field نصي وvalue نصية.
- أسماء الترويسات لا تميّز حالة الأحرف:
set-cookieوSet-Cookieترويسة واحدة، فاذكر كل اسم مرة واحدة فقط. والاسم الذي ليس اسم ترويسة صالحاً يُسقط، كما هو موضح في$done. - القيمة التي تحتوي على فاصل سطر أو NUL تُسقط، كما هو موضح في
$done. وفي المصفوفة لا تُسقط إلا تلك القيمة؛ وإذا سقطت كل القيم، تبقى الترويسة كما هي. - في HTTP/2 يصبح كل سطر حقلاً مستقلاً باسم بأحرف صغيرة. ولا تُرسل الترويسات التي لا تخص إلا اتصالاً واحداً، مثل
ConnectionوKeep-Alive.
أسماء الترويسات في $request.headers و$response.headers
يجد $request.headers و$response.headers الترويسة أياً كانت حالة الأحرف التي كُتب بها اسمها: $response.headers['ETag'] و$response.headers['etag'] و$response.headers['Etag'] تقرأ كلها الترويسة نفسها. وينطبق هذا أيضاً على in ('etag' in $response.headers)، وعلى الإسناد (headers['content-type'] = 'text/html' يغيّر Content-Type الموجود في الكائن بدلاً من إضافة اسم ثانٍ له)، وعلى delete.
وحين تسرد الأسماء (Object.keys()، for…in، JSON.stringify()) تظهر كل ترويسة مرة واحدة، مكتوبة كما يخزّنها Chute — كل كلمة تبدأ بحرف كبير، أياً كانت الطريقة التي كتبها بها العميل أو الخادم: Content-Type، Etag، X-Api-Key، Www-Authenticate.
ولا يطابق الأسماء بهذه الطريقة إلا الكائن الذي تقرؤه من $request.headers أو $response.headers. أما النسخة التي تصنعها بنفسك، مثلاً بـ Object.assign({}, $request.headers) أو {...$response.headers}، فهي كائن عادي؛ فاستخدم فيها الكتابة المبيّنة أعلاه. ومع full-header-mode=true تكون headers مصفوفة من كائنات {field, value}، ويُكتب field بالطريقة نفسها، ولا ينطبق هذا البحث؛ فقارن field.toLowerCase() بدلاً من ذلك.
$httpClient — عميل HTTP غير متزامن
إجراء طلبات HTTP من داخل السكريبتات. يتم إلغاء جميع الطلبات عند $done() أو المهلة.
$httpClient.get(url, function(error, response, data) {
if (error) {
console.log("Request failed: " + error)
} else {
console.log("Status: " + response.status)
console.log("Response: " + data)
}
})
$httpClient.post(url, {headers: {...}, body: "...", timeout: 5}, callback)
$httpClient.put(url, options, callback)
$httpClient.del(url, options, callback)
$httpClient.head(url, options, callback)
$httpClient.options(url, options, callback)
$httpClient.patch(url, options, callback)
يمكن أن يحمل كائن الخيارات أيضاً policy، وهو اسم سياسة معرّفة في الملف: عندئذٍ يخرج الطلب عبر تلك السياسة بدلاً من المسار الافتراضي. والاسم الذي لا يعرّفه الملف يُسجَّل كتحذير ويسلك الطلب المسار الافتراضي. أما صيغة policy-descriptor المضمّنة في Surge فغير مدعومة: تُسجَّل كتحذير ويسلك الطلب المسار الافتراضي كذلك — عرّف السياسة في الملف ومرّر اسمها.
توقيع callback: callback(error, response, data)
error: نص الخطأ أوnullresponse:{status: Number, headers: Object}أوnull. ويجدheadersالترويسة أياً كانت حالة أحرف اسمها، ويسرد الأسماء كما يسردها$request.headers— كل كلمة تبدأ بحرف كبير، مثلContent-TypeوEtagوX-Api-Key— معpolicyأو بدونهdata: محتوى الاستجابة مفكوكاً نصاً بـ UTF-8؛ أما المحتوى الذي ليس UTF-8 صالحاً فيصل بوصفه كائن بايتات مثل.bodyBytes، لاnull. ولا تكونnullإلا حين لا يوجد محتوى
$httpClientمتوفر في سكريبتاتhttp-requestوhttp-responseوhttp-request-before-sendوcronوevent. وهو غير متوفر في سكريبتاتruleوdns.يمكن أن يكون للسكريبت الواحد 8 طلبات معلّقة على الأكثر، ولجميع السكريبتات معاً 16. والطلب الذي يتجاوز أياً من الحدّين لا يُرسل ولا يُستدعى الـ callback الخاص به أبداً؛ ويذكر السجل ذلك مرة واحدة في كل تشغيل. والاستجابة التي يزيد محتواها على 4 ميغابايت تُفشل الطلب.
$persistentStore — تخزين Key-Value
تخزين key-value دائم يبقى عبر إعادة تشغيل السكريبتات والعمليات؛ وكل السكريبتات تقرأ المفاتيح نفسها وتكتبها. وعلى Android يُحفظ في مساحة التخزين الخاصة بـ Chute ويُستبعد من النسخ الاحتياطية للجهاز.
$persistentStore.write(data, key) // تخزين قيمة
$persistentStore.read(key) // استرجاع قيمة
$persistentStore.remove(key) // إزالة قيمة
$notification — الإشعارات المحلية
نشر إشعارات نظام محلية. ولا يعرض Chute Apple TV أياً منها.
$notification.post("Title", "Subtitle", "Notification body text")
يحمل وسيط رابع اختياري خيارات Surge: url (الرابط الذي يُفتح عند النقر على الإشعار)، وaction (يُفترض open-url عند وجود url، وهو الإجراء الوحيد الذي تتعامل معه التطبيقات — أما غيره، مثل clipboard في Surge، فيُرفق دون أن يتعامل معه شيء)، وauto-dismiss (بالثواني). تُرفق هذه الخيارات بالإشعار، أما المفاتيح الأخرى فتُتجاهل مع تحذير. ويتعامل Chute iOS و Chute Mac و Chute Android مع url فتفتحه عند النقر على الإشعار. أما auto-dismiss فلا يتعامل معه إلا Chute Android، الذي يزيل الإشعار بعد ذلك العدد من الثواني؛ وتتركه تطبيقات Apple في مكانه.
$notification.post("Title", "", "Tap to open", {url: "https://example.com", "auto-dismiss": 5})
تتبع إشعارات السكريبتات — مثل إشعارات القواعد التي تحمل notification-text — مفتاح الإشعارات في التطبيق: Allow Notification في Chute iOS، وShow report event notifications في Chute Mac، وإذن الإشعارات الذي يمنحه النظام لـ Chute في Chute Android. أطفئه فلا يظهر شيء. وينشرها Chute Android في قناة إشعارات خاصة بها، Script Notifications، يمكن إيقافها على حدة من إعدادات الإشعارات في النظام. راجع تقارير الإشعارات.
$network — معلومات الشبكة
معلومات حالة الشبكة للقراءة فقط.
$network.dns // مصفوفة من عناوين IP لخوادم DNS
$network.wifi // {ssid: "WiFiName", bssid: null}
$network.v4 // {primaryAddress, primaryRouter}
$network.v6 // {primaryAddress, primaryRouter}
$network.primaryRouter // البوابة الافتراضية IPv4، أو null
$network.cellularData // {radio: "LTE" | "5G" | ..., carrier: null}
تُعبأ
ssidوbssidعلى iOS، حيث يستطيع النفق قراءة هوية Wi-Fi، وعلى Android حين يملك Chute الإذن الذي يشترطه Android لقراءة اسم الشبكة (انظر بدء الاستخدام على Android)؛ أما على macOS وtvOS فتكونssidفارغة وbssidتساويnull. ويتخطىprimaryAddressعنوان النفق نفسه والعناوين المحلية للوصلة، فهو العنوان الذي يصل به الجهاز إلى الشبكة. وcarrierيساويnullدائماً — فقد أزال iOS 16 اسم المشغّل.
$environment — معلومات وقت التشغيل
$environment.system // "iOS" أو "macOS" أو "Android"
$environment.appVersion // نص إصدار KLNEKit SDK
$environment.surgeVersion // إصدار المحرك نفسه، بالاسم الذي تقرأه سكريبتات Surge
$environment.language // اللغة المفضلة كوسم BCP 47، مثل "en-US"
$environment.deviceModel // "iPhone" أو "Mac" أو "AppleTV"؛ وعلى Android طراز الجهاز، مثل "Pixel 8"
$utils — أدوات مساعدة
$utils.geoip("1.2.3.4") // رمز الدولة (مثال: "US")
$utils.ipasn("1.2.3.4") // رقم ASN (مثال: "13335")
$utils.ipaso("1.2.3.4") // اسم المنظمة المالكة لـ ASN (مثال: "CLOUDFLARENET")
$utils.ungzip(data) // فك ضغط بيانات gzip
$klne — واجهة التحكم بالبروكسي
التحكم في وقت تشغيل البروكسي من السكريبتات. $klne متوفر في جميع أنواع السكريبتات.
$klne.getPolicyGroups() // الحصول على جميع مجموعات السياسات
$klne.selectGroupDetails() // المجموعات نفسها، بصيغة Surge
$klne.selectPolicy("Group", "Proxy") // تبديل سياسة لمجموعة
$klne.getActiveConnections() // سرد الاتصالات النشطة
$klne.closeConnection("id") // إغلاق اتصال بالمعرّف الذي أعادته getActiveConnections()
$klne.flushDNS() // مسح ذاكرة DNS المؤقتة
$klne.startURLTest("Group") // تشغيل اختبار رابط لمجموعة
$klne.reloadConfiguration() // إعادة تحميل جميع الإعدادات
$klne.setOutboundMode("rule") // تعيين الوضع: "global"/"proxy", "direct", "rule"
$klne.setHTTPCaptureEnabled(true) // تفعيل/تعطيل MitM
$klne.setRewriteEnabled(true) // تفعيل/تعطيل عائلة إعادة الكتابة
تُرجع getPolicyGroups() مجموعات السياسات القابلة للاختيار:
{
count: 1, // Number — عدد المجموعات
policyGroups: [{
name: "MainGroup", // String — اسم المجموعة
type: 0, // Number — 0 select, 1 url-test, 2 fallback, 3 ssid, 5 load-balance
policyNames: ["A", "B"], // Array of String — أعضاء المجموعة الفعليون، بما فيهم العقد القادمة من الاشتراكات
selectedIndex: 0, // Number — فهرس السياسة النشطة داخل policyNames (يغيب عندما تكون غير محسومة)
selectedPolicy: "A" // String — اسم السياسة النشطة (يغيب عندما تكون غير محسومة)
}]
}
تُرجع selectGroupDetails() المجموعات نفسها بصيغة Surge — إذ تربط policyGroups كل اسم مجموعة بأسماء أعضائها، وتربط decisions كل اسم مجموعة بالعضو المختار الآن:
{
policyGroups: {MainGroup: ["A", "B"]},
decisions: {MainGroup: "A"}
}
أما المجموعة التي يكون اختيارها غير محسوم فتُترك خارج decisions.
تُرجع getActiveConnections() مصفوفة (Array) تصف الاتصالات الحالية:
[{
id: 1042, // Number — رقم الاتصال، وهو ما يقبله closeConnection
host: "example.com", // String — المضيف الوجهة (يغيب عندما يكون مجهولاً)
port: 443 // Number — منفذ الوجهة
}]
أما بقية الدوال فتأخذ وسائط بسيطة ولا تُرجع شيئاً:
selectPolicy(group, policy)— تجعلpolicy(اسماً منpolicyNamesالخاصة بالمجموعة) السياسة النشطة لـgroup. اسم المجموعة أو السياسة غير المعروف يُتجاهل مع تحذير في السجل.closeConnection(id)— يغلق الاتصال ذا المعرّف المحدّد، وهو الرقم الذي يبلّغ عنهgetActiveConnections(). أما المعرّف غير المفتوح فيُتجاهل مع تحذير.flushDNS()— تمسح ذاكرة DNS المؤقتة.startURLTest(group)— تبدأ اختبار زمن استجابة غير متزامن لمجموعة من نوعurl-testأوfallbackأوload-balance؛ أنواع المجموعات الأخرى والأسماء غير المعروفة تُتجاهل مع تحذير.reloadConfiguration()— تعيد الآن جلب مصدر#!MANAGED-CONFIGالخاص بالملف، دون انتظار فترة التحديث، وتطبّقه إن اختلف. وإعادة تطبيق الإعدادات التي يحملها المحرك أصلاً لا تغيّر شيئاً، لذا يكتفي الملف الذي لا مصدر مُدار له بتسجيل تحذير.setOutboundMode(mode)—"global"و"proxy"كلاهما يوجّه كل الحركة عبر البروكسي، و"direct"يرسل كل الحركة مباشرةً، وأي قيمة أخرى تختار وضع القواعد.setHTTPCaptureEnabled(enabled)— تفعّل أو تعطّل فك تشفير HTTPS (MitM) أثناء التشغيل؛ تأخذ قيمة منطقية.setRewriteEnabled(enabled)— تفعّل أو تعطّل عائلة إعادة الكتابة بأكملها — إعادة كتابة الرابط وإعادة كتابة الترويسات وإعادة كتابة المحتوى والاستجابة الوهمية — أثناء التشغيل؛ تأخذ قيمة منطقية.
يُتاح $surge لسكريبتات Surge بالاستدعاءات ذات المعنى المطابق: $surge.setSelectGroupPolicy(group, policy) (مثل selectPolicy)، و$surge.selectGroupDetails() (مثل selectGroupDetails)، و$surge.setOutboundMode(mode)، و$surge.setHTTPCaptureEnabled(enabled)، و$surge.setRewriteEnabled(enabled) (عائلة إعادة الكتابة كاملةً: إعادة كتابة الرابط وإعادة كتابة الترويسات وإعادة كتابة المحتوى والاستجابة الوهمية)، و$surge.retestGroup(name). أما بقية $surge فلا مقابل لها هنا وتُقرأ undefined، ويمكن للسكريبت اختبار ذلك.
$httpAPI — جسر واجهة التحكم
استدعاء HTTP Control API الخاصة بالمحرك من سكريبت. $httpAPI متوفر في جميع أنواع السكريبتات.
$httpAPI("GET", "/api/status", null, function(result) {
console.log(result.statusCode) // Number — حالة HTTP
console.log(result.body.data) // Object — محتوى JSON بعد تحليله
})
$httpAPI("/api/status") // وسيط واحد: طلب GET لذلك المسار
$httpAPI("DELETE", "/api/dns/cache") // الطريقة والمسار
var result = $httpAPI("GET", "/api/status") // ويُعاد الكائن نفسه كذلك
والاستدعاء متزامن — إذ يُوجَّه الطلب داخل المحرك، ويُمرَّر كائن {statusCode, body} إلى الـ callback ويُعاد في الوقت نفسه. ويجب أن يبدأ path بـ /. ويُرسَل body النصي كما كُتب؛ أما أي قيمة أخرى فتُرمَّز JSON. ولأن الطلب لا يعبر المستمع أصلاً، فلا رمز وصول فيه ولا حاجة إلى تفعيل الواجهة. ويُرفض POST /api/scripts/run بالرمز 409 وwould_reenter — إذ سيحتاج إلى المحرك الذي يحتجزه السكريبت المستدعي — ويُعطي المسار الذي لا يجيب خلال 12 ثانية الرمز 504.
$script — بيانات السكريبت الوصفية
$script.name // اسم السكريبت من الإعدادات
$script.type // نص نوع السكريبت
$script.startTime // طابع زمني رتيب (ثوانٍ منذ مرجع إقلاع النظام، وليس Epoch)
$script.sessionID // يختلف مع كل تنفيذ، لربط حالة التشغيل الواحد
المتغيرات العامة لكل تنفيذ
يتم حقن المتغيرات التالية لكل تنفيذ سكريبت وهي خاصة بأنواع سكريبتات معينة.
$argument — وسيط السكريبت
القيمة النصية من معامل argument= في إعدادات السكريبت، أو null حين لا يكون للسكريبت وسيط. متوفر في: http-request, http-response, http-request-before-send, rule, dns, cron, event.
console.log("Argument: " + $argument)
$domain — نطاق DNS (لسكريبتات DNS فقط)
اسم النطاق الذي يتم الاستعلام عنه. متوفر فقط في سكريبتات dns.
var domain = $domain // مثال: "example.com"
$cronexp — تعبير Cron (لسكريبتات Cron فقط)
تعبير جدولة cron من إعدادات السكريبت. متوفر فقط في سكريبتات cron.
console.log("Schedule: " + $cronexp) // مثال: "*/30 * * * *"
$event — معلومات الحدث (لسكريبتات Event فقط)
معلومات عن الحدث المشغل. يطلق Chute الأحداث network-changed وengine-started وprofile-reloaded — راجع سكريبت الحدث.
console.log("Event: " + $event.name) // "network-changed"
قيمة $event.name هي الحدث الذي انطلق فعلاً. فالسكريبت الذي يحمل event-name= لا يعمل إلا لذلك الحدث الواحد، فيكون الاسم دائماً هو الذي أعلنه؛ أما السكريبت الذي لا يحمل event-name= فيعمل على الثلاثة جميعاً، و$event.name هو ما يميّز بينها.
console — التسجيل
console.log("Debug message") // سجل تفصيلي
console.warn("Warning message") // سجل تحذيري
console.error("Error message") // سجل تحذيري موسوم كخطأ JavaScript
تُقتطع كل رسالة عند 256 حرفاً، وتُسجَّل بيانات الاعتماد التي فيها — ترويسة Authorization أو Cookie، أو قيمة password= أو token= وما شابهها — على أنها <redacted>. ويكتب console.log على المستوى التفصيلي، فلا يظهر إلا حين يكون loglevel هو verbose؛ أما console.warn وconsole.error فيظهران على المستوى الافتراضي warning.
setTimeout(fn, seconds) — مؤقت
جدولة دالة للتشغيل بعد تأخير.
setTimeout(function() {
console.log("Delayed execution")
}, 2.5) // 2.5 ثانية
$scriptImport(subScriptPath) — محمل السكريبتات الفرعية
تحميل وتقييم ملف JavaScript آخر. تُدعم مسارات الملفات المحلية فقط؛ روابط http(s):// وfile:// مرفوضة. ($script محجوز لكائن بيانات السكريبت الوصفية.)
$scriptImport("/path/to/helper.js")
// تمرير البيانات بين السكريبتات باستخدام $persistentStore
تفاصيل أنواع السكريبتات
لا ترى أنواع سكريبتات HTTP الثلاثة طلب HTTP العادي إلا حين يصل إلى Chute عبر بروكسي HTTP الخاص به، ولا ترى طلب HTTPS إلا حين يُفك تشفير مضيفه — أضف المضيف إلى
hostnameفي فك تشفير HTTPS.
سكريبت طلب HTTP
ينفذ عند استلام ترويسات الطلب. يمكنه تعديل الرابط، الترويسات، والمحتوى قبل توجيه الطلب.
[Script]
ModifyHeaders = type=http-request, script-path=modify.js, pattern=^https://api\.example\.com
سكريبت استجابة HTTP
ينفذ عند استلام ترويسات الاستجابة. يمكنه تعديل الحالة، الترويسات، والمحتوى قبل الإرجاع إلى العميل.
[Script]
ModifyResponse = type=http-response, script-path=response.js, pattern=^https://api\.example\.com
السكريبت الذي يأخذ المحتوى لا يعمل إلا بعد وصول المحتوى كاملاً. في HTTP/1.x، إذا أغلق الخادم الاتصال قبل ذلك، فالمحتوى الذي لا يحمل Content-Length ولا ترميز chunked ينتهي بانتهاء الاتصال، فيكون كاملاً ويعمل السكريبت كالمعتاد. أما المحتوى المؤطَّر بـ Content-Length أو بترميز chunked الذي قطعه الإغلاق فلا يُسلَّم إلى السكريبت: بل يُرسَل إلى العميل كما وصل تماماً، ثم يُغلَق الاتصال. وتُطبَّق أولاً قاعدة إعادة كتابة المحتوى المطابقة للرسالة نفسها، فيرى السكريبت المحتوى بعد إعادة كتابته.
سكريبت ما قبل إرسال طلب HTTP
يُنفَّذ قبيل إرسال الطلب إلى الخادم الأعلى مباشرة. فإذا احتُجز المحتوى — لأن لهذا السكريبت requires-body=true، أو كانت قاعدة إعادة كتابة المحتوى أو سكريبت http-request يأخذ المحتوى — عمل السكريبت بعد وصول المحتوى كاملاً. أما الطلب الذي لا محتوى له، أو الذي يتجاوز محتواه max-size، فلا يُحتجز، والمحتوى الذي يتجاوز max-size أثناء احتجازه يمضي كما وصل: وفي الحالتين يعمل السكريبت عند خروج الترويسة بمحتوى فارغ، ويُتخطى السكريبت الذي له requires-body=true. مفيد لتعديل محتويات طلبات POST/PUT. ويعمل أخيراً: بعد أي قاعدة إعادة كتابة المحتوى وأي سكريبت http-request يتلقى المحتوى، ويرى المحتوى الذي أنتجاه. والمحتوى الذي يعيده السكريبت يحل محل محتوى الطلب الأصلي، الذي يُسقَط، ويُضبط Content-Length بحسبه.
[Script]
BeforeSend = type=http-request-before-send, script-path=before-send.js, pattern=^https://api\.example\.com, requires-body=true
سكريبت القاعدة
مطابقة قواعد مخصصة. يجب على السكريبت استدعاء $done({matched: true}) أو $done({matched: false}).
[Rule]
SCRIPT,MyRuleScript,DIRECT
[Script]
MyRuleScript = type=rule, script-path=rule.js
سكريبت DNS
تحليل DNS مخصص. يستقبل $domain ويرجع عنواناً (عناوين) محللة.
// dns.js
var domain = $domain
if (domain === "internal.example.com") {
$done({address: "10.0.0.1", ttl: 300})
} else {
$done({}) // تمرير إلى تحليل DNS العادي
}
يُرسل مدخل [Host] بالشكل <domain> = script:<name> عمليات البحث عن النطاقات المطابقة إلى سكريبت DNS المسمّى — راجع تعيين DNS المحلي.
سكريبت Cron
تنفيذ مجدول باستخدام تعبيرات cron. الحد الأدنى للفاصل هو 60 ثانية.
[Script]
HalfHourlyTask = type=cron, script-path=task.js, cron-expression=*/30 * * * *
يجب أن يكون التعبير خمسة حقول بالضبط تفصلها مسافات مفردة. وأي شيء غير ذلك — مسافة مزدوجة، أو محرف جدولة، أو أربعة حقول، أو ستة — يعطّل السكريبت بصمت: فلا يُجدوَل أبداً، ولا يُكتب شيء في السجل.
ولا يُحترم من الحقول الخمسة إلا حقل الدقائق: */N يعمل كل N دقيقة. وأي حقل دقائق آخر ينطلق كل 60 ثانية، ويجب على السكريبت التحقق من الوقت الحالي بنفسه ليقرر ما إذا كان سيتصرف.
سكريبت الحدث
يتم تشغيله بواسطة أحداث النظام. وتُطلق ثلاثة أحداث:
| الحدث | متى ينطلق |
|---|---|
network-changed |
تغيّرت شبكة Wi-Fi أو الجوال |
engine-started |
انتهى المحرك من البدء — فالسياسات والقواعد والمستمعون تعمل |
profile-reloaded |
انتهت إعادة تحميل الإعدادات، على سكريبتات ملف الإعدادات المُعاد تحميله نفسه |
[Script]
NetChange = type=event, script-path=network-changed.js
كائن $event متاح:
$event.name // "network-changed" أو "engine-started" أو "profile-reloaded"
يسمّي Surge الحدث بـ event-name=، ويقرؤه Chute أيضاً: فـ event-name=engine-started يشغّل السكريبت على ذلك الحدث وحده. أما السكريبت الذي لا يسمّي حدثاً فيعمل على الثلاثة جميعاً ويقرأ $event.name ليعرف أيها انطلق. والسكريبت المرتبط بأي حدث آخر — إذ يطلق Surge أيضاً أحداثاً مثل notification — فلا يعمل أبداً في Chute، ويذكر السجل ذلك عند تحميل الإعدادات.
أمثلة عملية
إعادة توجيه الأجهزة المحمولة
سكريبت http-request يعيد توجيه مستخدمي الجوال بناءً على User-Agent:
[Script]
MobileRedirect = type=http-request, script-path=mobile-redirect.js, pattern=^https://example\.com
// mobile-redirect.js
var ua = $request.headers["User-Agent"] || ""
if (/Mobile|Android|iPhone/.test(ua)) {
$done({
response: {
status: 302,
headers: {"Location": "https://m.example.com" + $request.url.replace(/.*example\.com/, "")},
body: ""
}
})
} else {
$done({})
}
حظر المحتوى في استجابات API
سكريبت http-response يزيل الإعلانات والمحتوى المدعوم من استجابة JSON API:
[Script]
RemoveAds = type=http-response, script-path=remove-ads.js, pattern=^https://api\.example\.com/feed, requires-body=true
// remove-ads.js
var body = JSON.parse($response.body)
if (body.ads) {
delete body.ads
}
if (body.recommendations) {
body.recommendations = body.recommendations.filter(function(r) {
return !r.sponsored
})
}
$done({body: JSON.stringify(body)})
تعديل محتوى الطلب قبل الإرسال
سكريبت http-request-before-send ينقي حمولة POST:
[Script]
SanitizePayload = type=http-request-before-send, script-path=sanitize.js, pattern=^https://api\.example\.com/submit, requires-body=true
// sanitize.js
var body = JSON.parse($request.body)
body.clientSecret = "[REDACTED]"
body.timestamp = Math.floor(Date.now() / 1000)
$done({body: JSON.stringify(body)})
قاعدة مخصصة: التوجيه حسب الوقت
سكريبت rule يختار بروكسي مختلف حسب وقت اليوم:
[Rule]
SCRIPT,TimeBasedRule,ProxyA
[Script]
TimeBasedRule = type=rule, script-path=time-rule.js
// time-rule.js
var hour = new Date().getHours()
if (hour >= 9 && hour < 18) {
$done({matched: false}) // السقوط إلى القاعدة التالية خلال ساعات العمل
} else {
$done({matched: true}) // استخدام ProxyA خارج ساعات العمل
}
DNS مخصص للنطاقات الداخلية
سكريبت dns يحل أسماء المضيفين الداخلية إلى عناوين IP محلية:
[Script]
InternalDNS = type=dns, script-path=internal-dns.js
// internal-dns.js
var internalHosts = {
"gitlab.local": "10.0.0.10",
"registry.local": "10.0.0.11",
"monitor.local": "10.0.0.12"
}
if (internalHosts[$domain]) {
$done({address: internalHosts[$domain], ttl: 3600})
} else {
$done({}) // تمرير إلى DNS العادي
}
التبديل التلقائي للسياسة عند تغيّر الشبكة
سكريبت event يفحص الاتصال بعد تغيّر الشبكة ويبدل مجموعة السياسات وفقاً لذلك:
[Script]
NetSwitch = type=event, script-path=network-switch.js, timeout=15
// network-switch.js
$httpClient.head("https://www.google.com/generate_204", {timeout: 5},
function(error, response, data) {
if (error) {
// فشل الفحص على الشبكة الجديدة — التبديل إلى المجموعة الاحتياطية
$klne.selectPolicy("MainGroup", "BackupProxy")
console.log("Probe failed after " + $event.name)
} else {
$klne.selectPolicy("MainGroup", "MainProxy")
}
$done({})
})
ملاحظة:
$networkمرئي لكل السكريبتات، لكنwifi.ssidلا يُملأ إلا على iOS وAndroid (انظر الملاحظة في $network). على macOS وtvOS يكون الاسم فارغاً، لذا فسكريبت الحدث أو cron الذي يجب أن يعمل في كل مكان يستخدم الفحص عبر$httpClientكما في الأعلى بدل قراءة SSID.
فحص دوري للصحة
سكريبت cron يتحقق من صحة البروكسي كل 30 دقيقة:
[Script]
HealthCheck = type=cron, script-path=health-check.js, timeout=15, cron-expression=*/30 * * * *
// health-check.js
$httpClient.head("https://www.google.com/generate_204", {timeout: 10},
function(error, response, data) {
if (error || response.status !== 204) {
console.error("Health check failed: " + (error || "status " + response.status))
$notification.post("Chute Alert", "Health Check", "Cannot reach Google")
} else {
console.log("Health check OK")
}
$done({})
}
)
استدعِ
$done({})داخل الـ callback — إكمال السكريبت يلغي جميع طلبات$httpClientالمعلقة، لذا فإن$done()متزامناً في نهاية السكريبت سيلغي فحص الصحة قبل وصول استجابته. وللسبب نفسه يجب أن تكون مهلةtimeoutالخاصة بالطلب أقصر من مهلة السكريبت: فإذا نفدت مهلةtimeoutالخاصة بالسكريبت (5 ثوانٍ افتراضياً) أولاً، يُلغى الطلب ولا يُستدعى الـ callback الخاص به أبداً. ويضبط المثالان أعلاهtimeout=15على سطر[Script].
إثراء استجابات API ببيانات خارجية
سكريبت http-response يثري بيانات المستخدمين باستدعاء API ثانوي:
[Script]
EnrichUsers = type=http-response, script-path=enrich.js, pattern=^https://api\.example\.com/users, requires-body=true, timeout=20
// enrich.js — 8 طلبات على الأكثر قيد التنفيذ، وهو حد السكريبت الواحد
var users = JSON.parse($response.body)
var next = 0
var pending = 0
function launch() {
while (pending < 8 && next < users.length) {
fetchAvatar(next++)
}
if (pending === 0) {
$done({body: JSON.stringify(users)})
}
}
function fetchAvatar(index) {
pending++
$httpClient.get("https://internal-api.example.com/avatar/" + users[index].id,
function(error, resp, data) {
if (!error && resp.status === 200) {
users[index].avatar = JSON.parse(data).url
}
pending--
launch()
}
)
}
launch()
نموذج التنفيذ
- تعمل جميع السكريبتات على طابور تسلسلي مخصص لسلامة الخيوط.
- كل تنفيذ سكريبت له مهلة خاصة به، تُحسب من بداية التنفيذ: تحميل المصدر (والسكريبت البعيد يُنزَّل ضمنها) وتشغيل الشيفرة وانتظار المؤقتات والطلبات كلها تتقاسمها. إذا لم يتم استدعاء
$done()خلال المهلة، يعامل السكريبت كتمرير. والسكريبت الذي ينتهي دون$done()وليس لديه مؤقّتsetTimeoutأو طلب$httpClientمعلّق يُعامَل تمريراً في الحال. - يُحتفظ بمجموعة من 3 سياقات مهيأة مسبقاً؛ يُتخلص من السياق بعد كل تنفيذ ويُستبدل بآخر جديد، لذا لا تتسرب المتغيرات العامة أبداً بين التشغيلات.
- يسجّل Chute استهلاك العملية للذاكرة حين يبدأ محرك السكريبتات. فإذا زاد بأكثر من 10 ميغابايت على iOS وtvOS، أو 512 ميغابايت على macOS، يُتخطى كل سكريبت وتمرّ رسالته؛ وعلى Android يكون المقياس ذاكرة Java heap المستخدمة، بهامش 512 ميغابايت. والهامش للعملية كلها لا لسكريبت واحد، ولا تُعاد نقطة البداية أثناء عمل Chute.
- يمكن أن تجري 16 عملية تنفيذ للسكريبتات على الأكثر في وقت واحد على iOS وtvOS وAndroid (64 على macOS)، ولا تزيد مصادر السكريبتات بينها على 4 ميغابايت (8 ميغابايت على macOS). والتنفيذ الذي يتجاوز أياً من الحدين يُتخطى وتمرّ رسالته؛ ويقول السجل
execution admission is full. - يمكن أن يصل حجم مصدر السكريبت إلى 1 ميغابايت على macOS و512 كيلوبايت على iOS وtvOS وAndroid. ولا حدّ لعدد السكريبتات التي يجوز لملف الإعدادات إعلانها؛ والمحدود هو عدد مصادر السكريبتات التي تُحمَّل في الوقت نفسه — 32 على macOS، و8 على iOS وtvOS وAndroid. وفوق هذا الحدّ يقول السجل
source load queue is fullوأن ذلك التنفيذ الواحد يعمل بلا مصدر، ما يعني أنه يمرّ. - السكريبتات البعيدة (مسارات HTTP/HTTPS) تُجلب عند كل تنفيذ. وقيمة
script-update-intervalالموجبة، أو-1بالضبط، تجعل Chute يستطلع الرابط أيضاً بطلب HEAD شرطي كل 10 دقائق. والقيمة مفتاح تشغيل لا فترة: فالاستطلاع كل 10 دقائق مهما قال الرقم. أما0(الافتراضي) وأي قيمة سالبة أخرى فتتركان الاستطلاع مطفأً.
دمج سكريبتات الوحدات
يمكن أيضاً تعريف السكريبتات في ملفات الوحدات (.sgmodule) تحت القسم [Script].
هذه الصفحة ترجمة للنسخة الإنجليزية. في حال وجود اختلاف، يُعتمد على النسخة الإنجليزية.
ملاحظة: التطبيق لا يدعم اللغة العربية حاليًا.