Bắt đầu
Tab HTML & CSS vẽ nên widget của bạn. Khung nền trong suốt và nổi trên video — đừng bao giờ tô nền đặc kín toàn bộ.
Trong tab JS, đăng ký bằng TE.on('gift', fn). window.TE đã được tải sẵn — không cần import, không cần thiết lập.
Dùng Simulate hoặc các nút Fire để xem trước offline, rồi Add to overlay — hoặc để Claude làm qua MCP.
var box = document.getElementById('box');
TE.on('gift', function (ev) {
box.textContent = ev.user.name + ' sent ' + ev.gift.name + '!';
box.classList.remove('pop'); void box.offsetWidth; box.classList.add('pop');
});Kết nối Claude (MCP)
Tạo và sửa các widget này ngay từ Claude Desktop hoặc Claude Code. Tạo token, thêm connector, và Claude có thể gọi các công cụ sau trên tài khoản của bạn:
get_docsToàn bộ hướng dẫn viết widget + mọi trigger và hành động (Claude đọc cái này trước).list_widgetsLiệt kê các widget đã lưu kèm id.get_widgetLấy html / css / js của một widget.create_widgetTạo widget HTML mới từ tên + html/css/js.update_widgetSửa một widget có sẵn.delete_widgetXóa widget theo id.list_templatesLiệt kê các template có sẵn để sao chép.get_templateĐọc html/css/js của một template.create_from_templateSao chép template thành widget mới.list_community_templatesDuyệt các widget do cộng đồng đăng.install_community_templateCài một bản fork riêng tư, có thể chỉnh sửa, của template cộng đồng.TE SDK
Mọi thứ widget làm đều đi qua đối tượng toàn cục window.TE.
TE.on(type, fn)Đăng ký một sự kiện LIVE. `type` là bất kỳ trigger nào bên dưới; `fn(ev)` chạy mỗi khi nó xảy ra.
TE.on('*', fn)Bắt mọi sự kiện. `fn(ev, type)` nhận dữ liệu và tên sự kiện.
TE.off(type, fn)Gỡ một handler đã đăng ký trước đó.
TE.onGift(name, fn)Chỉ kích hoạt với quà có tên khớp (chuỗi con, không phân biệt hoa thường). Bỏ `name` để bắt mọi món quà.
TE.onSticker(idOrUrl, fn)Chỉ kích hoạt với một emote / sticker subscriber cụ thể — khớp theo id TikTok, URL ảnh hoặc nguồn của nó.
TE.rules.allowUser / allowGiftBộ lọc điều kiện dùng lại được cho vai trò, danh sách cho phép/chặn, tên quà và giá trị xu tối thiểu.
TE.metricsĐối tượng snapshot stream mới nhất (viewers, likes, coins, followers, topGifters…). Cũng được gửi qua TE.on('metrics', fn).
TE.demoTrue ở nơi widget được trưng bày (thư viện, ô xem trước, công tắc Demo trong builder). False trên overlay LIVE và khi bạn thử trong builder hoặc trình chỉnh overlay, nơi chỉ hiện những gì bạn kích hoạt hoặc những gì đến từ LIVE. Mọi widget đều tự trình diễn ở đó: đặt dữ liệu mẫu trông như sự kiện thật (hình quà thật, tên, ảnh đại diện) trong khối if (TE.demo), đi qua đúng luồng code thật của widget. Widget có khối như vậy sẽ không nhận sự kiện xem trước từ host, nên không có gì kích hoạt hai lần.
TE.defineSettings([...])Khai báo các điều khiển streamer chỉnh được; trả về giá trị hiện tại (mặc định gộp với lựa chọn của streamer).
TE.settingsĐối tượng giá trị cài đặt hiện tại (cùng cấu trúc defineSettings đã trả về).
TE.on('settings', fn)Chạy khi streamer đổi một cài đặt lúc đang LIVE — render lại với giá trị mới.
TE.state.get/set/incrementGiá trị nguyên tử dựa trên Promise cho tổng số và trạng thái game dùng chung.
TE.collection.*Danh sách người tham gia (tham gia một lần) và bảng điểm do server quản lý: join, increment, list, count, remove và clear.
TE.queue.*Hàng đợi FIFO có giới hạn do server quản lý cho media, yêu cầu và hành động do người xem kích hoạt.
TE.shared.*Cho nhiều widget dùng chung một không gian trạng thái kênh có tên; các widget không liên quan vẫn tách biệt.
TE.random.draw(name, opts)Bốc thăm an toàn một hoặc nhiều người thắng từ một collection và ghi lại kết quả.
TE.cooldown.claim(scope, user, ms)Giành cooldown toàn cục hoặc theo người dùng một cách nguyên tử; trả về claimed và retryAfterMs.
TE.timer.*Bắt đầu, tạm dừng, đặt lại và đọc một bộ hẹn giờ theo giờ thực được lưu lại, không mất khi tải lại trang.
TE.points.trySpend(user, amount, reason)Kiểm tra và trừ điểm trong một giao dịch; luôn kiểm tra result.ok trước khi chạy một tương tác trả phí.
Trigger
Các sự kiện LIVE bạn có thể lắng nghe. Mỗi user chứa { id, name, username, avatar, roles }.
TE.on('gift')Người xem đã tặng quà. coins = tổng của combo; streakEnd đánh dấu combo đã kết thúc.
{ user, gift: { name, image|null, coins, repeat, combo, streakEnd } }TE.on('follow')Người xem đã follow tài khoản.
{ user }TE.on('subscribe')Người xem đã đăng ký. months = số tháng liên tiếp.
{ user, months }TE.on('share')Người xem đã chia sẻ buổi LIVE.
{ user }TE.on('chat')Một tin nhắn chat. emotes = URL ảnh emote subscriber trong tin nhắn này.
{ user, comment, emotes:[url,…] }TE.on('like')Người xem đã thả like. count = đợt này; total = tổng cộng dồn.
{ user, count, total }TE.on('join')Người xem đã vào phòng. isTop = một top tặng quà đã vào.
{ user, isTop }TE.on('sticker')Một emote subscriber hoặc sticker trên màn hình được gửi trực tiếp. url = ảnh emote; id = id TikTok của nó.
{ user|null, id|null, url, source }TE.on('metrics')Snapshot stream mới nhất. TE.metrics chứa nó; sự kiện kích hoạt mỗi khi nó cập nhật. hostNick và hostAvatar là tên và ảnh đại diện của chính streamer.
{ live, viewers, likes, followers, coins, hostNick, hostAvatar, topGifters:[…], … }TE.on('milestone')Đã vượt một mốc số tròn (like, xu, follower, sub).
{ metric, value, label }TE.on('poll')Bình chọn LIVE gốc của TikTok — phiếu thật, từ đầu đến cuối.
{ state:'start'|'update'|'end', title, options:[{text,votes}], endsAt }TE.on('battle')Cập nhật battle LinkMic: thẻ battle, vé fan, quy mô đội quân.
{ card|null, tickets|null, armies|null, battleId|null }TE.on('rank')Vị trí trên bảng xếp hạng theo giờ và những lúc thăng hạng.
{ rank|null, from|null, to|null, countdown|null }TE.on('envelope')Một bao lì xì / rương báu được thả trong buổi LIVE.
{ }TE.on('pinned')Host đã ghim một bình luận.
{ text }TE.on('deleted')Một moderator đã xóa tin nhắn chat (id khớp với sự kiện chat trước đó).
{ id }TE.on('streamState')Stream đã lên sóng hoặc tắt.
{ live }TE.on('apiEvent')Một sự kiện tùy chỉnh gửi qua Event API. Lắng nghe apiEvent hoặc trực tiếp tên tùy chỉnh của nó.
{ name, data }TE.on('nowPlaying')Bài Spotify hiện tại của streamer KÈM hàng chờ tiếp theo. Kích hoạt khi kết nối và mỗi khi bài hát, trạng thái phát, tiến độ hoặc hàng chờ thay đổi. playback là null khi không phát gì; queue là các bài sắp tới.
{ connected, playback: { isPlaying, progressMs, durationMs, title, artists:[…], album, artwork|null } | null, queue:[{ title, artist, artwork|null, durationMs }] }TE.onGift('Rose', function (ev) { /* … */ });
TE.onSticker('<sticker id>', function (ev) { /* … */ });Emote của kênh bạn nằm trong widget builder: mở Simulate events, chọn Sticker rồi chọn một cái. Sự kiện thử mang id và URL ảnh thật của nó, nên handler dựa trên cái nào cũng được thử với dữ liệu thật.
Hành động
Những gì widget có thể làm để phản hồi — API trình duyệt thuần, dán vào là chạy.
Bật một hình lên màn hình — hình quà/emote, file đã tải lên, hoặc URL bất kỳ. Tự biến mất sau vài giây.
// Show an image, then fade it out
TE.on('gift', function (ev) {
var img = document.createElement('img');
img.src = ev.gift.image; // ← any image URL works
img.style.cssText = 'position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);max-width:60%';
document.body.appendChild(img);
setTimeout(function () { img.remove(); }, 4000); // ← how long it stays
});Phát âm thanh khi có sự kiện. Thêm cài đặt âm thanh để streamer tự chọn file — không phải sửa code.
// Play a sound the streamer chose in settings
var s = TE.defineSettings([{ key: 'sfx', label: 'Alert sound', type: 'sound', default: '' }]);
TE.on('settings', function (ns) { s = ns; });
TE.on('gift', function (ev) {
if (s.sfx) { var a = new Audio(s.sfx); a.volume = 0.8; a.play(); }
});Viết chữ động và kích hoạt lại animation CSS bằng cách bật/tắt class.
// Announce the event with a CSS pop animation
var box = document.getElementById('box'); // your element in the HTML tab
TE.on('follow', function (ev) {
box.textContent = ev.user.name + ' followed!';
box.classList.remove('pop'); void box.offsetWidth; box.classList.add('pop');
});Gắn một con số trên màn hình với số liệu LIVE — người xem, like, xu, follower — tự cập nhật.
// Keep a number in sync with the live stream
TE.on('metrics', function (m) {
document.getElementById('count').textContent = m.viewers.toLocaleString();
});Đọc sự kiện bằng chức năng chuyển văn bản thành giọng nói của trình duyệt. Rất hợp để shout-out khi có quà hoặc follow.
// Text-to-speech shout-out
TE.on('gift', function (ev) {
var u = new SpeechSynthesisUtterance(ev.user.name + ' sent ' + ev.gift.name);
speechSynthesis.speak(u);
});Mọi widget đều tự trình diễn trong thư viện và builder: dữ liệu mẫu trông như sự kiện thật, với hình quà, tên và ảnh đại diện thật. Khi LIVE, TE.demo là false và chỉ sự kiện thật xuất hiện.
// Preview samples through the real code path. Never on a live overlay.
if (TE.demo) {
var GIFTS = [
{ name: 'Rose', coins: 1, image: 'https://p16-webcast.tiktokcdn.com/img/maliva/webcast-va/eba3a9bb85c33e017f3648eaf88d7189~tplv-obj.png' },
{ name: 'Perfume', coins: 20, image: 'https://p16-webcast.tiktokcdn.com/img/maliva/webcast-va/20b8f61246c7b6032777bb81bf4ee055~tplv-obj.png' }
];
var NAMES = ['lunaa', 'nightowl_gaming_official'], n = 0;
function sample() {
n++;
onGift({ user: { name: NAMES[n % 2], avatar: window.__TE_AVATAR }, gift: GIFTS[n % 2] });
}
sample();
setInterval(sample, 6000);
}Lưu trữ đồng bộ đơn giản cho trạng thái hiển thị. Dùng runtime giao dịch cho người tham gia, giao dịch mua và điểm dùng chung.
// Persistent state: survives OBS reloads
var total = TE.store.get('total', 0); // read (with default)
TE.on('gift', function (ev) {
total += ev.gift.coins;
TE.store.set('total', total); // write (auto-saved)
render();
});Lệnh chat khớp chính xác cùng collection nguyên tử giúp đăng ký tham gia, bình chọn và quay thưởng an toàn.
// One server-authoritative entry per viewer.
// A function keyword reads s.command on every message, so the
// streamer can change the command in settings without a reload.
TE.onCommand(function () { return s.command || '!join'; }, function (c) {
TE.collection.join('entrants', c.user.id, c.user).then(function (result) {
if (result.joined) renderCount(result.count);
});
});Yêu cầu thông báo overlay, âm thanh, TTS, bộ đếm, thời gian subathon hoặc chuyển cảnh OBS trực tiếp từ widget.
// One call, one stream action
TE.act({ id: 'tts', message: 'New high score!' });
TE.act({ id: 'points', user: ev.user, amount: 50 });Kiểm tra, tiêu hoặc thưởng điểm thân thiết theo giao dịch và nhận số dư sau đó.
// Only spin after the points were really debited
TE.onCommand(function () { return s.command || '!spin'; }, function (ev) {
TE.points.trySpend(ev.user, 50, 'Wheel spin').then(function (result) {
if (result.ok) spin();
else showMissing(result.missing);
});
});Trạng thái nguyên tử, collection, bốc thăm ngẫu nhiên an toàn, cooldown và bộ hẹn giờ bền vững — an toàn kể cả khi browser source bị nhân đôi.
// Fair draw from a server-owned entrant collection
TE.random.draw('entrants', { count: 1 }).then(function (result) {
if (result.ok) reveal(result.winners[0].value);
});Cho phép chỉnh chữ, màu, số, công tắc — streamer chỉnh trực tiếp trong trình chỉnh overlay, không cần code.
// No-code controls for whoever uses the widget
var s = TE.defineSettings([
{ key: 'title', label: 'Title', type: 'text', default: 'Goal' },
{ key: 'accent', label: 'Color', type: 'color', default: '#FF2E4D' },
]);
document.body.style.setProperty('--accent', s.accent);
TE.on('settings', function (ns) { s = ns; /* re-render with new values */ });Cài đặt streamer
Khai báo điều khiển bằng TE.defineSettings([...]), ai dùng widget cũng chỉnh được trực tiếp trong trình chỉnh overlay — không cần code. Mỗi trường:
Widget đã đặt có một hoặc nhiều trường 'button' sẽ hiện các hành động đó trong phần cài đặt của nó bên trong Overlay Editor. Widget Builder hiển thị cùng các nút đó để thử xem trước an toàn khi đang code.
'text'Ô nhập chữ một dòngstring'number'Ô nhập sốnumber'color'Bộ chọn màuchuỗi hex, ví dụ "#FF2E4D"'select'Danh sách thả xuống (cần options: [...])một trong các options'toggle'Công tắc bật/tắtboolean'range'Thanh trượt (min / max / step)number'button'Nút hành động dạng khai báoincrement, set hoặc toggle — JSON đã kiểm tra, không bao giờ là code dashboard'sound'Bộ chọn trong các âm thanh streamer đã tải lên + Tải lênURL file đã chọn ("" = không có)'image'Bộ chọn trong các hình streamer đã tải lên + Tải lênURL file đã chọn ("" = không có)var s = TE.defineSettings([
{ key: 'title', label: 'Title', type: 'text', default: 'Follower goal' },
{ key: 'target', label: 'Goal', type: 'number', default: 100 },
{ key: 'accent', label: 'Color', type: 'color', default: '#FF2E4D' },
{ key: 'reset', label: 'Reset', type: 'button', default: 0,
action: { type: 'increment', step: 1 }, confirm: true, tone: 'danger' },
]);
// re-render when the streamer changes something live
var lastReset = s.reset;
TE.on('settings', function (ns) {
s = ns;
if (ns.reset !== lastReset) { lastReset = ns.reset; resetCounter(); }
render();
});Định nghĩa điều khiển là dữ liệu đã được kiểm tra. action của nút chỉ chấp nhận increment, set hoặc toggle. HTML, code callback và URL tùy ý sẽ bị trình render của dashboard từ chối hoặc bỏ qua.
Sandbox & giới hạn
Widget chạy trong iframe cô lập với chính sách bảo mật nội dung nghiêm ngặt, nên widget lỗi hay độc hại không bao giờ đụng được đến tài khoản của bạn hay phần còn lại của trang.
- Hình ảnh & GIF từ xa (hình quà TikTok, ảnh đại diện, URL bất kỳ)
- Google Fonts + @font-face của riêng bạn
- Âm thanh qua new Audio(url)
- Yêu cầu đến API của chính TokElements (ví dụ /api/files/…)
- Animation CSS, SVG, canvas, Web Audio
- Thẻ <script> bên ngoài / CDN
- fetch / XHR / WebSocket đến host khác
- Cookie, localStorage, truy cập trang cha
- Tải package npm
Ví dụ
Widget hoàn chỉnh, sao chép là dùng. Mỗi bản xem trước đều chạy thật — sự kiện demo được kích hoạt để bạn xem nó phản ứng.
var img = document.getElementById('emote');
// fires for every emote — use TE.onSticker('<id>', …) for one specific one
TE.on('sticker', function (ev) {
img.src = ev.url;
img.classList.remove('show'); void img.offsetWidth; img.classList.add('show');
});var target = 200; // ← your goal
TE.on('metrics', function (m) {
var pct = Math.min(100, m.follows / target * 100);
document.getElementById('fill').style.width = pct + '%';
document.getElementById('txt').textContent = m.follows + ' / ' + target;
});