Offene Dokumentation für TutuHai Mini-Apps
SDK-Integration · Fähigkeits-APIs · Cloud-Daten · Autorisierung · Veröffentlichung
TutuHai Mini-Apps sind leichtgewichtige Anwendungen, die innerhalb von TutuHai laufen. Entwickler laden ausschließlich ein Frontend-Code-Bundle hoch, das von der Plattform gehostet wird; Funktionen kommunizieren über das window.tt SDK mit TutuHai, ganz ohne eigenes Backend (Geschäftsdaten laufen über die TutuHai-Cloud-Daten). SDK-Integration · Funktions-APIs · Cloud-Daten · Autorisierung · Veröffentlichung — alles auf einer Seite, mit Beispielen zum Kopieren und Ausführen.
Inhalt · 26 Themen
Erste Schritte
Datenfunktionen
Unterhaltungen & Mehrspieler
Dateien & Cloud-Drive
App-übergreifende Interaktion
UI · Fenster · Host
Referenz
Erste Schritte
Einführung
TutuHai Mini-Apps sind leichtgewichtige Anwendungen, die innerhalb von TutuHai laufen. Entwickler laden ausschließlich ein Frontend-Code-Bundle hoch, das von der Plattform gehostet wird; Funktionen kommunizieren über das window.tt SDK mit TutuHai, ganz ohne eigenes Backend (Geschäftsdaten laufen über die TutuHai-Cloud-Daten).
Isolation und Sicherheit: Mini-Apps laufen in einem sandboxed iframe auf einem isolierten Origin; die Login-Sitzung des Hosts gelangt niemals in die Mini-App. Für jeden Aufruf stellt der Host ein kurzlebiges, eingeschränktes Token aus, und das Backend validiert erneut nach Funktion (Scope).
Schnellstart
- Klicke in der Mini-App-Konsole (
/applets) auf „Neu", um eine Mini-App zu erstellen (Name + eindeutiger Slug). - Schreibe eine Single-File-HTML (binde das SDK ein, rufe Funktionen über
window.tt.*auf). - Erstelle eine Version → gib die angeforderten Funktionen an → lade das Code-Bundle hoch (Single-File-HTML).
- Zur Prüfung einreichen → Admin genehmigt → mit einem Klick in die Produktion veröffentlichen.
- Nutzer finden/öffnen sie über die „Entdecken"-Suche, oder du teilst eine Karte / kopierst einen Link für den direkten Zugriff.
Paketierungsspezifikation
Mini-Apps unterstützen zwei Upload-Formen: ① ein Single-File-HTML-Bundle (in sich geschlossen, Einstieg = Wurzel, am einfachsten); ② ein echtes Framework-Build-Ausgabe-zip (das dist/ aus npm run build, mit index.html + Assets über mehrere Dateien — siehe „Framework-Build-Ausgabe"). Die Plattform führt vor dem Upload eine Spezifikationsprüfung und Optimierung durch.
Bundle-Struktur (Build-Ausgabe)
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
Das Code-Bundle muss erfüllen (beim Upload automatisch geprüft):
- Einstieg: eine einzelne HTML-Datei mit
<!doctype html>und einer Wurzel<html>. - Mobile Anpassung: muss
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">enthalten. - SDK: binde
<script src="/applet-sdk.js">ein (die Plattform schreibt es auf die absolute Adresse des Hosts um). - In sich geschlossen: CSS/JS inline; falls du externe Skripte benötigst, sind nur das Plattform-SDK + bekannte Framework-CDNs (unpkg / jsdelivr / cdnjs / esm.sh) erlaubt — beliebige entfernte Skripte sind verboten (Sicherheit). Bilder und andere Medien laufen über
tt.uploadImageoder ein CDN. - Größe: Single-File-HTML ≤ 1MB; Multi-File-zip ≤ 8MB gesamt, ≤ 1MB pro Datei, ≤ 100 Dateien; hochgeladene Bilder ≤ 4MB.
- Daten: kein eigenes Backend — Geschäftsdaten laufen über
tt.cloudCloud-Daten /tt.*storage.
📱🖥 Mobil / Desktop universell (eine Codebasis, beide Oberflächen)
Dasselbe Bundle läuft in einem isolierten iframe innerhalb von TutuHai; der Host trägt es sowohl auf Mobil (Vollbild) als auch auf Desktop (Panel / kann in den Vollbildmodus gehen). Schreibe eine universelle Codebasis mit einem responsiven Layout: ① viewport-fit=cover + Safe Areas env(safe-area-inset-*); ② Overlays als Bottom Sheets (mobil) ↔ zentriert (Desktop, @media(min-width:480px)); ③ Touch-Ziele ≥ 44px; ④ folge dem Dunkel-/Hellmodus des Hosts (tt.onThemeChange / [data-theme]); ⑤ reines DOM, keine fest kodierten Breiten. So bleibt das Erlebnis auf Handy und Computer gleich.
Framework-Build-Ausgabe (dist.zip)
Neben Single-File-HTML kannst du auch die Build-Ausgabe eines echten Frameworks hochladen — nutze React / Vue / Svelte / Angular / Solid / Astro / Next (statischer Export) / vanilla… den npm run build jeder beliebigen Toolchain, packe das dist/ (mit index.html + assets/*.js/css + Schriftarten/Bildern) in ein zip und lade es zum Hosten hoch.
Framework-agnostisch: Die Plattform erkennt nur einen „universellen statischen Bundle-Vertrag" — Einstieg
index.html+ relative Asset-Referenzen + SDK. Jedes Framework, das ein statischesdisterzeugen kann, das diesen Vertrag erfüllt, wird unterstützt; die untenstehenden Scaffolds sind nur kuratierte Abkürzungen, nicht die Grenze der Unterstützung.
zip-Struktur (Build-Ausgabe, zip-Wurzel = Bundle-Wurzel)
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…
Dreistufige Anpassung (funktioniert für jedes Framework):
- Setze eine relative Base (empfohlen, am sichersten) — sorge dafür, dass die Ausgabe Assets über relative Pfade referenziert (
./assets/x.jsstatt des wurzel-absoluten/assets/x.js), sodass das Hosting unter/<slug>/narrensicher ist. (Die Standard-Base funktioniert ebenfalls: Die Plattform schreibt wurzel-absolute statische Referenzen in HTML/CSS automatisch um und nutzt einen Referer-Fallback für wurzel-absolute Assets, die zur Laufzeit erzeugt werden — etwa Preloads von per Code-Splitting getrenntem CSS; aber unter striktenReferrer-Policy/ Offline-Prefetch-Grenzfällen kann der Referer fehlen und der Fallback schlägt fehl, weshalb eine relative Base am sichersten ist.) - Füge ein Manifest hinzu — lege
manifest.jsonin das statische Verzeichnis (z. B. Vites/SvelteKitsstatic/, daspublic/der meisten Frameworks), damit es nach dem Build in derdist-Wurzel landet; oder lasse das Manifest weg und füge<meta name="tt:slug" content="…">(plustt:name / tt:version / tt:scopes) inindex.htmlals Fallback hinzu. - Binde das SDK ein — zwei Wege: ① npm install (empfohlen, am besten für echte Scaffolds): nach
npm i @tutuhai/applet-sdk,import { tt } from '@tutuhai/applet-sdk'— zur Build-Zeit in die Ausgabe gebündelt, mit TypeScript-Typen, ohne Änderungen anindex.html; ② oder schreibe<script src="/applet-sdk.js">inindex.html(die Plattform schreibt es auf die absolute Adresse des Hosts um) und nutze das globalewindow.tt.
📦 npm SDK (getestet mit den offiziellen React- / Vue- / Svelte-Scaffolds)
Erstelle ein Projekt mit npm create vite@latest -- --template react-ts | vue-ts | svelte-ts, installiere dasselbe @tutuhai/applet-sdk und importiere es — echter Multi-File-Quellcode, npm run build erzeugt mehrere Chunks + einen Einstieg index.html; packe es in ein zip und lade es hoch.
# 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 .
Lauffähige Beispiele im Repository: applets/frameworks/{react,vue,svelte} (drei echte Projekte mit offiziellen Scaffolds, die alle dasselbe SDK-Paket importieren). SDK-Paketquelle: applet-sdk/.
manifest.json-Felder
{
"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: deklariert die Desktop-Öffnungsform — für Canvas-/Whiteboard-/Editor-Apps bevorzuge
fullscreen, für leichtgewichtige Karten/Formulare nutze das Standard-window. Es ist nur ein Anfangswert; nach der Veröffentlichung kannst du den „Öffnungsmodus" jederzeit in der Konsole ändern (die Konsole gewinnt). Mobil ist immer Vollbild, unabhängig von diesem Feld. Wenn das Manifest weggelassen wird, funktioniert<meta name="tt:display" content="fullscreen">als Fallback. - fileHandlers: deklariert, welche Dateiarten deine Mini-App aus einem Chat verarbeiten kann — wenn ein Nutzer bei einer Datei im Chat auf „Öffnen mit" tippt, erscheinen Mini-Apps, die einen passenden Typ deklariert haben, als Kandidaten; ein Tippen sendet diese Datei direkt in deine Mini-App (siehe „Chat-Dateien verarbeiten"). Jeder Eintrag:
kindsist eines vonimage / video / audio / pdf / office / text / any(mehrere erlaubt,any= beliebige Datei);roleisteditor(in einem Editor öffnen) oderviewer(Vorschau);labeloptional (≤20 Zeichen, der Anzeigename des Kandidaten). Bis zu 8 Einträge. Gating entspricht der Sichtbarkeit:private/self-usefunktioniert ohne Prüfung (erscheint nur in deinem eigenen „Öffnen mit");publicerfordert die Genehmigung eines Admins, bevor es für alle wirksam wird.
Einzeilige „relative Base"-Konfiguration pro Framework:
// 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-seitig geroutete SPAs (SvelteKit / React Router / Vue Router / Angular) Mini-Apps werden unter dem Unterpfad
/<slug>/gehostet. Eine client-seitig geroutete SPA muss ihre „Router-Base" auf deinen Slug setzen, sonst kann der Router des Frameworks den aktuellen Pfad nicht zuordnen → 404 der gesamten Seite (Assets laden, aber das Routing meldet „nicht gefunden"). Nur eine relative Asset-Base zu setzen, ist nicht genug — das behebt nur Asset-URLs, nicht das Routing. Pro Framework: SvelteKitkit.paths.base='/<slug>'; React Router<BrowserRouter basename="/<slug>">; Vue RoutercreateWebHistory('/<slug>/'); AngularAPP_BASE_HREF='/<slug>/'. (Apps ohne client-seitiges Routing — reines Rendering / ein einseitiges React ohne Router / vanilla — sind nicht betroffen.)
⚠ Upload-Prüfung Beim Upload führt die Plattform eine „Vertrags-Prüfung" am dist durch: Einstieg / relative Pfade / Manifest / SDK-Referenz / Limits / MIME werden jeweils mit Inline-Hinweisen validiert. Gesamt ≤ 8MB, ≤ 1MB pro Datei, ≤ 100 Dateien, nur zugelassene MIME-Typen (html/css/js/json/Bilder/Schriftarten/map/wasm). Komprimiere und subsette Schriftarten, um die Größe niedrig zu halten. Für schwergewichtige Framework-Ausgaben (z. B. tldraw / excalidraw mit einem einzelnen Chunk >1MB), die die Standard-Limits pro Datei/gesamt überschreiten, bitte das Betriebsteam, die Limits für „Bytes pro Datei" / „entpackt gesamt" im Admin-Panel anzuheben (zur Laufzeit änderbar, sofort wirksam); das Aufteilen von
manualChunkskann vendor ebenfalls unter das Limit bringen.
Minimalbeispiel
Eine vollständige, lauffähige Mini-App — binde das SDK ein, lies den Spitznamen des Nutzers:
<!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-Integration
Binde das SDK-Skript in deine Mini-App-HTML ein, dann nutze 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>
Kodiere niemals die Host-Domain fest in deine Mini-App. Schreibe das relative
/applet-sdk.js(oder einen beliebigen Platzhalter-Origin) — die Plattform schreibt die SDK-Skript-URL zur Auslieferungszeit auf den aktuellen Host um, wenn deine App in ihrem iframe läuft. Selbst wenn TutuHai also seine Domain ändert oder eine Domain blockiert wird, funktioniert jede veröffentlichte Mini-App ohne Codeänderung und ohne erneute Veröffentlichung weiter — ein Betreiber ändert nur einen einzigen Konfigurationswert.
Ready-Callback, Kontext sowie 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
});
Framework-Beispiele
window.tt ist framework-agnostisch und funktioniert direkt in allen gängigen Frameworks (Single-File, ohne Build). Jedes Beispiel behandelt sowohl Erfolg ✅ als auch Fehlschlag ❌ (Autorisierung verweigert / Netzwerkfehler):
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)) // ❌
);
}
}
Vollständige lauffähige Beispiele befinden sich im Repository: applet-platform/samples/demo-react.html, demo-svelte.html, demo-vue.html.
Datenfunktionen
Nutzerprofil · 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-Daten · cloud.data
Gehostete strukturierte Collections, die es einer Mini-App ermöglichen, Geschäftsdaten ganz ohne eigenes Backend zu persistieren. Drei Sichtbarkeitsstufen:
mine: nur eigene Dokumente lesen/schreiben (Standard).all: alles lesen/schreiben, nur der Mini-App-Entwickler (Eigentümer) — für eine „Händler-Konsole", die alle Bestellungen/Tickets sehen soll.public: jeder eingeloggte Nutzer kann alles lesen, der Collection-Name muss mitpub_beginnen — für Community/Marktplatz/Forum; Schreiben und Bearbeiten bleiben weiterhin auf den Autor beschränkt.
// 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);
}
Jede Zeile wird zurückgelesen wie { id, ownerId, mine, data:{…your fields}, createdAt, updatedAt } — deine Felder liegen alle in data (z. B. row.data.title).
KV-Speicher · storage.kv
Schlüssel-Wert-Paare, isoliert pro (Mini-App, Nutzer), gut für kleinen privaten Zustand wie Check-in-Zähler, Entwürfe usw. tt.cloudStorage (setItem/getItem/getKeys/removeItem) ist sein Telegram-artiger 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');
Unterhaltungen & Mehrspieler
Unterhaltungsfunktionen · im.share / im.send / im.read / media.upload
Jede Interaktion mit TutuHai-Unterhaltungen wird durch den Host vermittelt (der Nutzer wählt aktiv eine Unterhaltung); Mini-Apps können nicht die vollständige Unterhaltungsliste erhalten. im.read ist eine sensible Funktion.
// 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);
}
Mehrspieler-Räume · im.room
Verwandle eine „Unterhaltung" in einen Echtzeit-Raum für Mini-Games / Zusammenarbeit: Raum erstellen/beitreten, im Raum persistente Nachrichten und Echtzeit-Signale (Zustandssynchronisation, ≤2KB, flüchtig, nicht persistiert). Der Host überbrückt Echtzeit-Frames als du, das Host-JWT gelangt niemals in die Mini-App; beschränkt auf Räume, die diese Mini-App erstellt hat / Unterhaltungen, in die du eingeladen wurdest — es kann nicht die anderen privaten Chats des Nutzers berühren.
// 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: Signale sind Best-Effort, und Frame-Verlust bei einer Verbindungsunterbrechung ist normal. Bei
onReconnecthydratisiere den finalen Zustand erneut ausroom.history()oder Cloud-Daten — verlasse dich nicht auf Signale als einzige Quelle der Wahrheit.
Inline-Komponenten · wie native Komponenten · datenschutzsicher
Eine mit shareToChat({inline:true}) gesendete Karte rendert eine interaktive Komponente direkt innerhalb der Chat-Blase (z. B. eine Umfrage, eine Bewertung); Empfänger bedienen sie wie eine native Funktion, ohne ein Fenster zu öffnen. Die Mini-App rendert eine kompakte UI basierend auf ctx.inline; die Blase passt ihre Größe automatisch an den Inhalt an und passt ihr Theme live an den Hell-/Dunkelmodus des Hosts an. Datenschutz: Eine Inline-Instanz erhält nur ein eingeschränktes Token „nur cloud.data, keine Nebenwirkungen" — sie kann öffentliche Collections lesen + ihre eigenen Dokumente schreiben, kann private Daten anderer nicht berühren und fragt nicht nach Autorisierung.
// —— 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.
Wort nachschlagen / übersetzen · text.lookup / text.provider
Das Wort-Nachschlage-Popover ist selbst die Inline-Seite einer „Provider-Mini-App" — sein Inhalt/seine Funktionalität wird vollständig von dieser Mini-App gerendert; der Host stellt nur die Auswahl + Hover-Positionierung + ein flexibles SDK bereit. Verbraucher (mache Text in deiner eigenen Mini-App auswählbar): deklariere text.lookup, kein Code — beim Auswählen von Text erscheint die Inline-Seite des Providers direkt unter der Auswahl (auch Text von Chat-Nachrichten wird unterstützt). Provider (baue eine Nachschlage-Mini-App): deklariere text.provider (nach Prüfung gewährt), die Inline-Seite empfängt Wörter über tt.text.onLookup und rendert sich selbst; welcher Provider aktiv ist, wird im Admin-Panel konfiguriert — wenn keiner konfiguriert/autorisiert ist, ist das Nachschlagen deaktiviert. Füge data-tt-no-lookup hinzu, um einen Bereich auszuschließen.
// ── 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
Dateien & Cloud-Drive
Chat-Dateien verarbeiten · media.upload / im.share
Wenn ein Nutzer bei einem Bild/einer Datei im Chat auf „Öffnen mit" tippt, kann er deine Mini-App auswählen, um es zu verarbeiten — vorausgesetzt, du hast in den fileHandlers der manifest.json einen passenden Dateityp deklariert (image/video/audio/pdf/office/text/any). Nach dem Öffnen: getContextFile() holt die Datei, readFile() ruft die Bytes gleichem Origin über den Host ab (vermeidet CORS, sodass du Bilder/Audio/Video/jedes Format analysieren kannst), dann sendet nach der Verarbeitung uploadFile() + sendFileToChat() sie zurück in die Unterhaltung, oder saveFile() lädt sie herunter — ein nahtloser Ablauf.
// —— 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 (benötigt media.upload)
Wähle Dateien aus Tutu Drive aus oder speichere deine Ausgabe in das Drive (host-vermittelt: der Nutzer wählt Dateien nacheinander im Drive-Picker innerhalb der Host-Seite; die Mini-App hält kein Drive-Token). Wird abgelehnt, wenn das Drive nicht erreichbar ist / die Tutu-ID nicht föderiert ist — nach .catch kann die Mini-App auf ein lokales uploadFile zurückfallen.
// 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 }
App-übergreifende Interaktion
App-übergreifende Interaktion · Ziehen zwischen Apps · tt.link / tt.tray / tt.drag / tt.drop (keine Autorisierung nötig)
Mehrere Mini-Apps können gleichzeitig geöffnet sein (als Combo gespeichert, um mit einem Klick zusammen geöffnet zu werden, Mehrfenster-Layouts 2/3/4, eine angedockte Seitenleiste — alles vom Host verwaltet, kein Code nötig) und über die folgenden Funktionen interagieren:
tt.link: synchronisiere Ereignisse/Zustand in Echtzeit mit anderen geöffneten Mini-Apps (Broadcast oder gezielt, ≤2KB, ratenbegrenzt 25/s; verworfen beim Fensterschließen, nicht persistiert, nicht nutzer-/unterhaltungsübergreifend).tt.tray: hebe Inhalt in das Host-Tray, um ihn weiterzuleiten, und injiziere ihn dann in eine andere Mini-App oder Unterhaltung.tt.drag.start/tt.drag.bind: starte einen Drag / binde ein Element als Drag-Quelle, die direkt in eine andere Mini-App gezogen werden kann (natives HTML5-Drag-and-Drop zwischen Mini-Apps gleichen Origins).tt.drop.accept: die gesamte Mini-App kann Drops / Tray-Injektionen empfangen.tt.drop.zone: ★lasse ein bestimmtes internes Element einen Drop erkennen (mit Hover-Hervorhebungs-Feedback) und darauf reagieren. Eine Mini-App kann mehrere Zonen haben, die jeweils unabhängig erkennen.
Sicherheit: eine file-verpackte URL akzeptiert nur die /uploads/-Handles dieser Website (der Empfänger ruft Bytes über tt.readFile ab); gefälschte cross-origin externe Links werden verworfen; text/json/name haben alle Größenobergrenzen.
// —— ① 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 · Fenster · Host
UI · Fenster · Gerät (keine Autorisierung nötig)
WeChat-wx.*-artige UI-Funktionen — der Host rendert echte Toasts/Dialoge/Bildbetrachter, steuert das Kapsel-Fenster der Mini-App und greift auf Zwischenablage/Vibration/Anruf/Standort/Netzwerk zu — aufrufbar ohne Autorisierung anzufordern, was eine Mini-App so leistungsfähig wie eine native App macht.
// —— 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|...}
Schwebefenster · Vollbild · Branding
Steuere das Mini-App-Fenster: öffne ein ziehbares Schwebefenster aus einer Inline-Komponente/einer Auswahl, erweitere auf Vollbild / stelle wieder her, färbe die Host-Kapsel ein und lausche auf Anzeige-/Größenänderungen.
// —— 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 · Haptik · Telegram-Stil (keine Autorisierung nötig)
Eine Mini-App steuert die Buttons der Host-Chrome und empfängt deren Klick-Ereignisse (in beide Richtungen) — den unteren Hauptbutton mainButton, den Zurück-Button in der Kopfzeile backButton, die Haptik hapticFeedback. Dies lässt eine Mini-App tief mit der Host-UI verschmelzen (statt eine isolierte Seite zu sein).
// 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-Speicher · Telegram-Stil
colorScheme / themeParams halten die Farben der Mini-App konsistent mit dem Host und wechseln mit Hell-/Dunkelmodus; locale synchronisiert sich mit der i18n des Hosts.
// —— 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')
Referenz
Fehlerbehandlung
Jeder window.tt.* gibt ein Promise zurück und lehnt bei einem Fehlschlag mit einem Error ab. Produktions-Mini-Apps müssen jeden Aufruf mit .catch / try-catch behandeln. Häufige err.message:
| err.message | Bedeutung / empfohlene Behandlung |
|---|---|
User denied authorization |
Die bedarfsgesteuerte Autorisierungsaufforderung wurde verweigert → zu einem erneuten Versuch anleiten |
User cancelled |
Der Unterhaltungs-Picker/die Vorschau wurde abgebrochen → still bleiben |
Call timed out |
Der Host war lange nicht ansprechbar (selten) → zu einem erneuten Versuch auffordern |
Data too large / too many documents / too many storage items |
Kontingent überschritten → die Daten reduzieren |
public reads are limited to pub_-prefixed public collections |
Collection-Benennung passt nicht → ein pub_-Präfix verwenden |
Forbidden (403) |
Ein Nicht-Entwickler nutzte scope=all → keine Berechtigung |
Network error / Failed to fetch |
Netzwerkfehler → freundliche Aufforderung + erneuter Versuch |
// 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 */
}
});
Autorisierungsmodell
Bedarfsgesteuerte Autorisierung: Das Öffnen einer Mini-App erfordert nicht, alle Berechtigungen im Voraus zu gewähren; der Host fragt nur nach einem einzelnen Element, wenn eine Funktion zum ersten Mal aufgerufen wird (erlauben/verweigern). Der Nutzer kann „dieser Mini-App vertrauen", um alles auf einmal zu gewähren, oder Elemente einzeln umschalten und Nutzungsprotokolle auf der Einstellungsseite ansehen. Das Backend validiert weiterhin bei jedem Aufruf erneut (Defense in Depth). UI-/Fenster-/Geräte-/Theme-/App-übergreifende Funktionen sind ohne Autorisierung nutzbar.
| Funktion (Scope) | Beschreibung | Sensibel |
|---|---|---|
user.profile |
Spitzname und Avatar abrufen | — |
cloud.data |
Cloud-Daten (Collections; Bestellungen/Datensätze usw.) | — |
storage.kv |
Datenspeicher (KV) | — |
media.upload |
Bilder/Dateien hochladen (inkl. Drive tt.drive) | — |
im.share |
Eine Karte in einen Chat teilen | — |
im.send |
Nachrichten senden | — |
im.read |
Unterhaltungsverlauf lesen | Sensibel |
im.room |
Mehrspieler-Räume: Nachrichten senden/empfangen und Raum-Chat in deinem Namen lesen | Sensibel |
text.lookup |
Wort nachschlagen/übersetzen (der ausgewählte Text wird an einen Übersetzungsdienst gesendet) | Sensibel |
text.provider |
Nachschlage-Provider (die Seite dieser Mini-App fungiert als Nachschlage-/Übersetzungs-Popover) | Sensibel |
Sichtbarkeit
- Öffentlich: erscheint in „Entdecken" und der Suche; jeder kann sie finden.
- Nicht gelistet: nicht in Entdecken/Suche; nur über eine geteilte Karte oder einen kopierten Link (Deep Link) erreichbar — für die Verbreitung im privaten Umfeld ohne öffentliche Sichtbarkeit. In der Konsole mit einem Klick umschaltbar.
- Öffnungsmodus (Desktop): der „Öffnungsmodus" der Konsole schaltet zwischen „schwebend (Standard) / Vollbild" um — Canvas-/Whiteboard-/Editor-Apps setzen
fullscreen, um beim Öffnen den Bildschirm auszufüllen; leichtgewichtige Karten/Formulare nutzen schwebend. Du kannst den Anfangswert auch imdisplaydermanifest.jsondeklarieren. Mobil ist immer Vollbild, unabhängig von dieser Einstellung.
Versionen & Veröffentlichung
Versionsmodell: Entwurf → in Prüfung → bereit → live.
Alle historischen Versionen werden aufbewahrt, mit Ein-Klick-Rollback auf jede historische Version (sofortiger Austausch der Live-Version). Ablehnungen zeigen den Grund im Benachrichtigungszentrum.
Beschränkungen & Kontingente
- Cloud-Daten: einzelnes Dokument ≤ 8KB, ≤ 500 Dokumente pro (Mini-App, Nutzer),
list()≤ 200 auf einmal (nach neuesten); nutzelistPage()Cursor-Paginierung (mine/all) für mehr. Hinweis: Direkte Frontend-Aggregation (Zählen/Mitteln) mitlist()zählt den ältesten Teil zu niedrig, wenn eine Collection 200 überschreitet, und liefert ein zu niedriges Ergebnis — für vollständige Summen nutze listPage-Paginierung oder akzeptiere eine Näherung. - KV: einzelner Wert ≤ 8KB, ≤ 64 Schlüssel pro (Mini-App, Nutzer).
- Hochgeladene Bilder ≤ 4MB; hochgeladene Dateien ≤ 20MB; das Senden von Nachrichten ist ratenbegrenzt (≤ 20 pro Nutzer pro Minute); Raum-Signal-Payload ≤ 2KB; tt.link einzelne Nachricht ≤ 2KB, gedrosselt 25/s.
- Das Code-Bundle ist eine Single-File-HTML (bevorzuge reines DOM-Rendering; vermeide
innerHTML, um XSS zu verhindern). - Tokens sind kurzlebig (etwa 2 Stunden); der Host erneuert sie nach Ablauf stillschweigend; Funktionsaufrufe werden vom Backend erneut validiert.
Referenzbeispiele: das
applets/food(Bestellung, Cloud-Daten) undapplets/repair(Reparaturanfragen) des Repositorys sind beide reine Frontend- + Cloud-Daten-Mini-Apps.