TutuHai

圖圖嗨小程式開放文件

SDK 接入 · 能力 API · 雲資料 · 授權 · 釋出

圖圖嗨小程式是執行在圖圖嗨內的輕量應用。開發者只上傳前端程式碼包,由平臺託管;業務能力通過 window.tt SDK 與圖圖嗨通訊,無需自建後端(業務資料走圖圖嗨雲資料)。SDK 接入 · 能力 API · 雲資料 · 授權 · 釋出,一頁看全,示例可複製即用。

去控制台建小程式 7 大類 · 26 個主題 · 點選標題展開
目錄 · 26 個主題

入門

資料能力

會話與多人

檔案與雲盤

多程式聯動

介面 · 視窗 · 宿主

參考

入門

簡介

圖圖嗨小程式是執行在圖圖嗨內的輕量應用。開發者只上傳前端程式碼包,由平臺託管;業務能力通過 window.tt SDK 與圖圖嗨通訊,無需自建後端(業務資料走圖圖嗨雲資料)。

隔離與安全:小程式跑在獨立源的沙箱 iframe,宿主登入態絕不進入小程式;每次呼叫由宿主簽發短命受限令牌,後端按能力(scope)二次校驗。

快速開始
  1. 小程式控制臺(/applets)「新建」一個小程式(名稱 + 唯一 slug)。
  2. 寫一個單檔案 HTML(引入 SDK,用 window.tt.* 調能力)。
  3. 新建版本 → 填申請能力 → 上傳程式碼包(單檔案 HTML)。
  4. 提交稽核 → 管理員通過 → 一鍵上架為線上版本。
  5. 使用者在「發現」搜尋/開啟,或你分享卡片 / 複製連結直達。
打包規範

小程式支援兩種上傳形態:①單檔案 HTML 程式碼包(自包含,入口 = 根,最簡);②真實框架構建產物 zip(npm run builddist/,含 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…

三步適配(任意框架通用):

  1. 設相對 base(推薦,最穩妥) —— 讓產物用相對路徑引資源(./assets/x.js 而非根絕對 /assets/x.js),託管在 /<slug>/ 下萬無一失。(預設 base 也能跑:平臺會自動重寫 HTML/CSS 的靜態根絕對引用,並用 Referer 兜底執行時生成的根絕對資源——如程式碼分包 CSS 的 preload;但嚴格 Referrer-Policy / 離線預取等極端場景 Referer 可能缺失致兜底失效,故相對 base 最保險。)
  2. 放 manifest —— 把 manifest.json 放到靜態目錄(如 Vite/SvelteKit 的 static/、多數框架的 public/),build 後自動落到 dist 根;或省略 manifest,在 index.html<meta name="tt:slug" content="…">(及 tt:name / tt:version / tt:scopes)兜底。
  3. 引 SDK —— 兩種方式任選:①npm 安裝(推薦,真實腳手架首選) npm i @tutuhai/applet-sdkimport { 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:宣告你的小程式能處理對話裡的哪類檔案——使用者在聊天對某條檔案點「開啟方式」,聲明瞭匹配型別的小程式就會出現在候選裡,點選即把該檔案直接送進你的小程式(見「處理對話檔案」)。每項:kindsimage / video / audio / pdf / office / text / any(可多選,any=任意檔案);roleeditor(在編輯器中開啟)或 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,不修路由。各框架:SvelteKit kit.paths.base='/<slug>';React Router <BrowserRouter basename="/<slug>">;Vue Router createWebHistory('/<slug>/');Angular APP_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.htmldemo-svelte.htmldemo-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.jsondisplay 宣告初值。移動端始終全屏,不受此設定影響。
版本與釋出

版本模型:草稿 → 稽核中 → 待上架 → 線上

保留全部歷史版本,支援一鍵回滾到任一歷史版本(瞬時切換線上)。被駁回會在通知中心看到駁回原因。

約束與配額
  • 雲資料:單文件 ≤ 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(報修)均為純前端 + 雲資料小程式。