همسخن با یک تگ <script> روی هر سایتی مینشیند: وردپرس، Laravel، Nuxt، Next.js، یا HTML ساده. این صفحه هر چیزی را که برنامهنویس سایت لازم دارد توضیح میدهد: نصب، API جاوااسکریپت، شناسایی کاربر، اتصال اطلاعات مشتری، و عیبیابی.
یک تیم با اعضا، صندوق گفتگو، پاسخهای آماده و سؤالات متداول مشترک. دادهی هر فضای کاری کاملاً از بقیه جداست.
سایت (site)
هر دامنهای که ویجت رویش نصب میشود. هر فضای کاری میتواند چند سایت داشته باشد و هر سایت ظاهر و قوانین خودش را دارد.
کلید عمومی pk_…
شناسهی سایت در کد نصب. دیده شدنش در صفحه اشکالی ندارد.
کلید محرمانه sk_…
فقط روی سرور شما. با آن هویت کاربر را امضا میکنید و درخواستهای همسخن را بررسی میکنید. فقط یک بار نمایش داده میشود (هنگام ساخت سایت یا ساخت کلید جدید).
بازدیدکننده و مخاطب
هر مرورگر یک بازدیدکننده است. مخاطب، آدمی است که گفتگو میکند. با شناسایی، بازدیدکنندههای چند دستگاه یک مخاطب میشوند.
گفتگو (conversation)
رشتهی پیامهای یک مخاطب. یا باز است یا بسته؛ پیام بعد از بسته شدن، گفتگوی تازه باز میکند.
نصب
۱. کد نصب
این کد را در <head> همهی صفحهها بگذارید. کلید pk_… را از پنل بردارید: سایتها ← سایت شما ← نصب و امنیت. همانجا کد آماده با کلید خودتان هم هست.
بقیهی کارهای چت (WebSocket، فایلها، فونت) داخل قاب خود همسخن انجام میشود و به CSP سایت شما ربطی ندارد. اگر میکروفون بسته باشد، دکمهی ضبط بهجای خطا دادن پنهان میشود.
۴. در فریمورکها
در اپهای تکصفحهای (SPA) کد را فقط یک بار اضافه کنید. تغییر مسیر با pushState، replaceState، popstate و hashchange خودکار تشخیص داده میشود و قوانین نمایش دوباره بررسی میشوند.
plugins/livechat.client.ts
export default defineNuxtPlugin(() => {
const { origin, siteKey } = useRuntimeConfig().public.livechat;
if (!origin || !siteKey) return;
const w = window as any;
w.Livechat = w.Livechat || { q: [] };
['open', 'close', 'toggle', 'setUser', 'setContext', 'logout', 'on', 'off', 'showMessage', 'trackPage'].forEach((m) => {
w.Livechat[m] = w.Livechat[m] || ((...args: unknown[]) => w.Livechat.q.push([m, ...args]));
});
// After the page has loaded, so the chat never competes with the page itself.
const add = () => {
const script = document.createElement('script');
script.async = true;
script.src = `${origin}/loader.js`;
script.dataset.siteKey = siteKey;
document.head.append(script);
};
if (document.readyState === 'complete') add();
else window.addEventListener('load', add, { once: true });
});
شناسایی کاربر واردشده. بدون hash معتبر، کاربر ناشناس میماند. جزئیات
logout()
فراموش کردن کاربر و شروع یک نشست ناشناس تازه. بعد از خروج کاربر از سایت صدا بزنید.
setContext({ … })
اطلاعاتی برای قوانین نمایش، مثل { cart_count: 2 }. جزئیات
showMessage(text)
یک پیام محلی در ویجت نشان میدهد؛ ذخیره نمیشود و به اپراتور نمیرسد.
trackPage()
بررسی دوبارهی قوانین بعد از تغییر مسیر. معمولاً لازم نیست.
on(event, callback) · off(event, callback)
گوش دادن به رویدادها.
destroy()
ویجت را کامل از صفحه برمیدارد.
مثال
// A "need help?" link that opens the chat with a hint for the visitor.
document.querySelector('#help').addEventListener('click', (event) => {
event.preventDefault();
Livechat.open();
Livechat.showMessage('سفارش شما ثبت شده؛ برای پیگیری کد سفارش را بنویسید.');
});
// Display rules can use anything the page knows.
Livechat.setContext({ cart_count: cart.items.length, plan: 'gold' });
رویدادها
رویداد
داده
کی
ready
–
ویجت آماده است.
open · close
–
چت باز یا بسته شد.
unread
تعداد
تعداد پیامهای خواندهنشده عوض شد.
message:sent
{ body }
بازدیدکننده پیامی فرستاد.
message:received
{ body, sender }
پیامی از اپراتور یا دستیار هوشمند رسید؛ sender نام فرستنده است (برای ربات، نامی که در تنظیمات سایت گذاشتهاید).
form:submitted
{ kind }
فرم شروع یا آفلاین ارسال شد.
rating
{ score }
بازدیدکننده از ۱ تا ۵ امتیاز داد.
flow:option
{ flow, option, label }
گزینهای در گفتگوی آماده انتخاب شد.
نام دلخواه
{ flow, option }
گزینهای با کار «رویداد» در گفتگوی آماده انتخاب شد.
مثال
// Count conversations in your analytics.
Livechat.on('message:sent', () => gtag('event', 'chat_message'));
// A flow option with action "event" named "show-size-guide" opens your own size guide.
Livechat.on('show-size-guide', () => openSizeGuideModal());
// Badge on your own icon.
Livechat.on('unread', (count) => { badge.textContent = count || ''; });
دکمه و شمارندهی خود سایت، بدون جاوااسکریپت
loader دو ویژگی HTML را هر جای صفحه پیدا میکند، حتی روی المانهایی که بعداً اضافه شوند:
data-hamsokhan-open: کلیک روی المان چت را باز میکند (و اگر باز است، میبندد).
data-hamsokhan-counter: متن المان تعداد پیامهای خواندهنشده میشود (با ارقام زبان ویجت)، یا خالی وقتی پیامی نیست.
HTML
<button type="button" data-hamsokhan-open>
گفتگو با ما <span class="badge" data-hamsokhan-counter></span>
</button>
<style>
.badge:empty { display: none; }
</style>
برای اینکه فقط دکمهی خودتان دیده شود، در پنل «نمایش دکمهی چت» را خاموش کنید (ظاهر ویجت). پنجرهی چت همچنان از گوشهی صفحه باز میشود.
شناسایی کاربر (Identity Verification)
اگر سایتتان حساب کاربری دارد، این بخش را حتماً انجام دهید.
چرا امضا لازم است؟
اگر همسخن هر idی را از مرورگر قبول میکرد، هر کسی میتوانست در Console بنویسد setUser({ id: '42' }) و گفتگوهای مشتری شمارهی ۴۲ را بخواند. برای همین شناسه باید با کلید محرمانهای امضا شود که فقط روی سرور شماست.
مرورگر کاربرعلی وارد حسابش شده
GET /livechat/identity
سرور شماکلید sk_ فقط اینجاست
{ id: "42", hash }
مرورگرLivechat.setUser(…)
id + hash
همسخنامضا درست است ✓
۱. صفحه از سرور شما هویت کاربر واردشده را میخواهد.۲. سرور شناسه را با کلید محرمانه امضا میکند. کلید هرگز از سرور بیرون نمیرود.۳. شناسه و امضا به صفحه برمیگردند.۴. صفحه آنها را با setUser به همسخن میدهد.۵. همسخن همان امضا را حساب میکند. فقط اگر یکی باشند، کاربر «هویت تأییدشده» میشود.
// After login, or on page load when the user is already logged in.
const response = await fetch('/api/livechat/identity', { credentials: 'include' });
if (response.ok) Livechat.setUser(await response.json());
// Optional: facts for the team, shown beside the conversation. null removes one.
Livechat.setUser({ ...identity, traits: { plan: 'gold', orders: 12 } });
// On logout, so the next person on this browser does not see the conversation.
Livechat.logout();
قاعدهها
id شناسهی ثابت کاربر در سایت شماست و باید رشته باشد. ایمیل یا موبایل نه، چون عوض میشوند.
بدون hash درست، کاربر ناشناس میماند و name و بقیه فقط نمایشیاند.
با شناسایی، گفتگوهای ناشناس همان مرورگر به حساب کاربر منتقل میشوند. ورود با حساب دیگر هرگز تاریخچهی کاربر قبلی را منتقل نمیکند.
امضا را در هر بار لود صفحه بگیرید و جایی کش نکنید. اگر کلید عوض شود، امضاهای قبلی باطلاند.
traits تا ۳۰ مورد؛ نام تا ۴۰ و مقدار تا ۲۵۵ نویسه. هر فراخوانی با قبلی ادغام میشود.
⚠️ کلید محرمانه
sk_… فقط در متغیرهای محیطی سرور. هرگز در کد سمت مرورگر، فایلهای .env فرانتاند (مثل NUXT_PUBLIC_* یا NEXT_PUBLIC_*)، یا git.
قوانین نمایش و setContext
قوانینی که مدیر در پنل میسازد (نمایش ویجت، پیام خودکار، گفتگوی آماده، پیشنهاد مقاله، مسیریابی) روی این «واقعیتها» کار میکنند. loader بیشترشان را خودش جمع میکند؛ شما فقط context.* را با setContext میفرستید.
* هر چیزی (حتی /)، ? یک نویسه؛ کل مقدار باید جور باشد
regex
متن
حداکثر ۲۰۰ نویسه؛ فقط چیزهایی که هم در JavaScript و هم در PCRE کار میکنند
eq · neq · in · not_in
مقدار ساده
برابری دقیق
gt · gte · lt · lte
عدد
هر دو طرف باید عدد باشند
مقایسهی آدرسها
پیش از مقایسه، هم آدرس صفحه و هم مقدار قانون یکدست میشوند: #… و query حذف میشوند (مگر قانون with_query داشته باشد)، مسیر decode میشود (پس /product/کفش و نسخهی percent-encoded آن یکیاند)، اسلش آخر حذف میشود، و www. نادیده گرفته میشود. مقداری که با / شروع شود با مسیر مقایسه میشود و بقیه با «دامنه + مسیر».
اطلاعات مشتری از سایت (Context Provider)
یک endpoint روی سرور شما که همسخن برای مشتریهای شناساییشده صدا میزند. جوابش به شکل کارت کنار گفتگو نمایش داده میشود: سبد خرید، سفارشها، کیف پول، یا هر چیز دیگر. هیچکدام در همسخن ذخیره نمیشود.
اپراتور گفتگوی علی را باز میکند
POST + امضا
سرور شماامضا ✓ · کاربر 42
{ cards: […] }
کنار گفتگو🛒 سبد: ۲ کالا · 📦 #1042 ارسال شد
۱. همسخن درخواستی امضاشده با کلید محرمانهی سایت میفرستد.۲. سرور شما امضا و زمان را بررسی میکند و کاربر را پیدا میکند.۳. جواب: چند کارت به شکل JSON.۴. اپراتور کارتها را کنار گفتگو میبیند. جواب ۶۰ ثانیه کش میشود.
کلید همان sk_… است. درخواست را رد کنید مگر اینکه امضا روی بدنهی خام (همان بایتهایی که رسیده) درست باشد و زمان حداکثر ۵ دقیقه با ساعت سرور فاصله داشته باشد. مقایسه را با تابع زمانثابت انجام دهید.
۱۰ در هر کارت: title (۱۲۰)، اختیاری subtitle (۲۰۰)، meta (۶۰)، url
actions
۳ در هر کارت: label (۴۰)، url
کل جواب
۲۵۶ کیلوبایت
همهچیز متن ساده نمایش داده میشود و HTML اجرا نمیشود. فقط لینکهای http و https نگه داشته میشوند. هر چیز بیرون از این شکل یا حدها، بیصدا کنار گذاشته میشود.
404 یعنی «این کاربر را نداریم» و کارتی نشان داده نمیشود. هر وضعیت دیگر، timeout یا جواب نامعتبر، به اپراتور میگوید اطلاعات سایت در دسترس نیست.
رفتار
timeout: ۲ ثانیه، بدون تلاش دوباره و بدون دنبال کردن redirect. endpoint را سبک نگه دارید؛ فقط چیزی را بخوانید که کارتها نشان میدهند.
کش: جواب ۶۰ ثانیه برای هر مشتری و خطا ۱۵ ثانیه. اپراتور با دکمهی تازهسازی دوباره میپرسد.
شبکه: آدرس باید به یک IP عمومی برسد. اگر پشت CDN یا فایروال هستید، User-Agent Hamsokhan-Context/1 را مسدود نکنید.
تست: در پنل، «نصب و امنیت ← اطلاعات مشتری از سایت»، آدرس را بنویسید و با شناسهی یک کاربر واقعی «تست» بزنید.
صفحهی چت مستقل، CSS و صدا
صفحهی چت مستقل
با روشن کردن «صفحهی چت مستقل» در تب «پیشرفته»، آدرس https://hamsokhan.shop/chat/pk_… چت را تمامصفحه باز میکند. ظاهر، فرمها، گفتگوهای آماده و ساعات کاری همان سایت را دارد. این صفحه در iframe باز نمیشود و تا وقتی خاموش است، 404 میدهد.
CSS دلخواه
CSS فقط داخل پنجرهی چت اعمال میشود. کلاسهای ویجت با hs- شروع میشوند:
.hs-header { background: linear-gradient(135deg, #0f766e, #14b8a6); }
.hs-msg--mine .hs-bubble { border-radius: 18px 18px 4px 18px; }
CSS از دامنهی همسخن سرو میشود و CSP پنجرهی چت عکس، فونت یا فایل از دامنههای دیگر را بار نمیکند. نام کلاسها ممکن است در نسخههای بعد عوض شود؛ تغییرات در بخش تغییرات میآید.
صدای پیام
mp3، ogg، wav یا m4a تا ۳۰۰ کیلوبایت، از تب «پیشرفته». نوع فایل از روی محتوا بررسی میشود.
حدها
چه
حد
پیام متنی
۵۰۰۰ نویسه
فایل
۱۰ مگابایت: JPEG، PNG، WebP، GIF، PDF، متن، DOCX، XLSX، و صدای OGG، MP3، M4A، WebM. تشخیص از روی محتوا.
پیام صوتی
۵ دقیقه
عکس
حداکثر ۲۵ مگاپیکسل؛ ضلع بلند به ۲۵۶۰ پیکسل کوچک و اطلاعات EXIF پاک میشود.
ارسال پیام
۲۰ در دقیقه برای هر بازدیدکننده، ۱۲۰ برای هر IP
آپلود
۱۰ در دقیقه برای هر بازدیدکننده، ۳۰ برای هر IP
شروع نشست و setUser
۳۰ در دقیقه برای هر IP
مدل امنیتی
جداسازی: چت در iframe از دامنهی همسخن اجرا میشود. اسکریپتهای سایت شما به گفتگوها و توکن بازدیدکننده دسترسی ندارند. loader فقط دکمه و پیام خودکار را در صفحه میسازد.
دامنهها:frame-ancestors قاب چت فقط دامنههای مجاز سایت است.
هویت: بدون امضای HMAC هیچ شناسهای پذیرفته نمیشود. مقایسهها زمانثابتاند.
ذخیره در مرورگر: روی سایت شما فقط شمارندهی بازدید و UTM ورود (کلیدهای hamsokhan:*). توکن بازدیدکننده داخل iframe است و مرورگرها آن را برای هر سایت جدا نگه میدارند.
محتوا: پیامها متن سادهاند و لینکها امن ساخته میشوند. متن مقالهها Markdown محدود است و HTML اجرا نمیشود.
بدون کوکی: API ویجت کوکی ندارد، پس CSRF معنی ندارد.
عیبیابی
دکمهی چت نمیآید
Console مرورگر: خطای CSP دربارهی hamsokhan.shop؟ CSP را اصلاح کنید.
Network: loader.js با 200 آمده؟ data-site-key درست است؟
صفحهی چت در Console میگوید «refused to frame»؟ origin صفحه در دامنههای مجاز نیست (www، پورت و https را چک کنید).
قوانین نمایش، حالت آفلاین «مخفی» یا «نمایش در موبایل» ویجت را پنهان نکرده باشند.
کاربر بعد از setUser هنوز ناشناس است
id را دقیقاً همان رشتهای بدهید که امضا کردهاید ("42" و 42 فرق دارند؛ رشته بفرستید).
کلید سرور همان کلید فعلی سایت است؟ بعد از «ساخت کلید جدید» باید سرور را هم بهروز کنید.
در Laravel اگر config:cache دارید، بعد از تغییر .env کش را دوباره بسازید.
تست Context Provider خطا میدهد
خطا
علت محتمل
401
امضا روی بدنهی پردازششده حساب شده نه خام، کلید اشتباه است، یا ساعت سرور بیش از ۵ دقیقه جلو یا عقب است.
timeout
endpoint بیش از ۲ ثانیه طول کشیده، یا CDN و فایروال درخواست را نگه داشتهاند.
private_network
آدرس به IP خصوصی میرسد؛ آدرس عمومی بدهید.
not_json یا کارتی نمیآید
جواب باید {"cards": [...]} با Content-Type: application/json باشد.
دکمهی میکروفون نیست
صفحه باید HTTPS باشد و Permissions-Policy سایت میکروفون را برای https://hamsokhan.shop باز گذاشته باشد. نمونه
بعد از تغییر مسیر در SPA قوانین اعمال نمیشوند
اگر روتر شما از history استاندارد استفاده نمیکند، بعد از هر تغییر مسیر Livechat.trackPage() را صدا بزنید.