Tài liệu mở của Mini-App TutuHai
Tích hợp SDK · API năng lực · Dữ liệu đám mây · Cấp quyền · Phát hành
Mini-app TutuHai là các ứng dụng nhẹ chạy bên trong TutuHai. Lập trình viên chỉ tải lên một gói mã frontend, được nền tảng lưu trữ; các năng lực giao tiếp với TutuHai thông qua SDK window.tt, không cần backend riêng của bạn (dữ liệu nghiệp vụ đi qua cloud data của TutuHai). Tích hợp SDK · API năng lực · cloud data · phân quyền · phát hành — tất cả trên một trang, với các ví dụ chép-và-chạy.
Mục lục · 26 chủ đề
Bắt đầu
Năng lực dữ liệu
Trò chuyện & nhiều người chơi
File & ổ đĩa đám mây
Tương tác giữa các ứng dụng
UI · cửa sổ · host
Tham khảo
Bắt đầu
Giới thiệu
Mini-app TutuHai là các ứng dụng nhẹ chạy bên trong TutuHai. Lập trình viên chỉ tải lên một gói mã frontend, được nền tảng lưu trữ; các năng lực giao tiếp với TutuHai thông qua SDK window.tt, không cần backend riêng của bạn (dữ liệu nghiệp vụ đi qua cloud data của TutuHai).
Cô lập và bảo mật: mini-app chạy trong một iframe sandbox trên một origin cô lập; phiên đăng nhập của host không bao giờ đi vào mini-app. Mỗi lần gọi được host cấp một token ngắn hạn, có giới hạn, và backend xác thực lại theo từng năng lực (scope).
Khởi động nhanh
- Trong Bảng điều khiển Mini-App (
/applets), nhấn "New" để tạo một mini-app (tên + slug duy nhất). - Viết một file HTML đơn (bao gồm SDK, gọi các năng lực qua
window.tt.*). - Tạo một phiên bản → điền các năng lực yêu cầu → tải lên gói mã (HTML file đơn).
- Gửi để duyệt → admin phê duyệt → phát hành lên production chỉ với một cú nhấp.
- Người dùng tìm/mở nó qua tìm kiếm "Discover", hoặc bạn chia sẻ một thẻ / sao chép một liên kết để truy cập trực tiếp.
Đặc tả đóng gói
Mini-app hỗ trợ hai hình thức tải lên: ① một gói HTML file đơn (tự chứa, entry = root, đơn giản nhất); ② một zip đầu ra build của framework thật (thư mục dist/ từ npm run build, với index.html + assets trải trên nhiều file — xem "Đầu ra build của framework"). Nền tảng chạy kiểm tra đặc tả và tối ưu hóa trước khi tải lên.
Cấu trúc gói (đầu ra build)
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
Gói mã phải thỏa mãn (tự động kiểm tra khi tải lên):
- Entry: một file HTML đơn với
<!doctype html>và một<html>gốc. - Phù hợp di động: phải bao gồm
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">. - SDK: bao gồm
<script src="/applet-sdk.js">(nền tảng viết lại nó thành địa chỉ tuyệt đối của host). - Tự chứa: nội tuyến CSS/JS; nếu bạn cần script bên ngoài, chỉ SDK của nền tảng + các CDN framework nổi tiếng (unpkg / jsdelivr / cdnjs / esm.sh) được phép — cấm các script từ xa tùy ý (bảo mật). Hình ảnh và media khác đi qua
tt.uploadImagehoặc một CDN. - Kích thước: HTML file đơn ≤ 1MB; zip nhiều file ≤ 8MB tổng, ≤ 1MB mỗi file, ≤ 100 file; hình ảnh tải lên ≤ 4MB.
- Dữ liệu: không có backend riêng của bạn — dữ liệu nghiệp vụ đi qua cloud data
tt.cloud/tt.*storage.
📱🖥 Di động / máy tính để bàn dùng chung (một codebase, cả hai bề mặt)
Cùng một gói chạy trong một iframe cô lập bên trong TutuHai; host mang nó trên cả di động (toàn màn hình) và máy tính để bàn (bảng điều khiển / có thể toàn màn hình). Viết một codebase dùng chung với một bố cục responsive: ① viewport-fit=cover + vùng an toàn env(safe-area-inset-*); ② lớp phủ dạng bottom sheet (di động) ↔ căn giữa (máy tính, @media(min-width:480px)); ③ vùng chạm ≥ 44px; ④ theo sáng/tối của host (tt.onThemeChange / [data-theme]); ⑤ DOM thuần, không hard-code chiều rộng. Điều này giữ trải nghiệm nhất quán trên cả điện thoại và máy tính.
Đầu ra build của framework (dist.zip)
Ngoài HTML file đơn, bạn cũng có thể tải lên đầu ra build của một framework thật — dùng React / Vue / Svelte / Angular / Solid / Astro / Next (static export) / vanilla… npm run build của bất kỳ toolchain nào, nén thư mục dist/ (với index.html + assets/*.js/css + font/hình) và tải lên để được lưu trữ.
Không phụ thuộc framework: nền tảng chỉ nhận diện một "hợp đồng gói tĩnh dùng chung" — entry
index.html+ tham chiếu asset tương đối + SDK. Bất kỳ framework nào có thể tạo ra mộtdisttĩnh đáp ứng hợp đồng đó đều được hỗ trợ; các scaffold bên dưới chỉ là lối tắt được tuyển chọn, không phải giới hạn của việc hỗ trợ.
cấu trúc zip (đầu ra build, gốc zip = gốc gói)
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…
Ba bước thích ứng (áp dụng cho bất kỳ framework nào):
- Đặt một base tương đối (khuyến nghị, an toàn nhất) — làm cho đầu ra tham chiếu asset bằng đường dẫn tương đối (
./assets/x.jsthay vì/assets/x.jstuyệt đối từ gốc), để việc lưu trữ dưới/<slug>/chắc chắn không sai. (Base mặc định cũng hoạt động: nền tảng tự động viết lại các tham chiếu tĩnh tuyệt đối từ gốc trong HTML/CSS và dùng một Referer fallback cho các asset tuyệt đối từ gốc được tạo lúc chạy — chẳng hạn preload của CSS được tách mã; nhưng trong các trường hợp biênReferrer-Policynghiêm ngặt / offline-prefetch thì Referer có thể thiếu và fallback thất bại, nên base tương đối là an toàn nhất.) - Thêm một manifest — đặt
manifest.jsonvào thư mục tĩnh (ví dụstatic/của Vite/SvelteKit,public/của hầu hết framework) để nó nằm ở gốcdistsau khi build; hoặc bỏ manifest và thêm<meta name="tt:slug" content="…">(cộng vớitt:name / tt:version / tt:scopes) trongindex.htmllàm dự phòng. - Bao gồm SDK — hai cách: ① cài đặt npm (khuyến nghị, tốt nhất cho scaffold thật): sau khi
npm i @tutuhai/applet-sdk,import { tt } from '@tutuhai/applet-sdk'— được gộp vào đầu ra lúc build, có kiểu TypeScript, không sửaindex.html; ② hoặc viết<script src="/applet-sdk.js">trongindex.html(nền tảng viết lại nó thành địa chỉ tuyệt đối của host) và dùng biến toàn cụcwindow.tt.
📦 npm SDK (đã kiểm thử với scaffold chính thức của React / Vue / Svelte)
Tạo một dự án bằng npm create vite@latest -- --template react-ts | vue-ts | svelte-ts, cài cùng @tutuhai/applet-sdk, và import nó — mã nguồn nhiều file thật, npm run build tạo ra nhiều chunk + một entry index.html; nén nó lại và tải lên.
# 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 .
Các ví dụ chạy được trong repo: applets/frameworks/{react,vue,svelte} (ba dự án scaffold chính thức thật, tất cả đều import cùng gói SDK). Mã nguồn gói SDK: applet-sdk/.
các trường của 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: khai báo hình thức mở trên máy tính — với các ứng dụng canvas/whiteboard/editor ưu tiên
fullscreen, với các thẻ/biểu mẫu nhẹ dùngwindowmặc định. Đây chỉ là một giá trị ban đầu; sau khi phát hành bạn có thể thay đổi "Chế độ mở" bất cứ lúc nào trong bảng điều khiển (bảng điều khiển thắng). Di động luôn toàn màn hình, không bị ảnh hưởng bởi trường này. Khi bỏ manifest,<meta name="tt:display" content="fullscreen">hoạt động như một dự phòng. - fileHandlers: khai báo loại file nào mini-app của bạn có thể xử lý từ một cuộc trò chuyện — khi người dùng chạm "Open with" trên một file trong trò chuyện, các mini-app đã khai báo một loại khớp sẽ xuất hiện dưới dạng ứng viên; chạm vào một cái sẽ gửi file đó thẳng vào mini-app của bạn (xem "Xử lý file trò chuyện"). Mỗi mục:
kindslà một trongimage / video / audio / pdf / office / text / any(cho phép nhiều,any= bất kỳ file nào);rolelàeditor(mở trong một trình chỉnh sửa) hoặcviewer(xem trước);labeltùy chọn (≤20 ký tự, tên hiển thị của ứng viên). Tối đa 8 mục. Kiểm soát khớp với khả năng hiển thị:private/self-usehoạt động mà không cần duyệt (chỉ xuất hiện trong "Open with" của chính bạn);publicyêu cầu admin phê duyệt trước khi nó có hiệu lực cho mọi người.
Cấu hình "base tương đối" một dòng cho mỗi 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">
⚠ SPA định tuyến phía client (SvelteKit / React Router / Vue Router / Angular) Mini-app được lưu trữ dưới đường dẫn con
/<slug>/. Một SPA định tuyến phía client phải đặt "router base" của nó thành slug của bạn, nếu không router của framework không thể khớp đường dẫn hiện tại → 404 toàn trang (asset tải được, nhưng định tuyến báo không tìm thấy). Chỉ đặt base asset tương đối là không đủ — điều đó chỉ sửa URL asset, không sửa định tuyến. Theo từng framework: SvelteKitkit.paths.base='/<slug>'; React Router<BrowserRouter basename="/<slug>">; Vue RoutercreateWebHistory('/<slug>/'); AngularAPP_BASE_HREF='/<slug>/'. (Các ứng dụng không định tuyến phía client — render thuần / một React đơn trang không có Router / vanilla — không bị ảnh hưởng.)
⚠ Kiểm tra khi tải lên Khi tải lên, nền tảng chạy một "kiểm tra hợp đồng" trên dist: entry / đường dẫn tương đối / manifest / tham chiếu SDK / giới hạn / MIME đều được xác thực với gợi ý nội tuyến. Tổng ≤ 8MB, ≤ 1MB mỗi file, ≤ 100 file, chỉ MIME trong danh sách trắng (html/css/js/json/hình ảnh/font/map/wasm). Nén và tạo tập con font để giữ kích thước nhỏ. Với các đầu ra framework nặng (ví dụ tldraw / excalidraw với một chunk đơn >1MB) vượt quá giới hạn mỗi-file/tổng mặc định, hãy nhờ nhóm vận hành nâng giới hạn "byte mỗi file" / "tổng chưa nén" trong bảng quản trị (thay đổi được lúc chạy, có hiệu lực ngay); tách
manualChunkscũng có thể đưa vendor xuống dưới giới hạn.
Ví dụ tối giản
Một mini-app hoàn chỉnh, chạy được — bao gồm SDK, đọc biệt danh của người dùng:
<!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>
Tích hợp SDK
Bao gồm script SDK trong HTML của mini-app của bạn, sau đó dùng 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>
Không bao giờ hard-code tên miền của host vào mini-app của bạn. Viết
/applet-sdk.jstương đối (hoặc bất kỳ origin giữ chỗ nào) — nền tảng viết lại URL script SDK thành host hiện tại tại thời điểm phục vụ khi ứng dụng của bạn chạy trong iframe của nó. Vậy nên ngay cả khi TutuHai đổi tên miền, hoặc một tên miền bị chặn, mọi mini-app đã phát hành vẫn tiếp tục hoạt động mà không cần thay đổi mã và không cần phát hành lại — người vận hành chỉ lật một giá trị cấu hình duy nhất.
Callback ready, context, và 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
});
Ví dụ framework
window.tt không phụ thuộc framework và hoạt động trực tiếp trong tất cả các framework chính (file đơn, không build). Mỗi ví dụ bao gồm xử lý cả thành công ✅ và thất bại ❌ (phân quyền bị từ chối / lỗi mạng):
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)) // ❌
);
}
}
Các ví dụ chạy được đầy đủ có trong repo: applet-platform/samples/demo-react.html, demo-svelte.html, demo-vue.html.
Năng lực dữ liệu
Hồ sơ người dùng · 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
Các collection có cấu trúc được lưu trữ cho phép một mini-app lưu bền dữ liệu nghiệp vụ mà không cần backend riêng của nó. Ba tầng khả năng hiển thị:
mine: chỉ đọc/ghi tài liệu của riêng bạn (mặc định).all: đọc/ghi mọi thứ, chỉ lập trình viên mini-app (chủ sở hữu) — dành cho một "bảng điều khiển thương gia" để xem tất cả đơn hàng/phiếu.public: bất kỳ người dùng đã đăng nhập nào cũng có thể đọc mọi thứ, tên collection phải bắt đầu bằngpub_— dành cho cộng đồng/chợ/diễn đàn; việc ghi và chỉnh sửa vẫn giới hạn ở tác giả.
// 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);
}
Mỗi hàng đọc lại có dạng { id, ownerId, mine, data:{…your fields}, createdAt, updatedAt } — các trường của bạn đều nằm trong data (ví dụ row.data.title).
Lưu trữ KV · storage.kv
Key-value được cô lập theo (mini-app, người dùng), tốt cho trạng thái riêng tư nhỏ như số lần điểm danh, bản nháp, v.v. tt.cloudStorage (setItem/getItem/getKeys/removeItem) là bí danh kiểu Telegram của nó.
// 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');
Trò chuyện & nhiều người chơi
Năng lực trò chuyện · im.share / im.send / im.read / media.upload
Mọi tương tác với các cuộc trò chuyện TutuHai đều được host trung gian (người dùng chủ động chọn một cuộc trò chuyện); mini-app không thể lấy toàn bộ danh sách trò chuyện. im.read là một năng lực nhạy cảm.
// 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);
}
Phòng nhiều người chơi · im.room
Biến một "cuộc trò chuyện" thành một phòng thời gian thực cho mini-game / cộng tác: tạo/tham gia một phòng, tin nhắn lưu bền trong phòng và tín hiệu thời gian thực (đồng bộ trạng thái, ≤2KB, thoáng qua, không lưu bền). Host bắc cầu các khung thời gian thực với tư cách là bạn, JWT của host không bao giờ đi vào mini-app; giới hạn ở các phòng mà mini-app này đã tạo / các cuộc trò chuyện bạn được chia sẻ vào — nó không thể chạm vào các cuộc trò chuyện riêng tư khác của người dùng.
// 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
Bù nước khi kết nối lại: các tín hiệu là best-effort và mất khung khi mất kết nối là bình thường. Khi
onReconnect, hãy bù lại trạng thái cuối cùng từroom.history()hoặc cloud data — đừng dựa vào tín hiệu như nguồn chân lý duy nhất.
Thành phần nội tuyến · giống thành phần gốc · an toàn quyền riêng tư
Một thẻ được gửi bằng shareToChat({inline:true}) render một thành phần tương tác ngay bên trong bong bóng trò chuyện (ví dụ một cuộc bình chọn, một đánh giá); người nhận thao tác nó như một tính năng gốc mà không mở một cửa sổ nổi. Mini-app render một UI gọn dựa trên ctx.inline; bong bóng tự chỉnh kích thước theo nội dung và đổi theme trực tiếp theo sáng/tối của host. Quyền riêng tư: một phiên bản nội tuyến chỉ nhận một token giới hạn "cloud.data mà thôi, không tác dụng phụ" — nó có thể đọc các collection công khai + ghi tài liệu của riêng nó, không thể chạm vào dữ liệu riêng tư của người khác, và không nhắc phân quyền.
// —— 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.
Tra từ / dịch · text.lookup / text.provider
Popover tra từ bản thân nó chính là trang nội tuyến của một "mini-app nhà cung cấp" — nội dung/chức năng của nó đều do mini-app đó render; host chỉ cung cấp phần chọn văn bản + định vị khi rê chuột + một SDK linh hoạt. Bên tiêu thụ (làm cho văn bản trong mini-app của bạn có thể chọn được): khai báo text.lookup, không cần mã — chọn văn bản sẽ bật trang nội tuyến của nhà cung cấp ngay dưới vùng chọn (văn bản tin nhắn trò chuyện cũng được hỗ trợ). Nhà cung cấp (xây một mini-app tra từ): khai báo text.provider (được cấp sau khi duyệt), trang nội tuyến nhận các từ qua tt.text.onLookup và tự render; nhà cung cấp nào đang hoạt động được cấu hình trong bảng quản trị — nếu không cái nào được cấu hình/phân quyền, tra từ bị tắt. Thêm data-tt-no-lookup để loại trừ một vùng.
// ── 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
File & ổ đĩa đám mây
Xử lý file trò chuyện · media.upload / im.share
Khi người dùng chạm "Open with" trên một hình ảnh/file trong trò chuyện, họ có thể chọn mini-app của bạn để xử lý nó — miễn là bạn đã khai báo một loại file khớp trong fileHandlers của manifest.json (image/video/audio/pdf/office/text/any). Sau khi mở: getContextFile() lấy file, readFile() tải các byte cùng-origin qua host (tránh CORS, nên bạn có thể phân tích hình ảnh/âm thanh/video/bất kỳ định dạng nào), rồi sau khi xử lý uploadFile() + sendFileToChat() gửi nó lại vào cuộc trò chuyện, hoặc saveFile() tải nó về — một luồng liền mạch.
// —— 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 (cần media.upload)
Chọn file vào từ Tutu Drive, hoặc lưu đầu ra của bạn vào ổ đĩa (host trung gian: người dùng chọn file từng cái một trong bộ chọn ổ đĩa bên trong trang host; mini-app không giữ token ổ đĩa). Bị từ chối khi ổ đĩa không truy cập được / Tutu ID chưa được liên kết — sau .catch, mini-app có thể dự phòng về một uploadFile cục bộ.
// 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 }
Tương tác giữa các ứng dụng
Tương tác giữa các ứng dụng · kéo giữa các ứng dụng · tt.link / tt.tray / tt.drag / tt.drop (không cần phân quyền)
Nhiều mini-app có thể mở cùng lúc (được lưu thành một combo để mở cùng nhau chỉ với một cú nhấp, bố cục đa cửa sổ 2/3/4, một thanh bên được ghim — tất cả do host quản lý, không cần mã), và tương tác qua các năng lực sau:
tt.link: đồng bộ sự kiện/trạng thái thời gian thực với các mini-app đang mở khác (phát rộng hoặc nhắm mục tiêu, ≤2KB, giới hạn tần suất 25/giây; mất khi đóng cửa sổ, không lưu bền, không xuyên người dùng/cuộc trò chuyện).tt.tray: nhặt nội dung lên khay của host để chuyển tiếp nó, sau đó tiêm vào một mini-app hoặc cuộc trò chuyện khác.tt.drag.start/tt.drag.bind: bắt đầu một thao tác kéo / gắn một phần tử làm nguồn kéo có thể được kéo thẳng vào một mini-app khác (kéo-thả HTML5 gốc giữa các mini-app cùng origin).tt.drop.accept: cả mini-app có thể nhận thả / tiêm từ khay.tt.drop.zone: ★cho phép một phần tử nội bộ cụ thể cảm nhận một lần thả (với phản hồi highlight khi rê chuột) và hành động dựa trên nó. Một mini-app có thể có nhiều vùng, mỗi vùng cảm nhận độc lập.
Bảo mật: một url được bọc dạng file chỉ chấp nhận các handle /uploads/ của site này (bên nhận tải các byte qua tt.readFile); các liên kết ngoài xuyên origin giả mạo bị loại bỏ; text/json/name đều có giới hạn kích thước.
// —— ① 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 · cửa sổ · host
UI · cửa sổ · thiết bị (không cần phân quyền)
Các năng lực UI giống wx.* của WeChat — host render các toast/dialog/trình xem hình ảnh thật, điều khiển cửa sổ dạng viên nhộng của mini-app, và truy cập clipboard/rung/gọi điện/vị trí/mạng — có thể gọi mà không cần yêu cầu phân quyền, khiến một mini-app mạnh mẽ như một ứng dụng gốc.
// —— 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|...}
Cửa sổ nổi · toàn màn hình · thương hiệu
Điều khiển cửa sổ mini-app: mở một cửa sổ nổi có thể kéo từ nội tuyến/một vùng chọn, mở rộng ra toàn màn hình / khôi phục, tô màu viên nhộng của host, và lắng nghe các thay đổi hiển thị/kích thước.
// —— 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');
Nút host · rung · kiểu Telegram (không cần phân quyền)
Một mini-app điều khiển các nút của khung host và nhận các sự kiện nhấp của chúng (hai chiều) — nút chính ở dưới mainButton, nút quay lại ở tiêu đề backButton, rung hapticFeedback. Điều này cho phép một mini-app tích hợp sâu với UI của host (thay vì là một trang cô lập).
// 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 · lưu trữ đám mây · kiểu Telegram
colorScheme / themeParams giữ màu sắc của mini-app nhất quán với host và chuyển đổi theo sáng/tối; locale đồng bộ với i18n của host.
// —— 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')
Tham khảo
Xử lý lỗi
Mỗi window.tt.* trả về một Promise và từ chối một Error khi thất bại. Các mini-app production phải .catch / try-catch mọi lần gọi. Các err.message thường gặp:
| err.message | Ý nghĩa / cách xử lý gợi ý |
|---|---|
User denied authorization |
Lời nhắc phân quyền theo yêu cầu bị từ chối → hướng dẫn thử lại |
User cancelled |
Bộ chọn/xem trước cuộc trò chuyện bị hủy → giữ im lặng |
Call timed out |
Host không phản hồi trong thời gian dài (hiếm) → nhắc thử lại |
Data too large / too many documents / too many storage items |
Vượt hạn mức → cắt bớt dữ liệu |
public reads are limited to pub_-prefixed public collections |
Đặt tên collection không khớp → dùng tiền tố pub_ |
Forbidden (403) |
Một người không phải lập trình viên dùng scope=all → không có quyền |
Network error / Failed to fetch |
Mạng thất bại → nhắc thân thiện + thử lại |
// 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 */
}
});
Mô hình phân quyền
Phân quyền theo yêu cầu: mở một mini-app không yêu cầu cấp tất cả các quyền ngay từ đầu; host nhắc một mục duy nhất chỉ khi một năng lực được gọi lần đầu (cho phép/từ chối). Người dùng có thể "tin cậy mini-app này" để cấp tất cả cùng lúc, hoặc bật/tắt từng mục riêng lẻ và xem bản ghi sử dụng trên trang cài đặt. Backend vẫn xác thực lại mỗi lần gọi (phòng thủ theo chiều sâu). Các năng lực UI/cửa sổ/thiết bị/theme/xuyên-ứng-dụng có thể dùng được mà không cần phân quyền.
| Năng lực (scope) | Mô tả | Nhạy cảm |
|---|---|---|
user.profile |
Lấy biệt danh và ảnh đại diện của bạn | — |
cloud.data |
Cloud data (collection; đơn hàng/bản ghi v.v.) | — |
storage.kv |
Lưu trữ dữ liệu (KV) | — |
media.upload |
Tải lên hình ảnh/file (bao gồm ổ đĩa tt.drive) | — |
im.share |
Chia sẻ một thẻ vào trò chuyện | — |
im.send |
Gửi tin nhắn | — |
im.read |
Đọc lịch sử trò chuyện | Nhạy cảm |
im.room |
Phòng nhiều người chơi: gửi/nhận tin nhắn và đọc chat phòng thay mặt bạn | Nhạy cảm |
text.lookup |
Tra từ/dịch (văn bản được chọn được gửi đến một dịch vụ dịch) | Nhạy cảm |
text.provider |
Nhà cung cấp tra từ (trang của mini-app này đóng vai trò popover tra từ/dịch) | Nhạy cảm |
Khả năng hiển thị
- Công khai: xuất hiện trong "Discover" và tìm kiếm; ai cũng có thể tìm thấy nó.
- Không niêm yết: không có trong discover/tìm kiếm; chỉ truy cập được qua một thẻ được chia sẻ hoặc liên kết được sao chép (deep link) — dành cho lan truyền lưu lượng riêng tư mà không phơi bày công khai. Bật/tắt nó trong bảng điều khiển chỉ với một cú nhấp.
- Chế độ mở (máy tính): "Chế độ mở" của bảng điều khiển bật/tắt "nổi (mặc định) / toàn màn hình" — các ứng dụng canvas/whiteboard/editor đặt
fullscreenđể lấp đầy màn hình khi mở; các thẻ/biểu mẫu nhẹ dùng nổi. Bạn cũng có thể khai báo giá trị ban đầu trongdisplaycủamanifest.json. Di động luôn toàn màn hình, không bị ảnh hưởng bởi cài đặt này.
Phiên bản & phát hành
Mô hình phiên bản: draft → in review → ready → live.
Mọi phiên bản lịch sử đều được giữ lại, với khôi phục một cú nhấp về bất kỳ phiên bản lịch sử nào (hoán đổi tức thì phiên bản live). Các trường hợp bị từ chối hiển thị lý do trong trung tâm thông báo.
Ràng buộc & hạn mức
- Cloud data: mỗi tài liệu ≤ 8KB, ≤ 500 tài liệu mỗi (mini-app, người dùng),
list()≤ 200 mỗi lần (theo mới nhất); dùng phân trang con trỏlistPage()(mine/all) để lấy thêm. Lưu ý: thực hiện tổng hợp phía frontend (đếm/tính trung bình) trực tiếp bằnglist()sẽ đếm thiếu phần cũ nhất khi một collection vượt 200 và cho kết quả thấp — để có tổng đầy đủ hãy dùng phân trang listPage hoặc chấp nhận một giá trị xấp xỉ. - KV: mỗi giá trị ≤ 8KB, ≤ 64 khóa mỗi (mini-app, người dùng).
- Hình ảnh tải lên ≤ 4MB; file tải lên ≤ 20MB; việc gửi tin nhắn bị giới hạn tần suất (≤ 20 mỗi người dùng mỗi phút); payload tín hiệu phòng ≤ 2KB; một tin nhắn tt.link ≤ 2KB, bị điều tiết 25/giây.
- Gói mã là một HTML file đơn (ưu tiên render DOM thuần; tránh
innerHTMLđể ngăn XSS). - Token là ngắn hạn (khoảng 2 giờ); host âm thầm gia hạn sau khi hết hạn; các lần gọi năng lực được backend xác thực lại.
Ví dụ tham khảo:
applets/food(đặt món, cloud data) vàapplets/repair(yêu cầu sửa chữa) của repo đều là các mini-app frontend thuần + cloud-data.