وثائق التطبيقات المصغّرة المفتوحة في TutuHai
دمج SDK · واجهات القدرات · بيانات السحابة · التفويض · النشر
التطبيقات المصغّرة في TutuHai هي تطبيقات خفيفة تعمل داخل TutuHai. يقوم المطوّرون برفع حزمة كود واجهة أمامية فقط، وتستضيفها المنصّة؛ وتتواصل القدرات مع TutuHai عبر SDK الخاص بـ window.tt، دون أي خادم خلفي خاص بك (تمرّ بيانات الأعمال عبر بيانات سحابة TutuHai). تكامل SDK · واجهات القدرات · البيانات السحابية · التفويض · النشر — كل ذلك على صفحة واحدة، مع أمثلة جاهزة للنسخ والتشغيل.
المحتويات · 26 موضوعات
البدء
قدرات البيانات
المحادثات واللعب الجماعي
الملفات والقرص السحابي
التفاعل بين التطبيقات
واجهة المستخدم · النافذة · المضيف
المرجع
البدء
مقدّمة
التطبيقات المصغّرة في TutuHai هي تطبيقات خفيفة تعمل داخل TutuHai. يقوم المطوّرون برفع حزمة كود واجهة أمامية فقط، وتستضيفها المنصّة؛ وتتواصل القدرات مع TutuHai عبر SDK الخاص بـ window.tt، دون أي خادم خلفي خاص بك (تمرّ بيانات الأعمال عبر بيانات سحابة TutuHai).
العزل والأمان: تعمل التطبيقات المصغّرة داخل iframe معزول ضمن أصل (origin) منفصل؛ ولا تدخل جلسة تسجيل دخول المضيف إلى التطبيق المصغّر أبداً. يُصدر المضيف لكل استدعاء رمزاً (token) قصير العمر ومقيّداً، ويعيد الخادم الخلفي التحقّق منه بحسب القدرة (scope).
بداية سريعة
- في وحدة تحكّم التطبيقات المصغّرة (
/applets)، انقر "جديد" لإنشاء تطبيق مصغّر (اسم + slug فريد). - اكتب ملف HTML واحداً (ضمّن SDK، واستدعِ القدرات عبر
window.tt.*). - أنشئ إصداراً ← املأ القدرات المطلوبة ← ارفع حزمة الكود (ملف HTML واحد).
- أرسِله للمراجعة ← يوافق المشرف ← انشره إلى الإنتاج بنقرة واحدة.
- يجده المستخدمون ويفتحونه عبر بحث "اكتشاف"، أو تشارك بطاقة / تنسخ رابطاً للوصول المباشر.
مواصفة التغليف
تدعم التطبيقات المصغّرة شكلين للرفع: ① حزمة HTML من ملف واحد (مكتفية ذاتياً، نقطة الدخول = الجذر، الأبسط)؛ ② ملف zip لمخرجات بناء إطار عمل حقيقي (المجلد dist/ الناتج عن npm run build، مع index.html + الأصول موزّعة على ملفات متعددة — انظر "مخرجات بناء إطار العمل"). تُجري المنصّة فحصاً للمواصفة وتحسيناً قبل الرفع.
بنية الحزمة (مخرجات البناء)
your-applet/ # dev directory (any structure: src, components, assets…)
├─ src/ … # your source (React / Vue / Svelte / vanilla)
└─ dist/index.html # ★build output: single-file HTML (← upload this)
# inlined CSS/JS, or referencing whitelisted CDNs; self-contained, no server
يجب أن تستوفي حزمة الكود ما يلي (يُفحص تلقائياً عند الرفع):
- نقطة الدخول: ملف HTML واحد يحتوي
<!doctype html>وجذر<html>. - الملاءمة للجوال: يجب أن يتضمّن
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">. - SDK: ضمّن
<script src="/applet-sdk.js">(تعيد المنصّة كتابته إلى العنوان المطلق للمضيف). - الاكتفاء الذاتي: ضمّن CSS/JS مباشرةً؛ وإذا احتجت سكربتات خارجية، فلا يُسمح إلا بـ SDK المنصّة + شبكات CDN المعروفة للأطر (unpkg / jsdelivr / cdnjs / esm.sh) — السكربتات البعيدة العشوائية ممنوعة (لأسباب أمنية). تمرّ الصور والوسائط الأخرى عبر
tt.uploadImageأو CDN. - الحجم: ملف HTML الواحد ≤ 1MB؛ ملف zip متعدد الملفات ≤ 8MB إجمالاً، ≤ 1MB لكل ملف، ≤ 100 ملف؛ الصور المرفوعة ≤ 4MB.
- البيانات: لا خادم خلفي خاص بك — تمرّ بيانات الأعمال عبر بيانات سحابة
tt.cloud/tt.*storage.
📱🖥 عالمي للجوال / سطح المكتب (قاعدة كود واحدة، سطحان)
تعمل الحزمة نفسها داخل iframe معزول داخل TutuHai؛ ويحملها المضيف على كلٍّ من الجوال (ملء الشاشة) وسطح المكتب (لوحة / يمكن الانتقال لملء الشاشة). اكتب قاعدة كود عالمية واحدة بـ تخطيط متجاوب: ① viewport-fit=cover + المناطق الآمنة env(safe-area-inset-*)؛ ② طبقات التراكب كأوراق سفلية (bottom sheets) (الجوال) ↔ في المنتصف (سطح المكتب، @media(min-width:480px))؛ ③ أهداف اللمس ≥ 44px؛ ④ اتّبع وضع المضيف الداكن/الفاتح (tt.onThemeChange / [data-theme])؛ ⑤ DOM خالص، بلا عروض مثبّتة (hard-coded). هذا يبقي التجربة متّسقة على الهاتف والحاسوب معاً.
مخرجات بناء إطار العمل (dist.zip)
إلى جانب HTML من ملف واحد، يمكنك أيضاً رفع مخرجات بناء إطار عمل حقيقي — استخدم React / Vue / Svelte / Angular / Solid / Astro / Next (تصدير ثابت) / vanilla… أي npm run build لأي سلسلة أدوات، اضغط المجلد dist/ (مع index.html + assets/*.js/css + الخطوط/الصور) في ملف zip وارفعه ليُستضاف.
مستقل عن إطار العمل: لا تتعرّف المنصّة إلا على "عقد حزمة ثابتة عالمي" واحد — نقطة الدخول
index.html+ مراجع أصول نسبية + SDK. أي إطار عمل يمكنه إنتاجdistثابت يستوفي ذلك العقد مدعوم؛ والقوالب أدناه ليست إلا اختصارات منتقاة، لا حدّاً للدعم.
بنية zip (مخرجات البناء، جذر zip = جذر الحزمة)
myapp.zip
├─ index.html # ★entry (zip root)
├─ manifest.json # declares slug/name/version/scopes (see below)
└─ assets/
├─ index-*.js # built JS (relative references)
├─ index-*.css
└─ font/img…
تكييف من ثلاث خطوات (يعمل مع أي إطار عمل):
- اضبط أساساً نسبياً (موصى به، الأكثر أماناً) — اجعل المخرجات تشير إلى الأصول بمسار نسبي (
./assets/x.jsبدلاً من المطلق من الجذر/assets/x.js)، ليكون الاستضافة تحت/<slug>/مضمونة. (يعمل الأساس الافتراضي أيضاً: تعيد المنصّة تلقائياً كتابة المراجع الثابتة المطلقة من الجذر في HTML/CSS، وتستخدم احتياطي Referer للأصول المطلقة من الجذر المولّدة أثناء التشغيل — مثل التحميل المسبق لـ CSS المقسّم؛ لكن ضمن حالاتReferrer-Policyالصارمة / الجلب المسبق دون اتصال قد يكون Referer مفقوداً فيفشل الاحتياطي، لذا الأساس النسبي هو الأكثر أماناً.) - أضف manifest — ضع
manifest.jsonفي المجلد الثابت (مثلstatic/في Vite/SvelteKit، أوpublic/في معظم الأطر) ليصل إلى جذرdistبعد البناء؛ أو احذف الـ manifest وأضف<meta name="tt:slug" content="…">(بالإضافة إلىtt:name / tt:version / tt:scopes) فيindex.htmlكاحتياطي. - ضمّن SDK — بطريقتين: ① التثبيت عبر npm (موصى به، الأفضل للقوالب الحقيقية): بعد
npm i @tutuhai/applet-sdk، استخدمimport { tt } from '@tutuhai/applet-sdk'— يُدمج في المخرجات وقت البناء، مع أنواع TypeScript، دون تعديلindex.html؛ ② أو اكتب<script src="/applet-sdk.js">فيindex.html(تعيد المنصّة كتابته إلى العنوان المطلق للمضيف) واستخدمwindow.ttالعام.
📦 SDK عبر npm (مُختبَر مع قوالب React / Vue / Svelte الرسمية)
أنشئ مشروعاً بـ npm create vite@latest -- --template react-ts | vue-ts | svelte-ts، ثبّت الحزمة نفسها @tutuhai/applet-sdk، واستوردها بـ import — مصدر حقيقي متعدد الملفات، وينتج npm run build عدّة أجزاء (chunks) + نقطة دخول واحدة index.html؛ اضغطها في zip وارفعها.
# 1) Create a project with an official scaffold
npm create vite@latest my-applet -- --template react-ts # or vue-ts / svelte-ts
# 2) Install the SDK (same package for all three frameworks)
npm i @tutuhai/applet-sdk
# 3) import and use in your source (typed)
# src/App.tsx / App.vue / App.svelte
import { tt } from '@tutuhai/applet-sdk';
tt.ready((ctx) => {
tt.getProfile().then((me) => console.log('hi', me?.nickname));
});
await tt.cloud.add('notes', { text: 'hello' }); // cloud data, no backend of your own
# 4) vite.config: relative base; public/manifest.json declares slug/name/scopes
# export default { base: './', plugins: [react()] }
# 5) official build → zip dist → upload
npm run build && cd dist && zip -r ../my-applet.zip .
أمثلة قابلة للتشغيل في المستودع: applets/frameworks/{react,vue,svelte} (ثلاثة مشاريع حقيقية بقوالب رسمية، جميعها تستورد حزمة SDK نفسها بـ import). مصدر حزمة SDK: applet-sdk/.
حقول manifest.json
{
"slug": "myapp", // ★required, globally unique, determines hosting path /myapp/
"name": "My Mini-App", // ★required, display name
"version": "1.0.0", // ★required, must increment on every upload
"scopes": ["user.profile"], // requested capabilities (see "Authorization model")
"description": "One-line summary", // optional, shown on discover/detail
"icon": "icon.png", // optional, relative path in the bundle (or change it in the console after upload)
"display": "fullscreen", // optional, default open mode: window (floating, default) | fullscreen
"fileHandlers": [ // optional, declares "Open with" — which file kinds you can handle from a chat
{ "kinds": ["image"], "role": "editor", "label": "TutuEdit · Retouch" }
]
}
- display: يعلن شكل الفتح على سطح المكتب — لتطبيقات الرسم/اللوح الأبيض/المحرّر يُفضّل
fullscreen، وللبطاقات/النماذج الخفيفة استخدم الافتراضيwindow. إنه قيمة أوّلية فقط؛ فبعد النشر يمكنك تغيير "وضع الفتح" في أي وقت في وحدة التحكّم (تكون الغلبة لوحدة التحكّم). الجوال دائماً ملء الشاشة، لا يتأثّر بهذا الحقل. عند حذف الـ manifest، يعمل<meta name="tt:display" content="fullscreen">كاحتياطي. - fileHandlers: يعلن أنواع الملفات التي يمكن لتطبيقك المصغّر التعامل معها من محادثة — عندما ينقر مستخدم "الفتح باستخدام" على ملف في محادثة، تظهر التطبيقات المصغّرة التي أعلنت نوعاً مطابقاً كمرشّحين؛ والنقر على أحدها يرسل ذلك الملف مباشرةً إلى تطبيقك المصغّر (انظر "التعامل مع ملفات المحادثة"). كل عنصر:
kindsواحد منimage / video / audio / pdf / office / text / any(يُسمح بأكثر من واحد، وany= أي ملف)؛ وroleهوeditor(فتح في محرّر) أوviewer(معاينة)؛ وlabelاختياري (≤20 حرفاً، الاسم المعروض للمرشّح). حتى 8 عناصر. البوّابة تطابق الرؤية:private/self-useيعمل دون مراجعة (يظهر فقط في "الفتح باستخدام" الخاص بك)؛ وpublicيتطلّب موافقة المشرف قبل أن يسري على الجميع.
إعداد "الأساس النسبي" بسطر واحد لكل إطار عمل:
// Vite (React/Vue/Svelte/Solid/Preact/Lit…)
export default { base: './' }
// SvelteKit (client-side routing → base must = slug) — static export + base set to your slug (else routes 404)
import adapter from '@sveltejs/adapter-static';
export default { kit: {
adapter: adapter({ fallback: 'index.html' }),
paths: { base: '/your-slug', relative: true }
} };
// Astro — astro.config.mjs
export default { base: './', build: { assets: 'assets' } }
# Angular — set a relative base href at build time
ng build --base-href ./ --output-path dist
// Next.js (static export) — next.config.js
module.exports = { output: 'export', images: { unoptimized: true }, assetPrefix: './' }
// Nuxt 3 (static) — nuxt.config.ts
export default defineNuxtConfig({ app: { baseURL: './', cdnURL: './' }, ssr: false })
// Vue CLI / webpack — vue.config.js (or webpack output.publicPath)
module.exports = { publicPath: './' }
<!-- Vanilla / no build: just use relative paths -->
<script src="./app.js"></script>
<link rel="stylesheet" href="./style.css">
⚠ تطبيقات SPA ذات التوجيه من جهة العميل (SvelteKit / React Router / Vue Router / Angular) تُستضاف التطبيقات المصغّرة تحت المسار الفرعي
/<slug>/. يجب أن يضبط تطبيق SPA ذو التوجيه من جهة العميل "أساس الموجِّه (router base)" إلى slug الخاص بك، وإلا فلن يستطيع موجِّه إطار العمل مطابقة المسار الحالي ← خطأ 404 للصفحة كاملة (تُحمَّل الأصول، لكن التوجيه يُبلّغ عن عدم العثور). ضبط أساس أصول نسبي فقط لا يكفي — فهو يُصلح عناوين الأصول فقط، لا التوجيه. حسب إطار العمل: SvelteKitkit.paths.base='/<slug>'؛ React Router<BrowserRouter basename="/<slug>">؛ Vue RoutercreateWebHistory('/<slug>/')؛ AngularAPP_BASE_HREF='/<slug>/'. (لا تتأثّر التطبيقات بلا توجيه من جهة العميل — العرض الخالص / صفحة React واحدة بلا Router / vanilla.)
⚠ فحص الرفع عند الرفع تُجري المنصّة "فحص عقد" على الـ dist: نقطة الدخول / المسارات النسبية / manifest / مرجع SDK / الحدود / MIME، ويُتحقّق من كلٍّ منها مع تلميحات مضمّنة. الإجمالي ≤ 8MB، ≤ 1MB لكل ملف، ≤ 100 ملف، وأنواع MIME المدرجة في القائمة البيضاء فقط (html/css/js/json/الصور/الخطوط/map/wasm). اضغط الخطوط وجزّئها (subset) لإبقاء الحجم منخفضاً. أما مخرجات الأطر الثقيلة (مثل tldraw / excalidraw بجزء واحد >1MB) التي تتجاوز حدود الملف الواحد/الإجمالي الافتراضية، فاطلب من فريق التشغيل رفع حدَّي "بايتات الملف الواحد" / "الإجمالي غير المضغوط" في لوحة الإدارة (قابل للتغيير أثناء التشغيل، يسري فوراً)؛ كما أن تقسيم
manualChunksقد يُنزل vendor تحت الحد.
مثال بسيط
تطبيق مصغّر كامل قابل للتشغيل — ضمّن SDK، واقرأ اسم المستخدم المستعار:
<!doctype html>
<html>
<body>
<div id="who">Loading…</div>
<!-- Relative path — maintenance-free: survives domain changes/blocks (platform rewrites to the host's absolute URL) -->
<script src="/applet-sdk.js"></script>
<script>
window.tt.ready(function () {
window.tt.getProfile().then(function (me) {
document.getElementById('who').textContent = 'Hi, ' + me.nickname;
});
});
</script>
</body>
</html>
تكامل SDK
ضمّن سكربت SDK في HTML تطبيقك المصغّر، ثم استخدم window.tt:
<!-- Include the SDK in your mini-app HTML. Use a relative path — don't hardcode a domain -->
<script src="/applet-sdk.js"></script>
لا تثبّت نطاق المضيف مطلقاً داخل تطبيقك المصغّر. اكتب المسار النسبي
/applet-sdk.js(أو أي أصل بديل placeholder) — تعيد المنصّة كتابة عنوان سكربت SDK إلى المضيف الحالي وقت التقديم عند تشغيل تطبيقك في iframe الخاص به. لذا حتى لو غيّرت TutuHai نطاقها، أو حُجب نطاق، يبقى كل تطبيق مصغّر منشور يعمل دون أي تغيير في الكود ودون إعادة نشر — يقلب مُشغّل قيمة إعداد واحدة.
استدعاء الجهوزية (ready) والسياق والسمة/اللغة:
// After ready you get the context (appId / granted scopes / deep-link path / query / theme / locale / whether inline)
window.tt.ready(function (ctx) {
console.log(ctx.appId, ctx.scopes, ctx.path, ctx.query, ctx.theme, ctx.colorScheme, ctx.locale, ctx.inline);
});
window.tt.context(); // get the current context snapshot anytime (same as ready's ctx)
// Theme switching (fires live when the host toggles light/dark)
window.tt.onThemeChange(function (theme) {
document.documentElement.setAttribute('data-theme', theme);
});
// Locale switching (synced with the host's i18n; fires live when the host changes language — same mechanism as theme)
window.tt.onLocaleChange(function (locale) { // e.g. 'zh-CN' / 'en-US'
document.documentElement.setAttribute('lang', locale); // the SDK sets it already; you can also localize your own copy
});
أمثلة الأطر
window.tt مستقل عن إطار العمل ويعمل مباشرةً في كل الأطر الرئيسية (ملف واحد، بلا بناء). يتضمّن كل مثال معالجة كلٍّ من النجاح ✅ والفشل ❌ (رُفض التفويض / خطأ في الشبكة):
Vanilla JS
// No framework — vanilla DOM
window.tt.ready(function () {
window.tt.getProfile()
.then(function (me) { // ✅ success
document.getElementById('who').textContent = 'Hi, ' + me.nickname;
})
.catch(function (err) { // ❌ failure (user denied authorization / network error)
document.getElementById('who').textContent = 'Failed: ' + err.message;
});
});
React
// React 18 + htm (no build)
const { useState, useEffect } = React;
function App() {
const [me, setMe] = useState(null);
const [err, setErr] = useState('');
useEffect(() => {
window.tt.ready(() =>
window.tt.getProfile().then(setMe).catch((e) => setErr(e.message))
);
}, []);
if (err) return html`<div>Failed: ${err}</div>`; // ❌
return html`<div>Hi ${me ? me.nickname : '…'}</div>`; // ✅
}
Preact
// Preact + htm (no build)
const { useState, useEffect } = preactHooks;
function App() {
const [me, setMe] = useState(null), [err, setErr] = useState('');
useEffect(() => {
window.tt.ready(() =>
window.tt.getProfile().then(setMe).catch((e) => setErr(e.message))
);
}, []);
return html`<div>${err ? 'Failed: ' + err : 'Hi ' + (me ? me.nickname : '…')}</div>`;
}
Vue 3
// Vue 3 (CDN)
const { createApp, ref, onMounted } = Vue;
createApp({
setup() {
const me = ref(null), err = ref('');
onMounted(() => window.tt.ready(() =>
window.tt.getProfile()
.then((p) => (me.value = p)) // ✅
.catch((e) => (err.value = e.message)) // ❌
));
return { me, err };
},
template: `<div>{{ err ? 'Failed: ' + err : 'Hi ' + (me?.nickname ?? '…') }}</div>`
}).mount('#app');
Svelte
// Svelte (runtime compile)
let me = $state(null), err = $state('');
window.tt.ready(() =>
window.tt.getProfile()
.then((p) => (me = p)) // ✅
.catch((e) => (err = e.message)) // ❌
);
// template: <div>{err ? 'Failed: ' + err : 'Hi ' + (me?.nickname ?? '…')}</div>
Solid
// SolidJS
import { createSignal, onMount } from 'solid-js';
function App() {
const [me, setMe] = createSignal(null), [err, setErr] = createSignal('');
onMount(() => window.tt.ready(() =>
window.tt.getProfile().then(setMe).catch((e) => setErr(e.message))
));
return <div>{err() ? 'Failed: ' + err() : 'Hi ' + (me()?.nickname ?? '…')}</div>;
}
Alpine.js
<!-- Alpine.js: declarative in HTML, zero build -->
<div x-data="{ me: null, err: '' }"
x-init="window.tt.ready(() =>
window.tt.getProfile()
.then(p => me = p) /* ✅ */
.catch(e => err = e.message))"> <!-- ❌ -->
<span x-text="err ? 'Failed: ' + err : 'Hi ' + (me?.nickname ?? '…')"></span>
</div>
Lit
// Lit (Web Components)
import { LitElement, html } from 'lit';
class MyApp extends LitElement {
static properties = { me: {}, err: {} };
connectedCallback() {
super.connectedCallback();
window.tt.ready(() =>
window.tt.getProfile()
.then((p) => (this.me = p)) // ✅
.catch((e) => (this.err = e.message)) // ❌
);
}
render() {
return html`<div>${this.err ? 'Failed: ' + this.err : 'Hi ' + (this.me?.nickname ?? '…')}</div>`;
}
}
customElements.define('my-app', MyApp);
jQuery
// jQuery
$(function () {
window.tt.ready(function () {
window.tt.getProfile()
.then(function (me) { $('#who').text('Hi, ' + me.nickname); }) // ✅
.catch(function (err) { $('#who').text('Failed: ' + err.message); }); // ❌
});
});
Angular
// Angular (component)
@Component({ selector: 'app-root', template: `<div>{{ msg }}</div>` })
export class AppComponent implements OnInit {
msg = 'Loading…';
ngOnInit() {
const tt = (window as any).tt;
tt.ready(() =>
tt.getProfile()
.then((me: any) => (this.msg = 'Hi, ' + me.nickname)) // ✅
.catch((e: any) => (this.msg = 'Failed: ' + e.message)) // ❌
);
}
}
الأمثلة الكاملة القابلة للتشغيل موجودة في المستودع: applet-platform/samples/demo-react.html, demo-svelte.html, demo-vue.html.
قدرات البيانات
الملف الشخصي للمستخدم · user.profile
// Get the current user's profile (needs user.profile; on first call the host prompts for authorization as needed)
try {
const me = await window.tt.getProfile(); // ✅ success
console.log(me.userId, me.nickname, me.avatarUrl);
} catch (err) { // ❌ failure
// err.message: "User denied authorization" (tapped deny) / network error
console.warn('Failed to get profile:', err.message);
}
// Profile changes (you or someone in the room changed nickname/avatar) → re-fetch and refresh display
window.tt.onProfileChange(() => refreshWhoUI());
البيانات السحابية · cloud.data
مجموعات (collections) مبنيّة (structured) ومستضافة تتيح للتطبيق المصغّر حفظ بيانات الأعمال دون أي خادم خلفي خاص به. ثلاث مستويات رؤية:
mine: قراءة/كتابة مستنداتك الخاصة فقط (الافتراضي).all: قراءة/كتابة كل شيء، لمطوّر التطبيق المصغّر (المالك) فقط — لـ "وحدة تحكّم تاجر" لرؤية كل الطلبات/التذاكر.public: يمكن لأي مستخدم مسجَّل الدخول قراءة كل شيء، ويجب أن يبدأ اسم المجموعة بـpub_— للمجتمع/السوق/المنتدى؛ ولا تزال الكتابة والتعديل مقصورَين على المؤلّف.
// Cloud data: no backend of your own, business data hosted by the TutuHai platform. All calls return a Promise — always handle failure.
try {
// Create a document (owned by the current user)
const { id } = await window.tt.cloud.add('orders', { items: cart, total: 68, status: 'pending' });
// Idempotent upsert: create or update by docKey (most common for "one vote per person" / one record per user, avoids fetch-id-then-update)
await window.tt.cloud.put('votes', me.userId, { choice: 'A' });
// My documents
const mine = await window.tt.cloud.list('orders', { scope: 'mine' });
// Read one (by id; returns null if not found)
const doc = await window.tt.cloud.get('orders', id);
// All documents (developer/owner only, for the merchant console; regular users → 403)
const all = await window.tt.cloud.list('orders', { scope: 'all' });
// Public collection: name starts with pub_ → any logged-in user can read everything (community/marketplace)
const posts = await window.tt.cloud.list('pub_posts', { scope: 'public' });
// where equality filter (server filters on a single data field; on large collections it narrows by parent key to avoid child docs being cut off by the 200 cap)
const votes = await window.tt.cloud.list('pub_votes', { scope: 'public', where: { pollId: id } });
// Beyond 200 rows: listPage cursor pagination (mine/all; can take where), returns { docs, nextCursor }
const pg = await window.tt.cloud.listPage('orders', { scope: 'mine', limit: 100, before: cursor });
// Field-level update (owner or developer; patch merges with the original data)
await window.tt.cloud.update('orders', id, { status: 'done' });
// Delete a document (owner or developer; idempotent) — completes CRUD, no need to pile up soft-delete flags
await window.tt.cloud.delete('orders', id);
} catch (err) { // ❌ failure
// Insufficient permission (403) / using public on a non-pub_ collection (400) / quota exceeded / network
console.warn('Cloud data error:', err.message);
}
يُقرأ كل صف مرّةً أخرى بالشكل { id, ownerId, mine, data:{…your fields}, createdAt, updatedAt } — كل حقولك في data (مثل row.data.title).
تخزين KV · storage.kv
مفتاح-قيمة معزول لكل (تطبيق مصغّر، مستخدم)، جيّد للحالة الخاصة الصغيرة مثل عدّادات تسجيل الحضور والمسوّدات وغيرها. tt.cloudStorage (setItem/getItem/getKeys/removeItem) هو اسمه المستعار بأسلوب Telegram.
// Hosted KV (needs storage.kv): isolated per (mini-app, user), stores private state
try {
await window.tt.setStorage('count', 3);
const n = await window.tt.getStorage('count'); // 3 (returns null if absent)
await window.tt.removeStorage('count');
const keys = await window.tt.getStorageKeys();
} catch (err) { // ❌ quota (≤64 keys / 8KB) / network
console.warn('Storage failed:', err.message);
}
// Telegram-style alias (same as above, needs storage.kv):
await window.tt.cloudStorage.setItem('draft', 'unsent content');
const draft = await window.tt.cloudStorage.getItem('draft'); // returns null if absent
const ks = await window.tt.cloudStorage.getKeys();
await window.tt.cloudStorage.removeItem('draft');
المحادثات واللعب الجماعي
قدرات المحادثة · im.share / im.send / im.read / media.upload
كل تفاعل مع محادثات TutuHai يتوسّطه المضيف (يختار المستخدم محادثةً بفعّالية)؛ ولا يمكن للتطبيقات المصغّرة الحصول على قائمة المحادثات الكاملة. im.read قدرة حسّاسة.
// Conversation capabilities are all host-mediated (the user actively picks a conversation); always handle "user cancelled" and failure.
try {
// Share this mini-app's card to a conversation (needs im.share; if no conversation is passed the host shows a picker)
await window.tt.shareToChat({ title: 'Come vote for lunch 🍜' });
// Send a text notification to a conversation (needs im.send; the host shows a picker + preview, signed "via the X mini-app")
await window.tt.sendMessage('Vote result: Lanzhou beef noodles win');
// Read conversation messages (needs im.read, sensitive; the user picks a conversation each time, non-text is redacted)
const r = await window.tt.readMessages({ limit: 30 });
// Upload an image (needs media.upload; pass a dataURL, returns an absolute URL)
const url = await window.tt.uploadImage(dataUrl);
} catch (err) { // ❌ failure
// "User cancelled" (picker/preview cancelled) / authorization denied / "Call timed out" / network
console.warn('Capability call failed:', err.message);
}
غرف اللعب الجماعي · im.room
حوّل "محادثة" إلى غرفة زمن حقيقي للألعاب المصغّرة / التعاون: أنشئ/انضمّ إلى غرفة، مع رسائل دائمة داخل الغرفة وإشارات زمن حقيقي (مزامنة حالة، ≤2KB، عابرة، غير محفوظة). يجسّر المضيف أطر الزمن الحقيقي نيابةً عنك، ولا يدخل JWT الخاص بالمضيف إلى التطبيق المصغّر أبداً؛ ويقتصر ذلك على الغرف التي أنشأها هذا التطبيق المصغّر / المحادثات التي شُوركت فيها — ولا يمكنه المساس بمحادثات المستخدم الخاصة الأخرى.
// Multiplayer rooms (needs im.room): create/join/leave + in-room persistent messages + realtime signals (≤2KB, ephemeral, not persisted).
const { conversationId } = await window.tt.room.create({ title: 'Gomoku match' }); // create room, host auto-subscribes
await window.tt.room.join(conversationId); // idempotent join; host auto-subscribes to realtime frames
await window.tt.room.subscribe(conversationId); // subscribe to an existing conversation's realtime frames (e.g. a group you were shared into)
await window.tt.room.send(conversationId, 'Game on!'); // persistent text (visible even without opening the mini-app)
window.tt.room.signal(conversationId, { type:'move', cell:4 }); // send a realtime signal (state sync)
window.tt.room.setTyping(conversationId, true); // typing state (transient)
const members = await window.tt.room.members(conversationId); // roster [{userId,nickname,avatarUrl,online,isOwner}]
const past = await window.tt.room.history(conversationId, { limit: 50 }); // hydrate on reconnect (ascending)
await window.tt.room.leave(conversationId); // leave (empty rooms are auto-reclaimed)
// Realtime events (all under tt.room):
window.tt.room.onMessage((m) => appendMsg(m)); // new message {conversationId,id,senderId,senderName,kind,text,createdAt}
window.tt.room.onSignal((s) => applyMove(s.payload));// opponent's realtime action {conversationId,senderId,payload}
window.tt.room.onPresence((p) => refreshOnline(p)); // online/offline {userId,online}
window.tt.room.onTyping((t) => showTyping(t)); // typing {conversationId,userId,typing}
window.tt.room.onMember(() => reloadMembers()); // member joined/left {conversationId} → re-fetch members()
window.tt.room.onReconnect(() => rehydrate()); // dropped & reconnected → re-hydrate current state from history()/cloud
الترطيب عند إعادة الاتصال: الإشارات هي بذل أفضل جهد (best-effort)، وفقدان الأطر عند قطع الاتصال أمر طبيعي. عند
onReconnect، أعِد ترطيب الحالة النهائية منroom.history()أو البيانات السحابية — ولا تعتمد على الإشارات كمصدر وحيد للحقيقة.
المكوّنات المضمّنة (inline) · شبيهة بالمكوّن الأصلي · آمنة للخصوصية
بطاقة تُرسَل بـ shareToChat({inline:true}) تعرض مكوّناً تفاعلياً داخل فقاعة المحادثة مباشرةً (مثل استطلاع أو تقييم)؛ ويشغّله المستلمون كميزة أصلية دون فتح نافذة عائمة. يعرض التطبيق المصغّر واجهةً مدمجة استناداً إلى ctx.inline؛ وتتحجّم الفقاعة تلقائياً حسب المحتوى وتعيد تلوين نفسها فوراً مع وضع المضيف الفاتح/الداكن. الخصوصية: تحصل النسخة المضمّنة على رمز مقيّد "cloud.data فقط، بلا آثار جانبية" — يمكنه قراءة المجموعات العامة + كتابة مستنداته الخاصة، ولا يمكنه المساس ببيانات الآخرين الخاصة، ولا يطلب تفويضاً.
// —— Inline components: make a mini-app interact right in the chat bubble like a native feature (polls/ratings/relays…) ——
// 1) Send an inline card to a conversation (needs im.share): recipients operate it in the bubble without opening the mini-app
await window.tt.shareToChat({ inline: true, query: { pollId }, title: 'Poll', height: 200 });
// 2) The mini-app renders two forms based on ctx.inline
window.tt.ready((ctx) => {
if (ctx.inline) {
renderCompact(ctx.query.pollId); // inline: a compact "native-component-like" UI
window.tt.reportHeight(); // report height (the SDK also auto-reports via ResizeObserver; the bubble auto-sizes to content)
// When full functionality is needed, open the full page (floating/fullscreen) from the inline card:
// openBtn.onclick = () => window.tt.openFullPage('/detail?pollId=' + ctx.query.pollId);
} else {
renderFull(); // floating window: full creation UI
}
});
// Privacy: an inline instance gets only a "cloud.data only, no side effects" restricted token — it can read public collections + write its own documents,
// can't touch others' private data; doesn't prompt for authorization or pollute the authorization list. Sensitive capabilities (upload/send/location) are unavailable inline.
// Dark/mobile: inline cards switch light/dark live with the host and auto-fit width — no extra work for the developer.
البحث عن الكلمات / الترجمة · text.lookup / text.provider
النافذة المنبثقة للبحث عن الكلمات هي نفسها الصفحة المضمّنة لـ "تطبيق مصغّر مزوِّد" — محتواها/وظائفها كلها يعرضها ذلك التطبيق المصغّر؛ ولا يوفّر المضيف إلا التحديد + تحديد موضع التمرير (hover) + SDK مرن. المستهلكون (لجعل النص في تطبيقك المصغّر قابلاً للتحديد): أعلن text.lookup، بلا كود — يؤدي تحديد النص إلى ظهور الصفحة المضمّنة للمزوّد أسفل التحديد مباشرةً (نص رسائل المحادثة مدعوم أيضاً). المزوّدون (لبناء تطبيق مصغّر للبحث): أعلن text.provider (يُمنح بعد المراجعة)، وتتلقّى الصفحة المضمّنة الكلمات عبر tt.text.onLookup وتعرض نفسها؛ ويُضبط المزوّد النشط في لوحة الإدارة — وإن لم يُضبط/يُفوَّض أي مزوّد، يُعطَّل البحث. أضف data-tt-no-lookup لاستبعاد منطقة ما.
// ── A. Make "text inside your mini-app" selectable for lookup (consumer, needs text.lookup) ──【zero code】
// After declaring "text.lookup" in the manifest, when a user selects text in your mini-app, a popover
// 【the provider mini-app's inline page】pops up right below the selection (auto dictionary/translation). Chat message text is also supported (host built-in).
// Tap blank/scroll/Esc to hide; operating inside the popover doesn't close it. No JS needed.
// Opt out: add data-tt-no-lookup to regions you don't want selectable; <input type=password> is auto-excluded.
// ── B. Build a "word-lookup provider mini-app" (provider, needs text.provider, granted after review) ──
// The provider page (inline mode) receives the selected text pushed by the host via onLookup (pushed initially + on every new word, stays resident without reload),
// and looks up/translates/renders it into any UI; you can use tt.text.lookup to call the built-in engine, or your own cloud.data dictionary.
tt.ready(function () {
tt.text.onLookup(function (text) { // host pushes the selected text
tt.text.lookup(text).then(function (r) { // r={kind:'dict'|'translate',...}
render(r); // render into your own UI (auto-height)
});
});
});
// ── Escape hatch / flexible SDK primitives (available to any mini-app) ──
const r = await window.tt.text.lookup('lazy', { to:'en' }); // look up / translate on demand
window.tt.text.onSelect(function (sel){ /* {text, rect}; registering takes over, the default popover steps aside */ });
window.tt.openFloating({ path:'/detail', anchor: sel.rect, width:320, height:220 }); // open a floating window at the selection
window.tt.floating.moveTo(100, 200); // the floating window's position/size are fully adjustable via the SDK: setRect/moveTo/resize/close
الملفات والقرص السحابي
التعامل مع ملفات المحادثة · media.upload / im.share
عندما ينقر مستخدم "الفتح باستخدام" على صورة/ملف في محادثة، يمكنه اختيار تطبيقك المصغّر للتعامل معه — شريطة أن تكون قد أعلنت نوع ملف مطابقاً في fileHandlers ضمن manifest.json (image/video/audio/pdf/office/text/any). بمجرّد الفتح: يحصل getContextFile() على الملف، ويجلب readFile() البايتات من الأصل نفسه عبر المضيف (تفادياً لـ CORS، فتستطيع تحليل الصور/الصوت/الفيديو/أي صيغة)، ثم بعد المعالجة يرسل uploadFile() + sendFileToChat() الملف مرّةً أخرى إلى المحادثة، أو يقوم saveFile() بتنزيله — تدفّق سلس.
// —— Handling chat files: the user taps "Open with" on a file and picks your mini-app (must declare matching kinds in manifest.fileHandlers) ——
const f = window.tt.getContextFile();
// f = {url,name,mime,size,kind:'image'|'file',conversationId,messageId} or null (when opened standalone)
if (f) {
const bytes = await window.tt.readFile(f.url); // host fetches bytes same-origin (avoids iframe CORS; limited to this site's /uploads output)
// bytes = {dataUrl, mime, name, size, url} — feed to <img>/<video>/<audio>/canvas to analyze any format
imgEl.src = bytes.dataUrl;
}
// —— Process the output → send back to the conversation or download (needs media.upload / im.share) ——
const out = canvas.toDataURL('image/webp', 0.9); // e.g. convert image to WebP
const up = await window.tt.uploadFile({ dataUrl: out, name: 'result.webp' }); // {url,name,size,mime} (≤20MB)
await window.tt.sendFileToChat({
url: up.url, name: up.name, mime: up.mime, size: up.size,
conversationId: f.conversationId // pass it to send straight to the original conversation (skip the picker); omit it and the host shows a conversation picker
});
await window.tt.saveFile({ dataUrl: out, name: 'result.webp' }); // or: download locally (host downloads on your behalf)
قرص Tutu · tt.drive (يحتاج media.upload)
اختر ملفات من قرص Tutu، أو احفظ مخرجاتك في القرص (بوساطة المضيف: يختار المستخدم الملفات واحداً تلو الآخر في منتقي القرص داخل صفحة المضيف؛ ولا يحمل التطبيق المصغّر رمز قرص). يُرفض الطلب عند تعذّر الوصول إلى القرص / عدم اتحاد (federation) معرّف Tutu — وبعد .catch، يمكن للتطبيق المصغّر الرجوع إلى uploadFile محلّي.
// Pick files from the drive (the user picks in the host's drive picker; returns [] on cancel):
const picked = await window.tt.drive.pick({ multiple: true, accept: 'image/*' });
// picked = [{ nodeId, name, size, mime, downloadUrl }]
for (const file of picked) {
const bytes = await window.tt.readFile(file.downloadUrl); // fetch bytes to analyze/display
render(bytes.dataUrl);
}
// Save a file into the drive (pass this site's /uploads output absolutized as pullUrl, or a dataUrl directly):
const saved = await window.tt.drive.save({ pullUrl: up.url, name: 'export-result.png' });
// saved = { nodeId, name }
التفاعل بين التطبيقات
التفاعل بين التطبيقات · السحب بين التطبيقات · tt.link / tt.tray / tt.drag / tt.drop (لا حاجة لتفويض)
يمكن أن تكون عدّة تطبيقات مصغّرة مفتوحةً في الوقت نفسه (تُحفظ كـ مجموعة (combo) لفتحها معاً بنقرة واحدة، وتخطيطات متعددة النوافذ 2/3/4، وشريط جانبي مرسى (docked) — يديرها المضيف جميعاً، دون كود)، وتتفاعل عبر القدرات التالية:
tt.link: مزامنة الأحداث/الحالة في الزمن الحقيقي مع التطبيقات المصغّرة المفتوحة الأخرى (بثّ أو موجَّه، ≤2KB، محدود المعدّل 25/ثانية؛ يُسقَط عند إغلاق النافذة، غير محفوظ، غير عابر للمستخدمين/المحادثات).tt.tray: التقاط المحتوى إلى صينية (tray) المضيف لنقله، ثم حقنه في تطبيق مصغّر آخر أو محادثة.tt.drag.start/tt.drag.bind: بدء سحب / ربط عنصر كـ مصدر سحب يمكن سحبه مباشرةً إلى تطبيق مصغّر آخر (سحب وإفلات HTML5 أصلي بين التطبيقات المصغّرة ذات الأصل نفسه).tt.drop.accept: يمكن للتطبيق المصغّر كاملاً أن يستقبل الإفلاتات / حقن الصينية.tt.drop.zone: ★دع عنصراً داخلياً محدّداً يستشعر إفلاتاً (مع تعليقات تمييز عند التمرير) ويتصرّف بناءً عليه. يمكن أن يكون للتطبيق المصغّر عدّة مناطق (zones)، كلٌّ منها يستشعر باستقلالية.
الأمان: لا يقبل عنوان url مغلّف بـ file إلا مقابض /uploads/ الخاصة بهذا الموقع (يجلب المستقبِل البايتات عبر tt.readFile)؛ وتُسقَط الروابط الخارجية المزوّرة عبر أصل مختلف؛ ولـ text/json/name جميعها حدود حجم.
// —— ① Event/state sync tt.link (broadcast in realtime with "other open mini-apps"; drops on window close, not persisted, not cross-user/conversation) ——
tt.link.send({ type: 'color', color: '#ef4444' }); // broadcast to all linked mini-apps (≤2KB, rate-limited 25/s)
tt.link.sendTo(appId, { type: 'ping' }); // send to a specific peer
tt.link.on((from, msg) => { /* from={appId,slug,name} */ apply(msg); });
const peers = await tt.link.peers(); // the other linked mini-apps right now [{appId,slug,name}]
// —— ② Tray relay tt.tray (pick up → drop into another mini-app / conversation) ——
await tt.tray.put({ kind:'json', name:'color', data:{ color:'#ef4444' } }); // put into the host tray
const items = await tt.tray.list(); // view the tray [parcel]
const p = await tt.tray.take(id); // take a parcel out
// parcel = { kind:'file'|'text'|'json', url?/text?/data?, name?, mime? }
// —— ③ Direct drag between apps 【drag source】tt.drag ——
tt.drag.start({ kind:'json', name:'color', data:{ color:'#ef4444' } }); // start a drag, returns {id}
tt.drag.bind(swatchEl, () => ({ kind:'json', name:'color', data:{ type:'color', color:'#ef4444' } }));
// getParcel() returns this drag's parcel; return null to not start. Between same-origin mini-apps it uses native HTML5 drag-and-drop with the browser's built-in ghost.
// —— ④ Receive 【whole window】tt.drop.accept ——
tt.drop.accept(['json','file'], (parcel) => { apply(parcel); }); // callback on drop anywhere in this window / tray injection
// —— ⑤ ★Receive 【element-level】tt.drop.zone ——
tt.drop.zone(slotEl, ['json','file'], {
onEnter: () => slotEl.classList.add('hot'), // drag into this element → only it highlights (internal elements sense independently)
onOver: () => {}, // while hovering (do continuous feedback)
onLeave: () => slotEl.classList.remove('hot'), // move out → clear highlight
onDrop: (parcel) => fill(slotEl, parcel), // dropped on this element → only it receives and acts
});
واجهة المستخدم · النافذة · المضيف
واجهة المستخدم · النافذة · الجهاز (لا حاجة لتفويض)
قدرات واجهة مستخدم شبيهة بـ wx.* في WeChat — يعرض المضيف toasts/حوارات/عارضات صور حقيقية، ويتحكّم في نافذة كبسولة التطبيق المصغّر، ويصل إلى الحافظة/الاهتزاز/الاتصال الهاتفي/الموقع/الشبكة — قابلة للاستدعاء دون طلب تفويض، ما يجعل التطبيق المصغّر بقوّة تطبيق أصلي.
// —— Interaction feedback ——
window.tt.showToast({ title: 'Saved', icon: 'success' }); // icon: success|error|loading|none
window.tt.hideToast();
window.tt.showLoading({ title: 'Processing…' }); // pair with window.tt.hideLoading()
window.tt.hideLoading();
const { confirm } = await window.tt.showModal({ title: 'Confirm', content: 'Delete this?' });
const { tapIndex } = await window.tt.showActionSheet({ itemList: ['Camera', 'Album'] }); // rejects on cancel
// —— Window / system info ——
window.tt.setNavigationBarTitle({ title: 'My page' }); // change the mini-app capsule title
const info = await window.tt.getSystemInfo(); // {theme, platform, windowWidth, windowHeight, safeAreaInsets, appName, version}
// —— Device ——
await window.tt.setClipboardData({ data: 'copied text' });
const { data } = await window.tt.getClipboardData();
window.tt.vibrateShort(); window.tt.vibrateLong(); // haptic feedback
window.tt.makePhoneCall({ phoneNumber: '10086' });
// —— Media ——
window.tt.previewImage({ urls: [url1, url2], current: url1 }); // fullscreen image preview
// —— Location / external link / network ——
const loc = await window.tt.getLocation(); // browser prompts for permission → {latitude, longitude, accuracy, speed}
window.tt.openLocation({ latitude: loc.latitude, longitude: loc.longitude, name: 'Store' }); // view on a map
window.tt.openLink({ url: 'https://example.com' }); // open in a new tab (http/https only)
const net = await window.tt.getNetworkType(); // {isConnected, networkType: wifi|4g|...}
النافذة العائمة · ملء الشاشة · العلامة التجارية
تحكّم في نافذة التطبيق المصغّر: افتح نافذةً عائمة قابلة للسحب من الوضع المضمّن/من تحديد، ووسّع إلى ملء الشاشة / استعِد، ولوّن كبسولة المضيف، واستمع إلى تغييرات العرض/الحجم.
// —— Floating window (open a draggable floating window from inline / a selection) ——
window.tt.openFloating({ path:'/detail', anchor: rect, x:100, y:120, width:320, height:220 });
window.tt.floating.setRect({ x, y, width, height }); // also moveTo(x,y) / resize(w,h) / close()
window.tt.openFullPage('/detail'); // open the full page from an inline card (floating/fullscreen)
// —— Fullscreen / restore / close (desktop; mobile is already fullscreen) ——
window.tt.expand(); window.tt.collapse(); window.tt.close();
const dm = await window.tt.getDisplayMode(); // {maximized, mobile} — whether fullscreen / whether mobile
window.tt.onEvent('displayChanged', (p) => updateFullscreenChip(p.maximized)); // two-way sync with the host capsule's "fullscreen/restore"
window.tt.onEvent('viewportChanged', (p) => relayout(p.width, p.height)); // iframe size change → responsive re-layout
// —— Branding: tint the host capsule header/background ——
window.tt.setHeaderColor('#4f46e5');
window.tt.setBackgroundColor('#fdf6e3');
أزرار المضيف · اللمس التحفيزي (haptics) · بأسلوب Telegram (لا حاجة لتفويض)
يتحكّم التطبيق المصغّر في أزرار هيكل المضيف ويتلقّى أحداث نقرها (باتجاهين) — الزر الرئيسي السفلي mainButton، وزر الرجوع في الرأس backButton، واللمس التحفيزي hapticFeedback. هذا يتيح للتطبيق المصغّر التكامل بعمق مع واجهة المضيف (بدلاً من كونه صفحة معزولة).
// MainButton (the host's big bottom button, controlled by the mini-app + receives clicks) — Telegram-style
window.tt.mainButton.setText('Submit order').show(); // chainable; setText/setParams/show/hide/enable/disable/showProgress/hideProgress
window.tt.mainButton.onClick(() => { // click callback (host → mini-app event); offClick to unbind
window.tt.mainButton.showProgress();
submit().finally(() => window.tt.mainButton.hideProgress());
});
// BackButton (the host header's back button)
window.tt.backButton.show(); // show/hide/onClick/offClick
window.tt.backButton.onClick(() => history.back());
// Generic event listening (same as the onClick above)
window.tt.onEvent('mainButtonClicked', handler);
window.tt.onEvent('backButtonClicked', handler);
window.tt.offEvent('mainButtonClicked', handler); // unbind
// HapticFeedback
window.tt.hapticFeedback.impactOccurred('light'); // light|medium|heavy|rigid|soft
window.tt.hapticFeedback.notificationOccurred('success'); // error|success|warning
window.tt.hapticFeedback.selectionChanged();
السمة · التخزين السحابي · بأسلوب Telegram
يبقي colorScheme / themeParams ألوان التطبيق المصغّر متّسقة مع المضيف ويبدّلها مع الفاتح/الداكن؛ ويتزامن locale مع i18n الخاصة بالمضيف.
// —— Theme (consistent with the host's colors, switches with light/dark) ——
window.tt.colorScheme; // 'light' | 'dark'
window.tt.themeParams; // {bgColor,textColor,hintColor,linkColor,buttonColor,buttonTextColor,secondaryBgColor}
document.body.style.background = window.tt.themeParams.bgColor; // use the host color, consistent with the host
window.tt.onEvent('themeChanged', () => { // fires when the host toggles light/dark (same as onThemeChange)
applyTheme(window.tt.colorScheme, window.tt.themeParams);
});
// —— Locale i18n (synced with the host; the SDK already sets <html lang>) ——
window.tt.locale; // e.g. 'zh-CN' / 'en-US'; same as tt.context().locale
window.tt.onLocaleChange((locale) => renderInLang(locale)); // or onEvent('localeChanged')
المرجع
معالجة الأخطاء
يعيد كل window.tt.* وعداً (Promise) ويرفض بـ Error عند الفشل. يجب على التطبيقات المصغّرة الإنتاجية استخدام .catch / try-catch لكل استدعاء. أشيع رسائل err.message:
| err.message | المعنى / المعالجة المقترحة |
|---|---|
User denied authorization |
رُفض طلب التفويض عند الطلب ← وجّه لإعادة المحاولة |
User cancelled |
أُلغي منتقي المحادثة/المعاينة ← التزم الصمت |
Call timed out |
لم يستجب المضيف لفترة طويلة (نادر) ← اطلب إعادة المحاولة |
Data too large / too many documents / too many storage items |
تجاوُز الحصّة ← قلّص البيانات |
public reads are limited to pub_-prefixed public collections |
عدم تطابق تسمية المجموعة ← استخدم البادئة pub_ |
Forbidden (403) |
استخدم غير مطوّرٍ scope=all ← لا صلاحية |
Network error / Failed to fetch |
فشل الشبكة ← رسالة ودّية + إعادة المحاولة |
// Every tt.* returns a Promise and rejects an Error on failure; handle with .catch / try-catch.
window.tt.getProfile()
.then((me) => { /* … */ })
.catch((err) => {
switch (err.message) {
case 'User denied authorization': /* guide the user to retry authorization */ break;
case 'User cancelled': /* the user cancelled the conversation picker, stay silent */ break;
case 'Call timed out': /* the host was unresponsive for a long time (rare), prompt a retry */ break;
default: /* quota / permission (403) / network, etc — give a friendly prompt */
}
});
نموذج التفويض
التفويض عند الطلب: لا يتطلّب فتح تطبيق مصغّر منح كل الأذونات مسبقاً؛ إذ يطلب المضيف عنصراً واحداً فقط عند أوّل استدعاء لقدرة ما (سماح/رفض). يمكن للمستخدم "الوثوق بهذا التطبيق المصغّر" لمنح كل شيء دفعةً واحدة، أو تبديل العناصر فردياً وعرض سجلّات الاستخدام في صفحة الإعدادات. ولا يزال الخادم الخلفي يعيد التحقّق عند كل استدعاء (دفاع في العمق). أما قدرات واجهة المستخدم/النافذة/الجهاز/السمة/التفاعل بين التطبيقات فقابلة للاستخدام دون تفويض.
| القدرة (scope) | الوصف | حسّاسة |
|---|---|---|
user.profile |
الحصول على اسمك المستعار وصورتك الرمزية | — |
cloud.data |
البيانات السحابية (المجموعات؛ الطلبات/السجلّات إلخ) | — |
storage.kv |
تخزين البيانات (KV) | — |
media.upload |
رفع الصور/الملفات (بما في ذلك القرص tt.drive) | — |
im.share |
مشاركة بطاقة إلى محادثة | — |
im.send |
إرسال الرسائل | — |
im.read |
قراءة سجلّ المحادثة | حسّاسة |
im.room |
غرف اللعب الجماعي: إرسال/استقبال الرسائل وقراءة محادثة الغرفة نيابةً عنك | حسّاسة |
text.lookup |
البحث عن الكلمات/الترجمة (يُرسل النص المحدَّد إلى خدمة ترجمة) | حسّاسة |
text.provider |
مزوّد البحث (تعمل صفحة هذا التطبيق المصغّر كنافذة منبثقة للبحث/الترجمة) | حسّاسة |
الرؤية
- عام (Public): يظهر في "اكتشاف" والبحث؛ يمكن لأي أحد العثور عليه.
- غير مدرَج (Unlisted): ليس في اكتشاف/البحث؛ لا يُوصَل إليه إلا عبر بطاقة مُشاركة أو رابط منسوخ (رابط عميق) — للانتشار عبر حركة مرور خاصة دون عرض علني. بدّله في وحدة التحكّم بنقرة واحدة.
- وضع الفتح (سطح المكتب): يبدّل "وضع الفتح" في وحدة التحكّم بين "عائم (الافتراضي) / ملء الشاشة" — تضبط تطبيقات الرسم/اللوح الأبيض/المحرّر
fullscreenلملء الشاشة عند الفتح؛ وتستخدم البطاقات/النماذج الخفيفة الوضع العائم. يمكنك أيضاً إعلان القيمة الأوّلية فيdisplayضمنmanifest.json. الجوال دائماً ملء الشاشة، لا يتأثّر بهذا الإعداد.
الإصدارات والنشر
نموذج الإصدارات: مسوّدة ← قيد المراجعة ← جاهز ← مباشر.
تُحفظ كل الإصدارات التاريخية، مع تراجع بنقرة واحدة إلى أي إصدار تاريخي (تبديل فوري للإصدار المباشر). تعرض حالات الرفض السبب في مركز الإشعارات.
القيود والحصص
- البيانات السحابية: المستند الواحد ≤ 8KB، ≤ 500 مستند لكل (تطبيق مصغّر، مستخدم)،
list()≤ 200 في المرّة (بالأحدث)؛ استخدم ترقيم صفحات المؤشّرlistPage()(mine/all) للمزيد. ملاحظة: إجراء تجميع في الواجهة الأمامية (عدّ/متوسّط) مباشرةً بـlist()سيقلّل عدّ الجزء الأقدم عندما تتجاوز المجموعة 200، فيعطي نتيجة منخفضة — للإجماليات الكاملة استخدم ترقيم صفحات listPage أو اقبل تقريباً. - KV: القيمة الواحدة ≤ 8KB، ≤ 64 مفتاحاً لكل (تطبيق مصغّر، مستخدم).
- الصور المرفوعة ≤ 4MB؛ الملفات المرفوعة ≤ 20MB؛ إرسال الرسائل محدود المعدّل (≤ 20 لكل مستخدم في الدقيقة)؛ حمولة إشارة الغرفة ≤ 2KB؛ رسالة tt.link الواحدة ≤ 2KB، مقيّدة بـ 25/ثانية.
- حزمة الكود هي ملف HTML واحد (يُفضّل العرض بـ DOM الخالص؛ وتجنّب
innerHTMLلمنع XSS). - الرموز (Tokens) قصيرة العمر (نحو ساعتين)؛ يجدّدها المضيف بصمت بعد انتهائها؛ ويعيد الخادم الخلفي التحقّق من استدعاءات القدرات.
أمثلة مرجعية:
applets/food(الطلب، البيانات السحابية) وapplets/repair(طلبات الإصلاح) في المستودع، كلاهما تطبيقان مصغّران بواجهة أمامية خالصة + بيانات سحابية.