TutuHai मिनी-ऐप्स हल्के-फुल्के ऐप्स हैं जो TutuHai के अंदर चलते हैं। डेवलपर्स केवल एक फ्रंटएंड कोड बंडल अपलोड करते हैं, जिसे प्लेटफ़ॉर्म होस्ट करता है; क्षमताएँ (capabilities) window.tt SDK के ज़रिए TutuHai से बात करती हैं, और आपका अपना कोई बैकएंड नहीं होता (बिज़नेस डेटा TutuHai cloud data से गुज़रता है)। SDK एकीकरण · capability APIs · cloud data · authorization · publishing — सब कुछ एक ही पेज पर, कॉपी-करके-चलाने वाले उदाहरणों के साथ।
विषय-सूची · 26 विषय
शुरुआत करना
डेटा क्षमताएँ
बातचीत और मल्टीप्लेयर
फ़ाइलें और cloud drive
क्रॉस-ऐप इंटरैक्शन
UI · window · host
संदर्भ
शुरुआत करना
परिचय
TutuHai मिनी-ऐप्स हल्के-फुल्के ऐप्स हैं जो TutuHai के अंदर चलते हैं। डेवलपर्स केवल एक फ्रंटएंड कोड बंडल अपलोड करते हैं, जिसे प्लेटफ़ॉर्म होस्ट करता है; क्षमताएँ (capabilities) window.tt SDK के ज़रिए TutuHai से बात करती हैं, और आपका अपना कोई बैकएंड नहीं होता (बिज़नेस डेटा TutuHai cloud data से गुज़रता है)।
आइसोलेशन और सुरक्षा: मिनी-ऐप्स एक सैंडबॉक्स्ड iframe में एक अलग origin पर चलते हैं; होस्ट का लॉगिन सेशन कभी मिनी-ऐप में प्रवेश नहीं करता। हर कॉल के लिए होस्ट एक अल्पकालिक, सीमित टोकन जारी करता है, और बैकएंड capability (scope) के आधार पर दोबारा सत्यापन करता है।
त्वरित शुरुआत
- Mini-App Console (
/applets) में, एक मिनी-ऐप बनाने के लिए "New" पर क्लिक करें (नाम + अनूठा slug)। - एक सिंगल-फ़ाइल HTML लिखें (SDK शामिल करें,
window.tt.*के ज़रिए क्षमताओं को कॉल करें)। - एक version बनाएँ → अनुरोधित क्षमताएँ भरें → कोड बंडल अपलोड करें (सिंगल-फ़ाइल HTML)।
- समीक्षा के लिए सबमिट करें → admin स्वीकृति देता है → एक क्लिक में प्रोडक्शन पर publish करें।
- उपयोगकर्ता इसे "Discover" खोज के ज़रिए खोजते/खोलते हैं, या आप एक कार्ड शेयर करते हैं / सीधे पहुँच के लिए लिंक कॉपी करते हैं।
पैकेजिंग स्पेक
मिनी-ऐप्स दो अपलोड रूपों का समर्थन करते हैं: ① एक सिंगल-फ़ाइल HTML बंडल (स्व-निहित, entry = root, सबसे सरल); ② एक असली फ्रेमवर्क बिल्ड आउटपुट zip (npm run build से बनी dist/, जिसमें index.html + assets कई फ़ाइलों में फैले हों — देखें "Framework build output")। प्लेटफ़ॉर्म अपलोड से पहले एक स्पेक जाँच और ऑप्टिमाइज़ेशन चलाता है।
बंडल संरचना (बिल्ड आउटपुट)
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
कोड बंडल को यह संतुष्ट करना चाहिए (अपलोड पर स्वतः-जाँचा जाता है):
- Entry: एक सिंगल HTML फ़ाइल जिसमें
<!doctype html>और एक root<html>हो। - मोबाइल फिट: इसमें
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">शामिल होना चाहिए। - SDK:
<script src="/applet-sdk.js">शामिल करें (प्लेटफ़ॉर्म इसे होस्ट के absolute पते पर फिर से लिखता है)। - स्व-निहित: CSS/JS इनलाइन करें; यदि आपको बाहरी स्क्रिप्ट्स चाहिए, तो केवल प्लेटफ़ॉर्म SDK + प्रसिद्ध फ्रेमवर्क CDNs (unpkg / jsdelivr / cdnjs / esm.sh) की अनुमति है — मनमानी रिमोट स्क्रिप्ट्स वर्जित हैं (सुरक्षा)। छवियाँ और अन्य मीडिया
tt.uploadImageया किसी CDN से गुज़रते हैं। - आकार: सिंगल-फ़ाइल HTML ≤ 1MB; मल्टी-फ़ाइल zip कुल ≤ 8MB, प्रति फ़ाइल ≤ 1MB, ≤ 100 फ़ाइलें; अपलोड की गई छवियाँ ≤ 4MB।
- डेटा: आपका अपना कोई बैकएंड नहीं — बिज़नेस डेटा
tt.cloudcloud data /tt.*storageसे गुज़रता है।
📱🖥 मोबाइल / डेस्कटॉप यूनिवर्सल (एक ही codebase, दोनों सतहें)
वही बंडल TutuHai के अंदर एक अलग iframe में चलता है; होस्ट इसे मोबाइल (fullscreen) और डेस्कटॉप (panel / fullscreen हो सकता है) दोनों पर ले जाता है। एक responsive layout के साथ एक यूनिवर्सल codebase लिखें: ① viewport-fit=cover + safe areas env(safe-area-inset-*); ② overlays को bottom sheets (मोबाइल) ↔ केंद्रित (डेस्कटॉप, @media(min-width:480px)) के रूप में; ③ touch targets ≥ 44px; ④ होस्ट के dark/light का अनुसरण करें (tt.onThemeChange / [data-theme]); ⑤ शुद्ध DOM, कोई हार्ड-कोडेड चौड़ाई नहीं। इससे फ़ोन और कंप्यूटर दोनों पर अनुभव सुसंगत रहता है।
Framework build output (dist.zip)
सिंगल-फ़ाइल HTML के अलावा, आप एक असली फ्रेमवर्क का बिल्ड आउटपुट भी अपलोड कर सकते हैं — React / Vue / Svelte / Angular / Solid / Astro / Next (static export) / vanilla… किसी भी toolchain का npm run build इस्तेमाल करें, dist/ को zip करें (जिसमें index.html + assets/*.js/css + fonts/images हों) और होस्ट होने के लिए अपलोड करें।
Framework-agnostic: प्लेटफ़ॉर्म केवल एक "यूनिवर्सल static bundle contract" को पहचानता है — entry
index.html+ relative asset references + SDK। कोई भी फ्रेमवर्क जो उस contract को पूरा करने वाली एक staticdistबना सकता है, समर्थित है; नीचे दिए गए scaffolds बस चुनिंदा शॉर्टकट हैं, समर्थन की सीमा नहीं।
zip संरचना (बिल्ड आउटपुट, zip root = bundle root)
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…
तीन-चरणीय अनुकूलन (किसी भी फ्रेमवर्क के लिए काम करता है):
- एक relative base सेट करें (अनुशंसित, सबसे सुरक्षित) — आउटपुट को assets को relative path (
./assets/x.jsन कि root-absolute/assets/x.js) से संदर्भित करवाएँ, ताकि/<slug>/के तहत होस्टिंग पूरी तरह भरोसेमंद हो। (डिफ़ॉल्ट base भी काम करता है: प्लेटफ़ॉर्म HTML/CSS में root-absolute static references को स्वतः फिर से लिखता है और रनटाइम पर बने root-absolute assets — जैसे code-split CSS के preloads — के लिए एक Referer fallback का उपयोग करता है; लेकिन सख्तReferrer-Policy/ offline-prefetch किनारे के मामलों में Referer गायब हो सकता है और fallback विफल हो जाता है, इसलिए एक relative base सबसे सुरक्षित है।) - एक manifest जोड़ें — static directory में
manifest.jsonरखें (जैसे Vite/SvelteKit कीstatic/, अधिकांश फ्रेमवर्क कीpublic/) ताकि बिल्ड के बाद यहdistroot पर पहुँचे; या manifest छोड़ दें औरindex.htmlमें fallback के रूप में<meta name="tt:slug" content="…">(साथ मेंtt:name / tt:version / tt:scopes) जोड़ें। - SDK शामिल करें — दो तरीके: ① npm install (अनुशंसित, असली scaffolds के लिए सर्वोत्तम):
npm i @tutuhai/applet-sdkके बाद,import { tt } from '@tutuhai/applet-sdk'— बिल्ड समय पर आउटपुट में बंडल हो जाता है, TypeScript types के साथ, कोईindex.htmlसंपादन नहीं; ② याindex.htmlमें<script src="/applet-sdk.js">लिखें (प्लेटफ़ॉर्म इसे होस्ट के absolute पते पर फिर से लिखता है) और globalwindow.ttका उपयोग करें।
📦 npm SDK (React / Vue / Svelte आधिकारिक scaffolds के साथ परीक्षित)
npm create vite@latest -- --template react-ts | vue-ts | svelte-ts से एक प्रोजेक्ट बनाएँ, वही @tutuhai/applet-sdk इंस्टॉल करें, और इसे import करें — असली मल्टी-फ़ाइल source, npm run build कई chunks + एक entry 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 .
repo में चलने योग्य उदाहरण: applets/frameworks/{react,vue,svelte} (तीन असली आधिकारिक-scaffold प्रोजेक्ट, सभी वही SDK पैकेज import करते हुए)। SDK पैकेज source: 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: डेस्कटॉप के खुलने का रूप घोषित करता है — canvas/whiteboard/editor ऐप्स के लिए
fullscreenको प्राथमिकता दें, हल्के cards/forms के लिए डिफ़ॉल्टwindowका उपयोग करें। यह केवल एक आरंभिक मान है; publish के बाद आप console में कभी भी "Open mode" बदल सकते हैं (console की चलती है)। मोबाइल हमेशा fullscreen रहता है, इस फ़ील्ड से अप्रभावित। जब manifest छोड़ दिया जाता है, तो<meta name="tt:display" content="fullscreen">एक fallback के रूप में काम करता है। - fileHandlers: घोषित करता है कि आपका मिनी-ऐप chat से कौन-कौन सी फ़ाइल किस्में संभाल सकता है — जब कोई उपयोगकर्ता chat में किसी फ़ाइल पर "Open with" टैप करता है, तो मिलान करने वाला प्रकार घोषित करने वाले मिनी-ऐप्स उम्मीदवारों के रूप में दिखते हैं; किसी एक को टैप करने से वह फ़ाइल सीधे आपके मिनी-ऐप में भेज दी जाती है (देखें "Handling chat files")। प्रत्येक आइटम:
kindsइनमें से एक होता हैimage / video / audio / pdf / office / text / any(कई की अनुमति,any= कोई भी फ़ाइल);roleहोता हैeditor(एक editor में खोलें) याviewer(preview);labelवैकल्पिक (≤20 अक्षर, उम्मीदवार का display नाम)। अधिकतम 8 आइटम। Gating visibility से मेल खाता है:private/self-useसमीक्षा के बिना काम करता है (केवल आपके अपने "Open with" में दिखता है);publicको सबके लिए प्रभावी होने से पहले admin स्वीकृति चाहिए।
प्रति फ्रेमवर्क एक-पंक्ति "relative base" कॉन्फ़िग:
// 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">
⚠ Client-side-routed SPAs (SvelteKit / React Router / Vue Router / Angular) मिनी-ऐप्स
/<slug>/subpath के तहत होस्ट किए जाते हैं। एक client-side-routed SPA को अपना "router base" आपके slug पर सेट करना ही चाहिए, अन्यथा फ्रेमवर्क का router मौजूदा path से मेल नहीं करा पाता → पूरे-पेज का 404 (assets लोड होते हैं, लेकिन routing नहीं मिलने की सूचना देती है)। केवल एक relative asset base सेट करना पर्याप्त नहीं है — वह केवल asset URLs ठीक करता है, routing नहीं। प्रति फ्रेमवर्क: SvelteKitkit.paths.base='/<slug>'; React Router<BrowserRouter basename="/<slug>">; Vue RoutercreateWebHistory('/<slug>/'); AngularAPP_BASE_HREF='/<slug>/'। (client-side routing के बिना ऐप्स — शुद्ध rendering / बिना Router वाला single-page React / vanilla — अप्रभावित रहते हैं।)
⚠ Upload check-up अपलोड पर प्लेटफ़ॉर्म dist पर एक "contract check-up" चलाता है: entry / relative paths / manifest / SDK reference / limits / MIME में से प्रत्येक को इनलाइन संकेतों के साथ सत्यापित किया जाता है। कुल ≤ 8MB, प्रति फ़ाइल ≤ 1MB, ≤ 100 फ़ाइलें, केवल whitelisted MIME (html/css/js/json/images/fonts/map/wasm)। आकार कम रखने के लिए fonts को compress और subset करें। भारी-भरकम फ्रेमवर्क आउटपुट (जैसे tldraw / excalidraw जिनका एक >1MB single chunk हो) के लिए जो डिफ़ॉल्ट प्रति-फ़ाइल/कुल सीमा से अधिक हों, operations टीम से admin panel में "per-file bytes" / "unpacked total" सीमाएँ बढ़ाने को कहें (रनटाइम पर बदली जा सकती हैं, तुरंत प्रभावी);
manualChunksको विभाजित करने से भी vendor को सीमा के नीचे लाया जा सकता है।
न्यूनतम उदाहरण
एक पूर्ण, चलने योग्य मिनी-ऐप — SDK शामिल करें, उपयोगकर्ता का nickname पढ़ें:
<!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 एकीकरण
अपने मिनी-ऐप HTML में SDK स्क्रिप्ट शामिल करें, फिर 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>
अपने मिनी-ऐप में कभी भी होस्ट डोमेन हार्ड-कोड न करें। relative
/applet-sdk.jsलिखें (या कोई placeholder origin) — जब आपका ऐप अपने iframe में चलता है, तो प्लेटफ़ॉर्म serve समय पर SDK स्क्रिप्ट URL को मौजूदा होस्ट पर फिर से लिखता है। इसलिए भले ही TutuHai अपना डोमेन बदल दे, या कोई डोमेन ब्लॉक हो जाए, हर publish किया गया मिनी-ऐप बिना किसी कोड परिवर्तन और बिना दोबारा publish के काम करता रहता है — एक operator केवल एक config मान बदल देता है।
Ready callback, context, और theme/locale:
// 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 framework-agnostic है और सभी मुख्यधारा फ्रेमवर्क में सीधे काम करता है (सिंगल-फ़ाइल, बिना build)। प्रत्येक उदाहरण में सफलता ✅ और विफलता ❌ (authorization अस्वीकृत / नेटवर्क त्रुटि) दोनों की हैंडलिंग शामिल है:
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)) // ❌
);
}
}
पूर्ण चलने योग्य उदाहरण repo में हैं: 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 · cloud.data
होस्ट किए गए structured collections जो एक मिनी-ऐप को अपने बैकएंड के बिना बिज़नेस डेटा persist करने देते हैं। तीन visibility स्तर:
mine: केवल अपने दस्तावेज़ पढ़ें/लिखें (डिफ़ॉल्ट)।all: सब कुछ पढ़ें/लिखें, केवल मिनी-ऐप डेवलपर (owner) — एक "merchant console" के लिए ताकि वह सभी orders/tickets देख सके।public: कोई भी logged-in उपयोगकर्ता सब कुछ पढ़ सकता है, collection के नाम की शुरुआतpub_से होनी चाहिए — community/marketplace/forum के लिए; writes और edits अब भी केवल author तक सीमित रहते हैं।
// 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);
}
प्रत्येक row इस तरह पढ़ी जाती है { id, ownerId, mine, data:{…your fields}, createdAt, updatedAt } — आपके फ़ील्ड सब data में होते हैं (जैसे row.data.title)।
KV storage · storage.kv
प्रति (मिनी-ऐप, उपयोगकर्ता) पृथक Key-value, check-in गिनती, drafts आदि जैसे छोटे निजी state के लिए अच्छा। tt.cloudStorage (setItem/getItem/getKeys/removeItem) इसका Telegram-शैली alias है।
// 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 एक संवेदनशील capability है।
// 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);
}
मल्टीप्लेयर rooms · im.room
एक "बातचीत" को मिनी-गेम्स / सहयोग के लिए एक realtime room में बदलें: एक room बनाएँ/join करें, room-अंदर persistent messages और realtime signals (state sync, ≤2KB, क्षणिक, persist नहीं होते)। होस्ट realtime frames को आपके रूप में bridge करता है, host JWT कभी मिनी-ऐप में प्रवेश नहीं करता; इस मिनी-ऐप द्वारा बनाए गए rooms / उन बातचीतों तक सीमित जिनमें आपको share किया गया था — यह उपयोगकर्ता की अन्य निजी chats को छू नहीं सकता।
// 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
Reconnect hydration: signals best-effort होते हैं और disconnect पर frame loss सामान्य है।
onReconnectपर, अंतिम state कोroom.history()या cloud data से फिर से hydrate करें — signals पर सत्य के एकमात्र स्रोत के रूप में निर्भर न रहें।
Inline components · native-component-like · privacy-safe
shareToChat({inline:true}) से भेजा गया एक कार्ड chat bubble के अंदर ही एक interactive component render करता है (जैसे एक poll, एक rating); प्राप्तकर्ता इसे बिना floating window खोले एक native फ़ीचर की तरह संचालित करते हैं। मिनी-ऐप ctx.inline के आधार पर एक compact UI render करता है; bubble content के अनुसार auto-size होता है और होस्ट के light/dark के साथ live re-theme होता है। Privacy: एक inline instance को केवल एक "cloud.data only, no side effects" restricted token मिलता है — यह public collections पढ़ सकता है + अपने दस्तावेज़ लिख सकता है, दूसरों के निजी डेटा को छू नहीं सकता, और authorization के लिए prompt नहीं करता।
// —— 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.
शब्द lookup / translate · text.lookup / text.provider
शब्द-lookup popover स्वयं एक "provider मिनी-ऐप" का inline page है — इसकी content/functionality सब उसी मिनी-ऐप द्वारा render होती है; होस्ट केवल selection + hover positioning + एक flexible SDK प्रदान करता है। Consumers (अपने मिनी-ऐप में text को selectable बनाएँ): text.lookup घोषित करें, शून्य कोड — text चुनने पर provider का inline page selection के ठीक नीचे pop होता है (chat message text भी समर्थित है)। Providers (एक lookup मिनी-ऐप बनाएँ): text.provider घोषित करें (समीक्षा के बाद प्रदान किया जाता है), inline page tt.text.onLookup के ज़रिए शब्द प्राप्त करता है और खुद को render करता है; कौन-सा provider सक्रिय है यह admin panel में कॉन्फ़िगर किया जाता है — यदि कोई कॉन्फ़िगर/authorized नहीं है, तो lookup अक्षम रहता है। किसी क्षेत्र को opt out करने के लिए 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
फ़ाइलें और cloud drive
chat फ़ाइलें संभालना · media.upload / im.share
जब कोई उपयोगकर्ता chat में किसी छवि/फ़ाइल पर "Open with" टैप करता है, तो वे इसे संभालने के लिए आपका मिनी-ऐप चुन सकते हैं — बशर्ते आपने manifest.json के fileHandlers में एक मिलान करने वाला फ़ाइल प्रकार घोषित किया हो (image/video/audio/pdf/office/text/any)। खुलने के बाद: getContextFile() फ़ाइल प्राप्त करता है, readFile() होस्ट के ज़रिए same-origin में bytes लाता है (CORS से बचते हुए, ताकि आप images/audio/video/किसी भी format का विश्लेषण कर सकें), फिर प्रोसेसिंग के बाद uploadFile() + sendFileToChat() इसे बातचीत में वापस भेजता है, या saveFile() इसे डाउनलोड करता है — एक निर्बाध flow।
// —— 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 Drive · tt.drive (needs media.upload)
Tutu Drive से फ़ाइलें pick करें, या अपना output drive में save करें (host-mediated: उपयोगकर्ता होस्ट पेज के अंदर drive picker में एक-एक करके फ़ाइलें चुनता है; मिनी-ऐप कोई drive token नहीं रखता)। जब drive अगम्य हो / Tutu ID federated न हो तो reject करता है — .catch के बाद, मिनी-ऐप एक local uploadFile पर fall back कर सकता है।
// 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 }
क्रॉस-ऐप इंटरैक्शन
क्रॉस-ऐप इंटरैक्शन · ऐप्स के बीच drag · tt.link / tt.tray / tt.drag / tt.drop (authorization की आवश्यकता नहीं)
कई मिनी-ऐप्स एक साथ खुले हो सकते हैं (एक क्लिक में एक साथ खोलने के लिए एक combo के रूप में सहेजे गए, multi-window 2/3/4 layouts, एक docked sidebar — सब होस्ट द्वारा प्रबंधित, कोई कोड आवश्यक नहीं), और निम्न क्षमताओं के ज़रिए interact करते हैं:
tt.link: अन्य खुले मिनी-ऐप्स के साथ realtime में events/state sync करें (broadcast या targeted, ≤2KB, rate-limited 25/s; window बंद होने पर drop, persist नहीं, cross-user/conversation नहीं)।tt.tray: content को host tray में उठाएँ ताकि उसे relay किया जा सके, फिर किसी अन्य मिनी-ऐप या बातचीत में inject करें।tt.drag.start/tt.drag.bind: एक drag शुरू करें / एक element को एक drag source के रूप में bind करें जिसे सीधे किसी अन्य मिनी-ऐप में drag किया जा सके (same-origin मिनी-ऐप्स के बीच native HTML5 drag-and-drop)।tt.drop.accept: पूरा मिनी-ऐप drops / tray injections प्राप्त कर सकता है।tt.drop.zone: ★एक विशिष्ट आंतरिक element को एक drop महसूस करने दें (hover-highlight feedback के साथ) और उस पर कार्य करें। एक मिनी-ऐप में कई zones हो सकते हैं, प्रत्येक स्वतंत्र रूप से महसूस करता है।
सुरक्षा: एक file-wrapped url केवल इस साइट के /uploads/ handles स्वीकार करता है (receiver tt.readFile के ज़रिए bytes लाता है); जाली cross-origin external links को drop कर दिया जाता है; text/json/name सभी की size caps होती हैं।
// —— ① 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
});
UI · window · host
UI · window · device (authorization की आवश्यकता नहीं)
WeChat wx.*-जैसी UI क्षमताएँ — होस्ट असली toasts/dialogs/image viewers render करता है, मिनी-ऐप की capsule window को नियंत्रित करता है, और clipboard/vibration/dialing/location/network तक पहुँचता है — बिना authorization का अनुरोध किए callable, जो एक मिनी-ऐप को एक native ऐप जितना शक्तिशाली बनाती हैं।
// —— 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 · fullscreen · branding
मिनी-ऐप window को नियंत्रित करें: inline/किसी selection से एक draggable floating window खोलें, fullscreen में विस्तृत करें / restore करें, host capsule को tint करें, और show/size परिवर्तनों को सुनें।
// —— 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');
Host buttons · haptics · Telegram-शैली (authorization की आवश्यकता नहीं)
एक मिनी-ऐप host chrome के buttons को नियंत्रित करता है और उनके click events प्राप्त करता है (दोतरफा) — नीचे का मुख्य button mainButton, header का back button backButton, haptics hapticFeedback। इससे एक मिनी-ऐप host UI के साथ गहराई से एकीकृत हो जाता है (एक अलग-थलग पेज होने के बजाय)।
// 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();
Theme · cloud storage · Telegram-शैली
colorScheme / themeParams मिनी-ऐप के रंगों को होस्ट के साथ सुसंगत रखते हैं और light/dark के साथ switch होते हैं; locale होस्ट के i18n के साथ sync होता है।
// —— 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 reject करता है। प्रोडक्शन मिनी-ऐप्स को हर कॉल पर .catch / try-catch करना ही चाहिए। सामान्य err.message:
| err.message | अर्थ / सुझाई गई हैंडलिंग |
|---|---|
User denied authorization |
on-demand authorization prompt अस्वीकृत हुआ → एक retry के लिए मार्गदर्शन करें |
User cancelled |
बातचीत picker/preview रद्द कर दिया गया → चुप रहें |
Call timed out |
होस्ट लंबे समय तक अनुत्तरदायी रहा (दुर्लभ) → retry के लिए prompt करें |
Data too large / too many documents / too many storage items |
Quota पार हो गया → डेटा घटाएँ |
public reads are limited to pub_-prefixed public collections |
Collection नामकरण बेमेल → एक pub_ prefix का उपयोग करें |
Forbidden (403) |
एक non-developer ने scope=all इस्तेमाल किया → कोई अनुमति नहीं |
Network error / Failed to fetch |
नेटवर्क विफलता → मित्रवत prompt + retry |
// 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 */
}
});
Authorization मॉडल
On-demand authorization: एक मिनी-ऐप खोलने के लिए पहले से सभी permissions देने की आवश्यकता नहीं होती; होस्ट किसी एक आइटम के लिए केवल तभी prompt करता है जब कोई capability पहली बार कॉल की जाती है (allow/deny)। उपयोगकर्ता एक ही बार में सब कुछ देने के लिए "trust this mini-app" कर सकता है, या settings पेज पर आइटम एक-एक करके toggle कर सकता है और usage records देख सकता है। बैकएंड फिर भी हर कॉल पर दोबारा सत्यापन करता है (defense in depth)। UI/window/device/theme/cross-app क्षमताएँ बिना authorization के प्रयोग योग्य हैं।
| Capability (scope) | विवरण | संवेदनशील |
|---|---|---|
user.profile |
अपना nickname और avatar प्राप्त करें | — |
cloud.data |
Cloud data (collections; orders/records आदि) | — |
storage.kv |
डेटा storage (KV) | — |
media.upload |
Images/files अपलोड करें (drive tt.drive सहित) | — |
im.share |
chat में एक कार्ड share करें | — |
im.send |
संदेश भेजें | — |
im.read |
बातचीत इतिहास पढ़ें | संवेदनशील |
im.room |
मल्टीप्लेयर rooms: आपकी ओर से संदेश भेजें/प्राप्त करें और room chat पढ़ें | संवेदनशील |
text.lookup |
शब्द lookup/translate (चुना गया text एक translation service को भेजा जाता है) | संवेदनशील |
text.provider |
Lookup provider (इस मिनी-ऐप का पेज lookup/translate popover के रूप में कार्य करता है) | संवेदनशील |
Visibility
- Public: "Discover" और खोज में दिखता है; कोई भी इसे खोज सकता है।
- Unlisted: discover/search में नहीं; केवल एक share किए गए कार्ड या कॉपी किए गए लिंक (deep link) के ज़रिए पहुँच योग्य — सार्वजनिक प्रदर्शन के बिना private-traffic प्रसार के लिए। इसे console में एक क्लिक से toggle करें।
- Open mode (desktop): console का "Open mode" "floating (default) / fullscreen" के बीच toggle करता है — canvas/whiteboard/editor ऐप्स खुलने पर स्क्रीन भरने के लिए
fullscreenसेट करते हैं; हल्के cards/forms floating का उपयोग करते हैं। आपmanifest.jsonकेdisplayमें आरंभिक मान भी घोषित कर सकते हैं। मोबाइल हमेशा fullscreen रहता है, इस सेटिंग से अप्रभावित।
Versions और publishing
Version मॉडल: draft → in review → ready → live।
सभी ऐतिहासिक versions रखे जाते हैं, किसी भी ऐतिहासिक version पर एक-क्लिक rollback के साथ (live version का तत्काल swap)। Rejections notification center में कारण दिखाते हैं।
बाधाएँ और quotas
- Cloud data: एकल दस्तावेज़ ≤ 8KB, प्रति (मिनी-ऐप, उपयोगकर्ता) ≤ 500 दस्तावेज़,
list()एक बार में ≤ 200 (नवीनतम के अनुसार); अधिक के लिएlistPage()cursor pagination (mine/all) का उपयोग करें। ध्यान दें: जब कोई collection 200 से अधिक हो तोlist()से सीधे frontend aggregation (counting/averaging) करने से सबसे पुराना हिस्सा under-count हो जाएगा और कम परिणाम मिलेगा — पूर्ण totals के लिए listPage pagination का उपयोग करें या एक approximation स्वीकार करें। - KV: एकल मान ≤ 8KB, प्रति (मिनी-ऐप, उपयोगकर्ता) ≤ 64 keys।
- अपलोड की गई images ≤ 4MB; अपलोड की गई files ≤ 20MB; संदेश भेजना rate-limited है (प्रति उपयोगकर्ता प्रति मिनट ≤ 20); room signal payload ≤ 2KB; tt.link एकल संदेश ≤ 2KB, throttled 25/s।
- कोड बंडल एक सिंगल-फ़ाइल HTML है (शुद्ध DOM rendering को प्राथमिकता दें; XSS रोकने के लिए
innerHTMLसे बचें)। - Tokens अल्पकालिक होते हैं (लगभग 2 घंटे); समाप्ति के बाद होस्ट चुपचाप renew करता है; capability calls बैकएंड द्वारा दोबारा सत्यापित होते हैं।
संदर्भ उदाहरण: repo के
applets/food(ordering, cloud data) औरapplets/repair(repair requests) दोनों शुद्ध-frontend + cloud-data मिनी-ऐप्स हैं।