圖圖嗨小程式是執行在圖圖嗨內的輕量應用。開發者只上傳前端程式碼包,由平臺託管;業務能力通過 window.tt SDK 與圖圖嗨通訊,無需自建後端(業務資料走圖圖嗨雲資料)。SDK 接入 · 能力 API · 雲資料 · 授權 · 釋出,一頁看全,示例可複製即用。
目錄 · 26 個主題
入門
資料能力
會話與多人
檔案與雲盤
多程式聯動
介面 · 視窗 · 宿主
參考
入門
簡介
圖圖嗨小程式是執行在圖圖嗨內的輕量應用。開發者只上傳前端程式碼包,由平臺託管;業務能力通過 window.tt SDK 與圖圖嗨通訊,無需自建後端(業務資料走圖圖嗨雲資料)。
隔離與安全:小程式跑在獨立源的沙箱 iframe,宿主登入態絕不進入小程式;每次呼叫由宿主簽發短命受限令牌,後端按能力(scope)二次校驗。
快速開始
- 在小程式控制臺(
/applets)「新建」一個小程式(名稱 + 唯一 slug)。 - 寫一個單檔案 HTML(引入 SDK,用
window.tt.*調能力)。 - 新建版本 → 填申請能力 → 上傳程式碼包(單檔案 HTML)。
- 提交稽核 → 管理員通過 → 一鍵上架為線上版本。
- 使用者在「發現」搜尋/開啟,或你分享卡片 / 複製連結直達。
打包規範
小程式支援兩種上傳形態:①單檔案 HTML 程式碼包(自包含,入口 = 根,最簡);②真實框架構建產物 zip(npm run build 的 dist/,含 index.html + assets 多檔案,見「框架構建產物」)。平臺在上傳前均做規範校驗與最佳化。
包結構(編譯產物)
your-applet/ # 開發目錄(任意結構:src、元件、資源…)
├─ src/ … # 你的原始碼(可 React / Vue / Svelte / 原生)
└─ dist/index.html # ★編譯產物:單檔案 HTML(← 上傳這個)
# 內聯 CSS/JS,或引用白名單 CDN;自包含、無需伺服器
程式碼包必須滿足(上傳時自動校驗):
- 入口:單個 HTML 檔案,含
<!doctype html>與根<html>。 - 移動端適配:必須含
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">。 - SDK:引入
<script src="/applet-sdk.js">(平臺自動改寫為宿主絕對地址)。 - 自包含:CSS/JS 內聯;如需外部指令碼,僅允許平臺 SDK + 知名框架 CDN(unpkg / jsdelivr / cdnjs / esm.sh),禁止任意遠端指令碼(安全)。圖片等媒體走
tt.uploadImage或 CDN。 - 體積:單檔案 HTML ≤ 1MB;多檔案 zip 總量 ≤ 8MB、單檔案 ≤ 1MB、檔案數 ≤ 100;上傳的圖片 ≤ 4MB。
- 資料:無自有後端,業務資料走
tt.cloud雲資料 /tt.*storage。
📱🖥 移動端 / 桌面端通用(一份程式碼兩端跑)
同一個程式碼包在圖圖嗨內以隔離 iframe 執行,宿主在移動端(全屏)與桌面端(面板 / 可全屏)均能承載。請用響應式佈局寫一份通用程式碼:①viewport-fit=cover + 安全區 env(safe-area-inset-*);②彈層用底部抽屜(移動)↔ 居中(桌面,@media(min-width:480px));③觸控目標 ≥ 44px;④跟隨宿主暗黑/淺色(tt.onThemeChange / [data-theme]);⑤純 DOM、不寫死寬度。這樣無論手機還是電腦,體驗一致。
框架構建產物(dist.zip)
除了單檔案 HTML,你也可以直接上傳真實框架的構建產物 —— 用 React / Vue / Svelte / Angular / Solid / Astro / Next(靜態匯出)/ 原生……任意工具鏈 npm run build,把 dist/(含 index.html + assets/*.js/css + 字型/圖片)打包成 zip 上傳即可託管執行。
平臺框架無關:它只認一份「通用靜態包契約」—— 入口
index.html+ 相對資源引用 + SDK。任何能產出滿足該契約的靜態dist的框架都支援,下方腳手架只是精選便捷模板,不是支援邊界。
zip 結構(build 產物,zip 根 = 包根)
myapp.zip
├─ index.html # ★入口(zip 根)
├─ manifest.json # 宣告 slug/name/version/scopes(見下)
└─ assets/
├─ index-*.js # 構建後的 JS(相對引用)
├─ index-*.css
└─ font/img…
三步適配(任意框架通用):
- 設相對 base(推薦,最穩妥) —— 讓產物用相對路徑引資源(
./assets/x.js而非根絕對/assets/x.js),託管在/<slug>/下萬無一失。(預設 base 也能跑:平臺會自動重寫 HTML/CSS 的靜態根絕對引用,並用 Referer 兜底執行時生成的根絕對資源——如程式碼分包 CSS 的 preload;但嚴格Referrer-Policy/ 離線預取等極端場景 Referer 可能缺失致兜底失效,故相對 base 最保險。) - 放 manifest —— 把
manifest.json放到靜態目錄(如 Vite/SvelteKit 的static/、多數框架的public/),build 後自動落到dist根;或省略 manifest,在index.html加<meta name="tt:slug" content="…">(及tt:name / tt:version / tt:scopes)兜底。 - 引 SDK —— 兩種方式任選:①npm 安裝(推薦,真實腳手架首選)
npm i @tutuhai/applet-sdk後import { tt } from '@tutuhai/applet-sdk',構建時打進產物、帶 TypeScript 型別、無需改index.html;②或在index.html寫<script src="/applet-sdk.js">(平臺自動改寫為宿主絕對地址)用全域性window.tt。
📦 npm SDK(React / Vue / Svelte 官方腳手架實測)
用 npm create vite@latest -- --template react-ts | vue-ts | svelte-ts 建工程,裝同一個 @tutuhai/applet-sdk,import 使用即可 —— 真實原始碼多檔案、npm run build 官方編譯產出多 chunk + 一個入口 index.html,打包成 zip 上傳。
# 1) 官方腳手架建工程
npm create vite@latest my-applet -- --template react-ts # 或 vue-ts / svelte-ts
# 2) 裝 SDK(三框架同一個包)
npm i @tutuhai/applet-sdk
# 3) 原始碼裡 import 使用(帶型別)
# 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: '你好' }); // 雲資料,無需自有後端
# 4) vite.config:相對 base;public/manifest.json 宣告 slug/name/scopes
# export default { base: './', plugins: [react()] }
# 5) 官方編譯 → 打包 dist → 上傳
npm run build && cd dist && zip -r ../my-applet.zip .
倉內可執行範例:applets/frameworks/{react,vue,svelte}(三個官方腳手架真實工程,均 import 同一個 SDK 包)。SDK 包源:applet-sdk/。
manifest.json 欄位
{
"slug": "myapp", // ★必填,全域性唯一,決定託管路徑 /myapp/
"name": "我的小程式", // ★必填,展示名
"version": "1.0.0", // ★必填,每次上傳須遞增
"scopes": ["user.profile"], // 申請的能力(見「授權模型」)
"description": "一句話簡介", // 選填,發現頁/詳情展示
"icon": "icon.png", // 選填,包內相對路徑(亦可上傳後在控制台換)
"display": "fullscreen", // 選填,預設開啟方式:window(浮窗,預設)| fullscreen(全屏)
"fileHandlers": [ // 選填,宣告「開啟方式」——能處理對話裡哪類檔案
{ "kinds": ["image"], "role": "editor", "label": "圖圖修圖·修圖" }
]
}
- display:宣告桌面開屏形態——畫板/白板/編輯器類建議
fullscreen,輕量卡片/表單用預設window。僅作初值,上架後可在控制台「開啟方式」隨時改(以控制台為準)。移動端始終全屏,不受此欄位影響。省略 manifest 時可用<meta name="tt:display" content="fullscreen">兜底。 - fileHandlers:宣告你的小程式能處理對話裡的哪類檔案——使用者在聊天對某條檔案點「開啟方式」,聲明瞭匹配型別的小程式就會出現在候選裡,點選即把該檔案直接送進你的小程式(見「處理對話檔案」)。每項:
kinds取image / video / audio / pdf / office / text / any(可多選,any=任意檔案);role取editor(在編輯器中開啟)或viewer(預覽檢視);label選填(≤20 字,候選項顯示名)。最多 8 項。生效門控與可見性一致:自用/私享免審即用(僅你自己的「開啟方式」裡出現),公開需管理員稽核通過後對所有人生效。
各框架「相對 base」一句話配置:
// Vite (React/Vue/Svelte/Solid/Preact/Lit…)
export default { base: './' }
// SvelteKit(客戶端路由 → base 必須=slug)—— 靜態匯出 + base 設為你的 slug(否則路由 404)
import adapter from '@sveltejs/adapter-static';
export default { kit: {
adapter: adapter({ fallback: 'index.html' }),
paths: { base: '/你的slug', relative: true }
} };
// Astro —— astro.config.mjs
export default { base: './', build: { assets: 'assets' } }
# Angular —— 構建時設相對基址
ng build --base-href ./ --output-path dist
// Next.js(靜態匯出)—— next.config.js
module.exports = { output: 'export', images: { unoptimized: true }, assetPrefix: './' }
// Nuxt 3(靜態)—— nuxt.config.ts
export default defineNuxtConfig({ app: { baseURL: './', cdnURL: './' }, ssr: false })
// Vue CLI / webpack —— vue.config.js(或 webpack output.publicPath)
module.exports = { publicPath: './' }
<!-- 原生 / 無構建:直接寫相對路徑即可 -->
<script src="./app.js"></script>
<link rel="stylesheet" href="./style.css">
⚠ 客戶端路由的 SPA(SvelteKit / React Router / Vue Router / Angular) 小程式託管在
/<slug>/子路徑下。帶客戶端路由的 SPA 必須把「路由 base」設為你的 slug,否則框架路由匹配不到當前路徑 → 整頁 404(資源能載入,但路由報 not found)。僅設相對資源 base 不夠——那隻修資源 URL,不修路由。各框架:SvelteKitkit.paths.base='/<slug>';React Router<BrowserRouter basename="/<slug>">;Vue RoutercreateWebHistory('/<slug>/');AngularAPP_BASE_HREF='/<slug>/'。(無客戶端路由的應用——純渲染 / 單頁 React 無 Router / 原生——不受影響。)
⚠ 上傳體檢 上傳時平臺對 dist 做「契約體檢」:入口 / 相對路徑 / manifest / SDK 引用 / 限額 / MIME 逐項校驗並行內提示。總量 ≤ 8MB、單檔案 ≤ 1MB、檔案數 ≤ 100、僅白名單 MIME(html/css/js/json/圖片/字型/map/wasm)。含字型的產物請壓縮子集化以控體積。重量級框架產物(如 tldraw / excalidraw 含 >1MB 單 chunk)超預設單檔案/總量限額時,可請運營方在後臺調高「單檔案位元組」「解壓總量」上限(執行時可改、即時生效);拆分
manualChunks亦可把 vendor 壓到限額內。
最小示例
一個完整能跑的小程式 —— 引入 SDK,讀取使用者暱稱:
<!doctype html>
<html>
<body>
<div id="who">載入中…</div>
<!-- 相對路徑即可,免維護:換域名/被封都無需改程式碼(平臺託管時自動改寫為宿主絕對址) -->
<script src="/applet-sdk.js"></script>
<script>
window.tt.ready(function () {
window.tt.getProfile().then(function (me) {
document.getElementById('who').textContent = '你好,' + me.nickname;
});
});
</script>
</body>
</html>
SDK 接入
在小程式 HTML 引入 SDK 指令碼,之後即可用 window.tt:
<!-- 在你的小程式 HTML 裡引入 SDK。用相對路徑即可,別寫死域名 -->
<script src="/applet-sdk.js"></script>
不要把宿主域名硬編碼進小程式。 寫相對的
/applet-sdk.js(或隨便一個佔位源)即可——平臺在 iframe 託管下發時會自動把 SDK 指令碼地址改寫成當前執行的宿主域。這樣即使圖圖嗨換域名、或某個域名被封,已上架的所有小程式都無需改程式碼、無需重新發布,由運維改一處配置即全量生效。
就緒回撥、上下文與主題/語言:
// 就緒後拿到上下文(appId / 已授權能力 / 深鏈 path / query / 主題 / 語言 / 是否內聯)
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(); // 隨時取當前上下文快照(等價 ready 的 ctx)
// 主題切換(宿主明暗切換會即時回撥)
window.tt.onThemeChange(function (theme) {
document.documentElement.setAttribute('data-theme', theme);
});
// 語言切換(隨宿主 i18n 同步;宿主切語言會即時回撥 —— 與主題同機制)
window.tt.onLocaleChange(function (locale) { // 如 'zh-CN' / 'en-US'
document.documentElement.setAttribute('lang', locale); // SDK 已自動設,可再本地化你的文案
});
框架示例
window.tt 與框架無關,主流框架均可直接使用(單檔案、免構建)。每個示例都含成功 ✅ 與失敗 ❌(拒絕授權 / 網路異常)處理:
Vanilla JS
// 無框架 —— 原生 DOM
window.tt.ready(function () {
window.tt.getProfile()
.then(function (me) { // ✅ 成功
document.getElementById('who').textContent = '你好,' + me.nickname;
})
.catch(function (err) { // ❌ 失敗(使用者拒絕授權 / 網路異常)
document.getElementById('who').textContent = '獲取失敗:' + err.message;
});
});
React
// React 18 + htm(免構建)
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>獲取失敗:${err}</div>`; // ❌
return html`<div>你好 ${me ? me.nickname : '…'}</div>`; // ✅
}
Preact
// Preact + htm(免構建)
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 ? '獲取失敗:' + err : '你好 ' + (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 ? '獲取失敗:' + err : '你好 ' + (me?.nickname ?? '…') }}</div>`
}).mount('#app');
Svelte
// Svelte(runtime 編譯)
let me = $state(null), err = $state('');
window.tt.ready(() =>
window.tt.getProfile()
.then((p) => (me = p)) // ✅
.catch((e) => (err = e.message)) // ❌
);
// 模板:<div>{err ? '獲取失敗:' + err : '你好 ' + (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() ? '獲取失敗:' + err() : '你好 ' + (me()?.nickname ?? '…')}</div>;
}
Alpine.js
<!-- Alpine.js:HTML 內宣告式,零構建 -->
<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 ? '獲取失敗:' + err : '你好 ' + (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 ? '獲取失敗:' + this.err : '你好 ' + (this.me?.nickname ?? '…')}</div>`;
}
}
customElements.define('my-app', MyApp);
jQuery
// jQuery
$(function () {
window.tt.ready(function () {
window.tt.getProfile()
.then(function (me) { $('#who').text('你好,' + me.nickname); }) // ✅
.catch(function (err) { $('#who').text('獲取失敗:' + err.message); }); // ❌
});
});
Angular
// Angular(元件)
@Component({ selector: 'app-root', template: `<div>{{ msg }}</div>` })
export class AppComponent implements OnInit {
msg = '載入中…';
ngOnInit() {
const tt = (window as any).tt;
tt.ready(() =>
tt.getProfile()
.then((me: any) => (this.msg = '你好,' + me.nickname)) // ✅
.catch((e: any) => (this.msg = '獲取失敗:' + e.message)) // ❌
);
}
}
完整可執行示例見倉庫 applet-platform/samples/demo-react.html、demo-svelte.html、demo-vue.html。
資料能力
使用者資料 · user.profile
// 獲取當前使用者資料(需 user.profile;首次呼叫時宿主按需彈授權)
try {
const me = await window.tt.getProfile(); // ✅ 成功
console.log(me.userId, me.nickname, me.avatarUrl);
} catch (err) { // ❌ 失敗
// err.message: "使用者拒絕授權"(點了拒絕)/ 網路異常
console.warn('獲取資料失敗:', err.message);
}
// 使用者資料變化(自己或房間內他人改了暱稱/頭像)→ 重拉重新整理顯示
window.tt.onProfileChange(() => refreshWhoUI());
雲資料 · cloud.data
託管的結構化集合,讓小程式無需自有後端即可持久化業務資料。三檔可見性:
mine:只讀寫自己的文件(預設)。all:讀寫全部,僅小程式開發者(主體)——供「商家後臺」看所有訂單/工單。public:任何登入使用者可讀全部,集合名須以pub_開頭——供社群/市場/論壇;寫與改仍限本人。
// 雲資料:無需自有後端,業務資料由圖圖嗨平臺託管。所有呼叫返回 Promise,務必處理失敗。
try {
// 新建文件(歸屬當前使用者)
const { id } = await window.tt.cloud.add('orders', { items: cart, total: 68, status: 'pending' });
// 冪等 upsert:按 docKey 建或改(「一人一票」/ 每使用者單條狀態最常用,免先查 id 再更新)
await window.tt.cloud.put('votes', me.userId, { choice: 'A' });
// 我的文件
const mine = await window.tt.cloud.list('orders', { scope: 'mine' });
// 讀單條(按 id;不存在返回 null)
const doc = await window.tt.cloud.get('orders', id);
// 全部文件(僅開發者/主體,供商家後臺;普通使用者 → 403)
const all = await window.tt.cloud.list('orders', { scope: 'all' });
// 公開集合:集合名以 pub_ 開頭 → 任何登入使用者可讀全部(社群/市場)
const posts = await window.tt.cloud.list('pub_posts', { scope: 'public' });
// where 等值過濾(服務端按 data 單欄位過濾,大集合下按父鍵收斂,避免子文件被 200 上限截斷)
const votes = await window.tt.cloud.list('pub_votes', { scope: 'public', where: { pollId: id } });
// 超過 200 條:listPage 游標翻頁(mine/all;可帶 where),返回 { docs, nextCursor }
const pg = await window.tt.cloud.listPage('orders', { scope: 'mine', limit: 100, before: cursor });
// 欄位級更新(owner 或開發者;patch 與原 data 合併)
await window.tt.cloud.update('orders', id, { status: 'done' });
// 刪除文件(owner 或開發者;冪等)—— 補齊 CRUD,無需再靠軟刪標記堆積
await window.tt.cloud.delete('orders', id);
} catch (err) { // ❌ 失敗
// 許可權不足(403)/ 非 pub_ 集合用 public(400)/ 配額超限 / 網路
console.warn('雲資料錯誤:', err.message);
}
讀回每行形如 { id, ownerId, mine, data:{...你存的欄位}, createdAt, updatedAt },你存的欄位都在 data 裡(如 row.data.title)。
KV 儲存 · storage.kv
按(小程式,使用者)隔離的鍵值,適合存打卡數、草稿等私有小狀態。tt.cloudStorage(setItem/getItem/getKeys/removeItem)是它的 Telegram 式別名。
// 託管 KV(需 storage.kv):按(小程式,使用者)隔離,存私有狀態
try {
await window.tt.setStorage('count', 3);
const n = await window.tt.getStorage('count'); // 3(不存在返回 null)
await window.tt.removeStorage('count');
const keys = await window.tt.getStorageKeys();
} catch (err) { // ❌ 配額(≤64 鍵 / 8KB)/ 網路
console.warn('儲存失敗:', err.message);
}
// Telegram 式別名(等價上面,需 storage.kv):
await window.tt.cloudStorage.setItem('draft', '未傳送的內容');
const draft = await window.tt.cloudStorage.getItem('draft'); // 不存在返回 null
const ks = await window.tt.cloudStorage.getKeys();
await window.tt.cloudStorage.removeItem('draft');
會話與多人
會話能力 · im.share / im.send / im.read / media.upload
與圖圖嗨會話互動均由宿主中介(使用者主動選擇會話),小程式拿不到會話全量列表;im.read 為敏感能力。
// 會話能力均由宿主中介(使用者主動選擇會話);務必處理"使用者取消"與失敗。
try {
// 分享本小程式卡片到會話(需 im.share;不傳會話則宿主彈選擇器)
await window.tt.shareToChat({ title: '來投票選午餐 🍜' });
// 傳送文本通知到會話(需 im.send;宿主彈選擇器+預覽,署名"通過 X 小程式")
await window.tt.sendMessage('投票結果:蘭州拉麵勝出');
// 讀取會話訊息(需 im.read,敏感;使用者逐次主動選擇會話,非文本脫敏)
const r = await window.tt.readMessages({ limit: 30 });
// 上傳圖片(需 media.upload;傳 dataURL,返回絕對 URL)
const url = await window.tt.uploadImage(dataUrl);
} catch (err) { // ❌ 失敗
// "使用者取消"(選擇器/預覽取消)/ 拒絕授權 / "呼叫超時" / 網路
console.warn('能力呼叫失敗:', err.message);
}
多人房間 · im.room
把「會話」變成即時房間,做小遊戲 / 協作:建/入房、房內持久訊息與即時信令(state 同步,≤2KB、臨時不落庫)。宿主用你本人身份自動橋接即時幀,宿主 JWT 絕不進小程式;僅限本小程式建的房間 / 被分享入的會話,碰不到使用者其它私聊。
// 多人房間(需 im.room):建/入/離房 + 房內持久訊息 + 即時信令(≤2KB,臨時不落庫)。
const { conversationId } = await window.tt.room.create({ title: '五子棋對局' }); // 建房,宿主自動訂閱
await window.tt.room.join(conversationId); // 冪等入房;宿主自動訂閱即時幀
await window.tt.room.subscribe(conversationId); // 訂閱既有會話(如被分享入的群)的即時幀
await window.tt.room.send(conversationId, '開局!'); // 持久文本(不開小程式也可見)
window.tt.room.signal(conversationId, { type:'move', cell:4 }); // 發即時信令(state 同步)
window.tt.room.setTyping(conversationId, true); // 輸入中狀態(瞬態)
const members = await window.tt.room.members(conversationId); // 花名冊 [{userId,nickname,avatarUrl,online,isOwner}]
const past = await window.tt.room.history(conversationId, { limit: 50 }); // 重連水合(正序)
await window.tt.room.leave(conversationId); // 離房(空房間自動回收)
// 即時事件(全部在 tt.room 下):
window.tt.room.onMessage((m) => appendMsg(m)); // 新訊息 {conversationId,id,senderId,senderName,kind,text,createdAt}
window.tt.room.onSignal((s) => applyMove(s.payload));// 對手即時動作 {conversationId,senderId,payload}
window.tt.room.onPresence((p) => refreshOnline(p)); // 上/下線 {userId,online}
window.tt.room.onTyping((t) => showTyping(t)); // 輸入中 {conversationId,userId,typing}
window.tt.room.onMember(() => reloadMembers()); // 成員進/退房 {conversationId} → 重拉 members()
window.tt.room.onReconnect(() => rehydrate()); // 掉線重連 → 從 history()/cloud 重新水合當前局面
重連水合:信令 best-effort、掉線會丟幀屬正常。
onReconnect收到即應用room.history()或雲資料重新水合最終狀態,別隻靠信令做唯一真相。
內聯元件 · 原生元件式 · 隱私安全
用 shareToChat({inline:true}) 發出的卡片會直接在聊天氣泡內渲染可互動元件(如投票、打分),收件人無需開啟浮窗,像原生功能一樣操作。小程式據 ctx.inline 渲染緊湊 UI,氣泡隨內容自適應高度、隨宿主明暗即時換膚。隱私:內聯例項僅獲「僅 cloud.data、無副作用」受限令牌,只能讀公開集合 + 寫自有文件,碰不到他人私有,也不彈授權。
// —— 內聯元件:讓小程式像原生功能一樣直接在聊天氣泡裡互動(投票/打分/接龍…)——
// 1) 發一張內聯卡到會話(需 im.share):收件人無需開啟小程式,直接在氣泡內操作
await window.tt.shareToChat({ inline: true, query: { pollId }, title: '投票', height: 200 });
// 2) 小程式據 ctx.inline 渲染兩種形態
window.tt.ready((ctx) => {
if (ctx.inline) {
renderCompact(ctx.query.pollId); // 內聯:緊湊「原生元件」式 UI
window.tt.reportHeight(); // 上報高度(SDK 亦自動 ResizeObserver 上報,氣泡隨內容自適應)
// 需要完整功能時,從內聯卡開啟完整頁(浮窗/全屏):
// openBtn.onclick = () => window.tt.openFullPage('/detail?pollId=' + ctx.query.pollId);
} else {
renderFull(); // 浮窗:完整建立 UI
}
});
// 隱私:內聯例項僅獲「僅 cloud.data、無副作用」的受限令牌 —— 只能讀公開集合 + 寫自有文件,
// 碰不到他人私有;不彈授權、不汙染授權列表。敏感能力(上傳/發訊息/定位)內聯不可用。
// 暗黑/移動:內聯卡隨宿主明暗即時切換、寬度自適應,開發者無需額外適配。
劃詞查詞 / 翻譯 · text.lookup / text.provider
劃詞 popover 本身就是一個「provider 小程式」的內聯頁——內容/功能全由那個小程式渲染,宿主只給選區 + 懸浮定位 + 靈活 SDK。消費方(讓自己小程式內文字可被劃詞):宣告 text.lookup,零程式碼,劃選即在選區正下方彈 provider 內聯頁(對話訊息文本也支援)。提供方(做一個劃詞小程式):宣告 text.provider(經稽核授予),內聯頁用 tt.text.onLookup 收詞、自己渲染;哪個當生效 provider 由後臺配置,未配/未授權則劃詞不啟用。退訂區域加 data-tt-no-lookup。
// ── A. 讓「你的小程式內的文字」可被劃詞(消費方,需 text.lookup)──【零程式碼】
// manifest 宣告 "text.lookup" 後,使用者在你的小程式裡劃選文字,即在選區正下方彈出
// 【provider 小程式的內聯頁】popover(自動出詞典/翻譯)。對話訊息文本也同樣支援(宿主內建)。
// 點空白/滾動/Esc 隱藏;popover 內操作不關閉。無需寫任何 JS。
// 退訂:不想被劃詞的區域加 data-tt-no-lookup;<input type=password> 自動排除。
// ── B. 做一個「劃詞 provider 小程式」(提供方,需 text.provider,經稽核授予)──
// provider 頁(內聯模式)用 onLookup 接收宿主推來的劃詞文本(初始+每次換詞都推,常駐不過載),
// 自己查/翻/渲染成任意 UI;可用 tt.text.lookup 調內建引擎,或用自有 cloud.data 詞典。
tt.ready(function () {
tt.text.onLookup(function (text) { // 宿主推劃詞文本
tt.text.lookup(text).then(function (r) { // r={kind:'dict'|'translate',...}
render(r); // 渲染成你自己的介面(高度自適應)
});
});
});
// ── 逃生口 / 靈活 SDK 原語(任意小程式可用)──
const r = await window.tt.text.lookup('lazy', { to:'en' }); // 主動查詞/翻譯
window.tt.text.onSelect(function (sel){ /* {text, rect};註冊即接管,預設 popover 讓位 */ });
window.tt.openFloating({ path:'/detail', anchor: sel.rect, width:320, height:220 }); // 選區處開懸浮全窗
window.tt.floating.moveTo(100, 200); // 懸浮窗位置/尺寸 SDK 全程可調:setRect/moveTo/resize/close
檔案與雲盤
處理對話檔案 · media.upload / im.share
使用者在聊天裡對某條圖片/檔案點「開啟方式」,即可選你的小程式處理它——前提是你在 manifest.json 的 fileHandlers 裡聲明瞭匹配的檔案型別(image/video/audio/pdf/office/text/any)。開啟後:getContextFile() 拿到檔案,readFile() 由宿主同源代取位元組(規避跨域,可分析圖片/音影片/任意格式),處理後 uploadFile() + sendFileToChat() 發回對話或 saveFile() 下載——無縫一條龍。
// —— 處理對話檔案:使用者對某條檔案點「開啟方式」選中你的小程式進入(須在 manifest.fileHandlers 宣告匹配 kinds)——
const f = window.tt.getContextFile();
// f = {url,name,mime,size,kind:'image'|'file',conversationId,messageId} 或 null(獨立開啟時)
if (f) {
const bytes = await window.tt.readFile(f.url); // 宿主同源代取位元組(規避 iframe 跨域;僅限本站 /uploads 產物)
// bytes = {dataUrl, mime, name, size, url} —— 可喂 <img>/<video>/<audio>/canvas 分析任意格式
imgEl.src = bytes.dataUrl;
}
// —— 處理產物 → 發回對話 或 下載(需 media.upload / im.share)——
const out = canvas.toDataURL('image/webp', 0.9); // 例:圖片轉 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 // 帶上則直髮原會話(跳過選擇器);不帶則宿主彈會話選擇器
});
await window.tt.saveFile({ dataUrl: out, name: 'result.webp' }); // 或:下載到本地(宿主代下)
圖圖雲盤 · tt.drive(需 media.upload)
從圖圖雲盤選檔案進來、或把產物存進雲盤(host-mediated:使用者在宿主頁面內的雲盤選擇器裡逐次選取,小程式不持雲盤 token)。雲盤不可達 / 未聯邦圖圖 ID 時 reject —— 小程式 .catch 後可回退到本地 uploadFile。
// 從雲盤選檔案(使用者在宿主雲盤選擇器裡挑;取消返 []):
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); // 取位元組分析/展示
render(bytes.dataUrl);
}
// 把檔案存進雲盤(pullUrl 傳本站 /uploads 產物絕對化,或直接 dataUrl):
const saved = await window.tt.drive.save({ pullUrl: up.url, name: '匯出結果.png' });
// saved = { nodeId, name }
多程式聯動
多程式聯動 · 互拖 · tt.link / tt.tray / tt.drag / tt.drop(無需授權)
多個小程式可同時開啟(可存為組合一鍵同開、多窗 2/3/4 佈局、邊欄常駐——這些均由宿主管理,小程式無需寫碼),並用下列能力彼此聯動:
tt.link:與同時開啟的其它小程式即時同步事件/狀態(廣播或定向,≤2KB、限流 25/s;關窗即斷,不落庫、不跨使用者/會話)。tt.tray:把內容拿起放入宿主托盤中轉,再注入別的小程式或對話。tt.drag.start/tt.drag.bind:發起拖拽 / 把元素綁成可直接拖到別的小程式的拖源(同源小程式間走原生 HTML5 拖放)。tt.drop.accept:整個小程式都能接收拖入 / 托盤注入。tt.drop.zone:★讓內部某個具體元素感知拖入(懸停高亮反饋)並在其上產生效果。一個小程式可有多個 zone,各自獨立感知。
安全:file 包裹的 url 僅接受本站 /uploads/ 控制代碼(收方用 tt.readFile 取位元組),偽造的跨源外鏈會被丟棄;text/json/name 均有大小上限。
// —— ① 事件/狀態同步 tt.link(與"同時開啟的其它小程式"即時廣播;關窗即斷,不落庫、不跨使用者/會話)——
tt.link.send({ type: 'color', color: '#ef4444' }); // 廣播給所有聯動的小程式(≤2KB,限流 25/s)
tt.link.sendTo(appId, { type: 'ping' }); // 定向發給某個 peer
tt.link.on((from, msg) => { /* from={appId,slug,name} */ apply(msg); });
const peers = await tt.link.peers(); // 當前聯動的其它小程式 [{appId,slug,name}]
// —— ② 托盤中轉 tt.tray(拿起 → 放到別的小程式 / 對話)——
await tt.tray.put({ kind:'json', name:'顏色', data:{ color:'#ef4444' } }); // 放入宿主托盤
const items = await tt.tray.list(); // 檢視托盤 [parcel]
const p = await tt.tray.take(id); // 取走某個包裹
// 包裹 parcel = { kind:'file'|'text'|'json', url?/text?/data?, name?, mime? }
// —— ③ 直接互拖【拖源】tt.drag ——
tt.drag.start({ kind:'json', name:'顏色', data:{ color:'#ef4444' } }); // 主動發起一次拖拽,返回 {id}
tt.drag.bind(swatchEl, () => ({ kind:'json', name:'顏色', data:{ type:'color', color:'#ef4444' } }));
// getParcel() 返回本次拖拽的包裹;返回 null 則不發起。同源小程式間用原生 HTML5 拖放,瀏覽器自帶跟手 ghost。
// —— ④ 接收【整窗】tt.drop.accept ——
tt.drop.accept(['json','file'], (parcel) => { apply(parcel); }); // 拖到本窗任意處 / 托盤注入都回調
// —— ⑤ ★接收【元素級】tt.drop.zone ——
tt.drop.zone(slotEl, ['json','file'], {
onEnter: () => slotEl.classList.add('hot'), // 拖入這個元素 → 只有它高亮(內部元素各自感知)
onOver: () => {}, // 懸停中(可做持續反饋)
onLeave: () => slotEl.classList.remove('hot'), // 移出 → 取消高亮
onDrop: (parcel) => fill(slotEl, parcel), // 落在這個元素上 → 僅它接收併產生效果
});
介面 · 視窗 · 宿主
介面 · 視窗 · 裝置(無需授權)
仿微信 wx.* UI 能力,由宿主渲染真實的提示/對話方塊/圖片檢視器、控制小程式膠囊視窗、訪問剪貼簿/震動/撥號/位置/網路——無需申請授權即可呼叫,讓小程式像原生 App 一樣強大。
// —— 互動反饋 ——
window.tt.showToast({ title: '已儲存', icon: 'success' }); // icon: success|error|loading|none
window.tt.hideToast();
window.tt.showLoading({ title: '處理中…' }); // 配對 window.tt.hideLoading()
window.tt.hideLoading();
const { confirm } = await window.tt.showModal({ title: '確認', content: '要刪除嗎?' });
const { tapIndex } = await window.tt.showActionSheet({ itemList: ['拍照', '相簿'] }); // 取消則 reject
// —— 視窗 / 系統資訊 ——
window.tt.setNavigationBarTitle({ title: '我的頁面' }); // 改小程式膠囊標題
const info = await window.tt.getSystemInfo(); // {theme, platform, windowWidth, windowHeight, safeAreaInsets, appName, version}
// —— 裝置 ——
await window.tt.setClipboardData({ data: '複製的文本' });
const { data } = await window.tt.getClipboardData();
window.tt.vibrateShort(); window.tt.vibrateLong(); // 觸感反饋
window.tt.makePhoneCall({ phoneNumber: '10086' });
// —— 媒體 ——
window.tt.previewImage({ urls: [url1, url2], current: url1 }); // 全屏圖片預覽
// —— 位置 / 外鏈 / 網路 ——
const loc = await window.tt.getLocation(); // 瀏覽器彈許可權 → {latitude, longitude, accuracy, speed}
window.tt.openLocation({ latitude: loc.latitude, longitude: loc.longitude, name: '門店' }); // 地圖檢視
window.tt.openLink({ url: 'https://example.com' }); // 新標籤開啟(僅 http/https)
const net = await window.tt.getNetworkType(); // {isConnected, networkType: wifi|4g|...}
懸浮窗 · 全屏 · 品牌化
控制小程式視窗:內聯/浮窗開啟懸浮全窗、展開全屏 / 還原、給宿主膠囊上色,並監聽顯示/尺寸變化。
// —— 懸浮窗(從內聯/選區處開一個可拖動懸浮窗)——
window.tt.openFloating({ path:'/detail', anchor: rect, x:100, y:120, width:320, height:220 });
window.tt.floating.setRect({ x, y, width, height }); // 也可 moveTo(x,y) / resize(w,h) / close()
window.tt.openFullPage('/detail'); // 從內聯卡開啟完整頁(浮窗/全屏)
// —— 全屏 / 還原 / 關閉(桌面;移動端本就全屏)——
window.tt.expand(); window.tt.collapse(); window.tt.close();
const dm = await window.tt.getDisplayMode(); // {maximized, mobile} —— 是否全屏 / 是否移動端
window.tt.onEvent('displayChanged', (p) => updateFullscreenChip(p.maximized)); // 與宿主膠囊「全屏/還原」雙向聯動
window.tt.onEvent('viewportChanged', (p) => relayout(p.width, p.height)); // iframe 尺寸變化 → 響應式重排
// —— 品牌化:給宿主膠囊頭部/背景上色 ——
window.tt.setHeaderColor('#4f46e5');
window.tt.setBackgroundColor('#fdf6e3');
宿主按鈕 · 觸感 · Telegram 式(無需授權)
小程式控制宿主 chrome 的按鈕並接收其點選事件(雙向)——底部主按鈕 mainButton、頭部返回鍵 backButton、觸感 hapticFeedback。小程式由此深度整合宿主 UI(而非孤立頁面)。
// 主按鈕 MainButton(宿主底部大按鈕,小程式控制 + 接收點選)—— 仿 Telegram
window.tt.mainButton.setText('提交訂單').show(); // 鏈式;setText/setParams/show/hide/enable/disable/showProgress/hideProgress
window.tt.mainButton.onClick(() => { // 點選回撥(宿主 → 小程式 事件);offClick 解綁
window.tt.mainButton.showProgress();
submit().finally(() => window.tt.mainButton.hideProgress());
});
// 返回按鈕 BackButton(宿主頭部返回鍵)
window.tt.backButton.show(); // show/hide/onClick/offClick
window.tt.backButton.onClick(() => history.back());
// 通用事件監聽(等價上面的 onClick)
window.tt.onEvent('mainButtonClicked', handler);
window.tt.onEvent('backButtonClicked', handler);
window.tt.offEvent('mainButtonClicked', handler); // 解綁
// 觸感反饋 HapticFeedback
window.tt.hapticFeedback.impactOccurred('light'); // light|medium|heavy|rigid|soft
window.tt.hapticFeedback.notificationOccurred('success'); // error|success|warning
window.tt.hapticFeedback.selectionChanged();
主題 · 雲端儲存 · Telegram 式
colorScheme / themeParams 讓小程式與宿主配色一致並隨明暗切換;locale 隨宿主 i18n 同步。
// —— 主題(與宿主配色一致,隨明暗切換)——
window.tt.colorScheme; // 'light' | 'dark'
window.tt.themeParams; // {bgColor,textColor,hintColor,linkColor,buttonColor,buttonTextColor,secondaryBgColor}
document.body.style.background = window.tt.themeParams.bgColor; // 用宿主色,與宿主一致
window.tt.onEvent('themeChanged', () => { // 宿主切換明暗時觸發(等價 onThemeChange)
applyTheme(window.tt.colorScheme, window.tt.themeParams);
});
// —— 語言 i18n(隨宿主同步;SDK 已自動設 <html lang>)——
window.tt.locale; // 如 'zh-CN' / 'en-US';等價 tt.context().locale
window.tt.onLocaleChange((locale) => renderInLang(locale)); // 或 onEvent('localeChanged')
參考
錯誤處理
所有 window.tt.* 都返回 Promise,失敗時 reject 一個 Error。生產小程式務必對每個呼叫做 .catch / try-catch,常見 err.message:
| err.message | 含義 / 處理建議 |
|---|---|
使用者拒絕授權 |
按需授權彈窗被拒 → 引導重試 |
使用者取消 |
會話選擇器/預覽被取消 → 靜默 |
呼叫超時 |
宿主長時間無響應(罕見)→ 提示重試 |
資料過大 / 文件過多 / 儲存項過多 |
配額超限 → 精簡資料 |
public 讀取僅限 pub_ 字首的公開集合 |
集合命名不符 → 改用 pub_ 字首 |
禁止訪問(403) |
非開發者用 scope=all → 無許可權 |
網路異常 / Failed to fetch |
網路故障 → 友好提示 + 重試 |
// 所有 tt.* 都返回 Promise,失敗會 reject 一個 Error;用 .catch / try-catch 處理。
window.tt.getProfile()
.then((me) => { /* … */ })
.catch((err) => {
switch (err.message) {
case '使用者拒絕授權': /* 引導使用者重試授權 */ break;
case '使用者取消': /* 使用者取消了會話選擇,靜默即可 */ break;
case '呼叫超時': /* 宿主長時間無響應(罕見),提示重試 */ break;
default: /* 配額 / 許可權(403)/ 網路等,給友好提示 */
}
});
授權模型
按需授權:開啟小程式無需上來授全部許可權;某能力首次呼叫時宿主才彈單項徵詢(允許/拒絕)。使用者可「信任該小程式」一次授予全部,或在設定頁逐項開關、檢視使用記錄。後端每次仍二次校驗(縱深防禦)。介面/視窗/裝置/主題/多程式聯動等無需授權即可用。
| 能力(scope) | 說明 | 敏感 |
|---|---|---|
user.profile |
獲取你的暱稱和頭像 | — |
cloud.data |
雲資料(集合;訂單/記錄等) | — |
storage.kv |
資料儲存(KV) | — |
media.upload |
上傳圖片/檔案(含雲盤 tt.drive) | — |
im.share |
分享卡片到聊天 | — |
im.send |
傳送訊息 | — |
im.read |
讀取會話歷史 | 敏感 |
im.room |
多人房間:代你在房間收發訊息並讀取房間聊天 | 敏感 |
text.lookup |
劃詞查詞翻譯(所選文字將傳送到翻譯服務) | 敏感 |
text.provider |
劃詞提供方(本小程式頁作為劃詞/翻譯 popover) | 敏感 |
可見性
- 公開(public):出現在「發現」與搜尋,任何人可找到。
- 私有(unlisted):不進發現/搜尋,只能通過分享卡片或複製連結(深鏈)進入——用於私域流量傳播、不公開曝光。可在控制台一鍵切換。
- 開啟方式(桌面):控制台「開啟方式」可切「浮窗(預設)/ 全屏」——畫板/白板/編輯器類設
fullscreen開屏即鋪滿,輕量卡片/表單用浮窗。亦可在manifest.json的display宣告初值。移動端始終全屏,不受此設定影響。
版本與釋出
版本模型:草稿 → 稽核中 → 待上架 → 線上。
保留全部歷史版本,支援一鍵回滾到任一歷史版本(瞬時切換線上)。被駁回會在通知中心看到駁回原因。
約束與配額
- 雲資料:單文件 ≤ 8KB,每(小程式,使用者)≤ 500 文件,
list()單次 ≤ 200(按最新);更多用listPage()游標翻頁(mine/all)。注意:直接用list()做前端聚合(計數/均分)在集合 >200 時會漏算最舊部分、結果偏低——需全量時改用 listPage 翻頁或接受近似。 - KV:單值 ≤ 8KB,每(小程式,使用者)≤ 64 鍵。
- 上傳圖片 ≤ 4MB;上傳檔案 ≤ 20MB;傳送訊息限流(每使用者每分鐘 ≤ 20 條);房間信令 payload ≤ 2KB;tt.link 單條 ≤ 2KB、節流 25/s。
- 程式碼包為單檔案 HTML(建議純 DOM 渲染、避免
innerHTML以防 XSS)。 - 令牌短命(約 2 小時),過期後宿主靜默續簽;能力呼叫後端二次校驗。
參考示例:倉庫
applets/food(點餐,雲資料)、applets/repair(報修)均為純前端 + 雲資料小程式。