fix(widget): make connection errors actionable

This commit is contained in:
WangShen007 committed 2026-10-28 20:01:18 +08:00
1 parent 3017753b88
commit bb541bd212
4 files changed
+251 -40

No files matched your search

+34 -11
View File
@@ -1,5 +1,5 @@
(()=>{var we=(ie,ne,t)=>new Promise((E,p)=>{var D=y=>{try{z(t.next(y))}catch(B){p(B)}},G=y=>{try{z(t.throw(y))}catch(B){p(B)}},z=y=>y.done?E(y.value):Promise.resolve(y.value).then(D,G);z((t=t.apply(ie,ne)).next())});var _e=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 48" fill="none"><path d="M29 20 39 6 42 21Z" fill="#568B73"/><path d="M4 16 32 26 23 43H15Z" fill="#2E6A5C"/><path d="M35 26 60 16 49 43H27Z" fill="#8BB49C"/></svg> (()=>{var ve=(re,oe,t)=>new Promise(($,p)=>{var G=y=>{try{P(t.next(y))}catch(R){p(R)}},q=y=>{try{P(t.throw(y))}catch(R){p(R)}},P=y=>y.done?$(y.value):Promise.resolve(y.value).then(G,q);P((t=t.apply(re,oe)).next())});var ze=`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 48" fill="none"><path d="M29 20 39 6 42 21Z" fill="#568B73"/><path d="M4 16 32 26 23 43H15Z" fill="#2E6A5C"/><path d="M35 26 60 16 49 43H27Z" fill="#8BB49C"/></svg>
`;window.replypierId;window.replypierBaseUrl;window.ReplyPier;window.replypierConfig;(function(){function ie(e){return/^#[0-9A-F]{6}$/i.test(e)}function ne(){return typeof window!="undefined"&&window.replypierBaseUrl?window.replypierBaseUrl:typeof window!="undefined"&&window.APP_CONFIG&&window.APP_CONFIG.API_URL?window.APP_CONFIG.API_URL:"https://api.replypier.site/api/v1"}let t={baseUrl:ne(),containerId:"replypier-container",backdropId:"replypier-backdrop",buttonId:"replypier-button",chatBubbleColor:"#f34611",loadingContainerId:"replypier-loading",tokenKey:"rpid",tokenProvider:null,containerBottom:100,containerRight:20,containerWidth:400,containerHeight:560,launcherBottom:20,launcherRight:20,side:"right",displayMode:"floating",launcher:!0,sidebarWidth:420,searchBarWidth:320,searchPlaceholder:"\u6709\u95EE\u9898\u968F\u65F6\u95EE\u6211\u2026",zIndex:999999,trigger:null,hotkey:!0,surfaceIsPalette:null,chatStyle:null,chatInitiationMessages:[],initiationMessageId:"replypier-initiation",initiationShownKey:"rpim_shown",unreadCount:0,hasOpened:!1},E={};function p(e,i){t[e]=i,E[e]=!0}let D=["floating","sidebar-left","sidebar-right","search-bar"],G=["ASK_ANYTHING","AURORA"],z=680,y=620,B=140,A={width:!1,height:!1},P=0;function j(){return t.surfaceIsPalette===!1?!1:(G.indexOf(t.chatStyle)!==-1||t.displayMode==="search-bar")&&t.displayMode!=="sidebar-left"&&t.displayMode!=="sidebar-right"}function q(){let e=document.getElementById(t.containerId);if(!e)return;if(!j()||!P){e.style.height="";return}let i=Math.min(oe().height,Math.round(window.innerHeight*.76));e.style.height=Math.max(B,Math.min(i,P))+"px"}function oe(){return{width:A.width?t.containerWidth:z,height:A.height?t.containerHeight:y}}function Te(e){if(!e||typeof e!="object")return;let i=function(n,r){E[n]||(t[n]=r)};D.indexOf(e.mode)!==-1&&i("displayMode",e.mode),(e.side==="left"||e.side==="right")&&i("side",e.side),typeof e.launcher=="boolean"&&i("launcher",e.launcher),m(e.width)&&(i("containerWidth",Math.max(280,e.width)),E.containerWidth||(A.width=!0)),m(e.height)&&(i("containerHeight",Math.max(400,e.height)),E.containerHeight||(A.height=!0)),m(e.sidebar_width)&&i("sidebarWidth",Math.max(280,e.sidebar_width)),typeof e.search_placeholder=="string"&&e.search_placeholder&&i("searchPlaceholder",e.search_placeholder.slice(0,80)),m(e.offset_bottom)&&i("launcherBottom",Math.max(0,e.offset_bottom)),m(e.offset_side)&&i("launcherRight",Math.max(0,e.offset_side)),m(e.z_index)&&i("zIndex",Math.max(1,Math.floor(e.z_index)))}function Ie(){let e=j(),i=e?oe():{width:t.containerWidth,height:t.containerHeight};return{type:"WIDGET_DISPLAY",mode:e?"ask-ai":t.displayMode,width:i.width,height:i.height,hotkey:t.hotkey!==!1}}let l=null,R=[],V=!1,g=null,L=null,x=!1;function Z(e){l?e():x||R.push(e)}let X={},re=!1;function _(e,i){let n=X[e];!n||!n.length||n.slice().forEach(function(r){try{r(i)}catch(c){}})}function ze(e){let i=/^#?([0-9a-f]{6})$/i.exec((e||"").trim());if(!i)return"#0B0C10";let n=parseInt(i[1],16);return(.299*(n>>16&255)+.587*(n>>8&255)+.114*(n&255))/255>.6?"#0B0C10":"#FFFFFF"}let ae=!1,M=null,se=[];function N(){if(ae)return;ae=!0,M&&(clearTimeout(M),M=null);let e=document.getElementById(t.buttonId);e&&e.classList.remove("replypier-pending"),se.splice(0).forEach(i=>{try{i()}catch(n){}})}function F(){let e=document.getElementById(t.buttonId);if(!e)return;let i=e.querySelector(".rp-badge");if(!i)return;let n=t.hasOpened?t.unreadCount:(t.chatInitiationMessages||[]).length;i.textContent=n>99?"99+":String(n),i.style.display=n>0&&!e.classList.contains("active")?"flex":"none"}function O(){let e=t.displayMode,i=e==="sidebar-left"||e==="sidebar-right",n=e==="sidebar-left"?"left":e==="sidebar-right"?"right":t.side,r=e==="search-bar"?48:64,c=G.indexOf(t.chatStyle)===-1,a=j(),d=oe(),w=document.createElement("style");w.id="replypier-styles",w.textContent=` `;window.replypierId;window.replypierBaseUrl;window.ReplyPier;window.replypierConfig;(function(){function re(e){return/^#[0-9A-F]{6}$/i.test(e)}function oe(){return typeof window!="undefined"&&window.replypierBaseUrl?window.replypierBaseUrl:typeof window!="undefined"&&window.APP_CONFIG&&window.APP_CONFIG.API_URL?window.APP_CONFIG.API_URL:"https://api.replypier.site/api/v1"}let t={baseUrl:oe(),containerId:"replypier-container",backdropId:"replypier-backdrop",buttonId:"replypier-button",chatBubbleColor:"#f34611",loadingContainerId:"replypier-loading",tokenKey:"rpid",tokenProvider:null,containerBottom:100,containerRight:20,containerWidth:400,containerHeight:560,launcherBottom:20,launcherRight:20,side:"right",displayMode:"floating",launcher:!0,sidebarWidth:420,searchBarWidth:320,searchPlaceholder:"\u6709\u95EE\u9898\u968F\u65F6\u95EE\u6211\u2026",zIndex:999999,trigger:null,hotkey:!0,surfaceIsPalette:null,chatStyle:null,chatInitiationMessages:[],initiationMessageId:"replypier-initiation",initiationShownKey:"rpim_shown",unreadCount:0,hasOpened:!1},$={};function p(e,i){t[e]=i,$[e]=!0}let G=["floating","sidebar-left","sidebar-right","search-bar"],q=["ASK_ANYTHING","AURORA"],P=680,y=620,R=140,A={width:!1,height:!1},N=0;function V(){return t.surfaceIsPalette===!1?!1:(q.indexOf(t.chatStyle)!==-1||t.displayMode==="search-bar")&&t.displayMode!=="sidebar-left"&&t.displayMode!=="sidebar-right"}function Z(){let e=document.getElementById(t.containerId);if(!e)return;if(!V()||!N){e.style.height="";return}let i=Math.min(ae().height,Math.round(window.innerHeight*.76));e.style.height=Math.max(R,Math.min(i,N))+"px"}function ae(){return{width:A.width?t.containerWidth:P,height:A.height?t.containerHeight:y}}function Be(e){if(!e||typeof e!="object")return;let i=function(n,o){$[n]||(t[n]=o)};G.indexOf(e.mode)!==-1&&i("displayMode",e.mode),(e.side==="left"||e.side==="right")&&i("side",e.side),typeof e.launcher=="boolean"&&i("launcher",e.launcher),h(e.width)&&(i("containerWidth",Math.max(280,e.width)),$.containerWidth||(A.width=!0)),h(e.height)&&(i("containerHeight",Math.max(400,e.height)),$.containerHeight||(A.height=!0)),h(e.sidebar_width)&&i("sidebarWidth",Math.max(280,e.sidebar_width)),typeof e.search_placeholder=="string"&&e.search_placeholder&&i("searchPlaceholder",e.search_placeholder.slice(0,80)),h(e.offset_bottom)&&i("launcherBottom",Math.max(0,e.offset_bottom)),h(e.offset_side)&&i("launcherRight",Math.max(0,e.offset_side)),h(e.z_index)&&i("zIndex",Math.max(1,Math.floor(e.z_index)))}function ke(){let e=V(),i=e?ae():{width:t.containerWidth,height:t.containerHeight};return{type:"WIDGET_DISPLAY",mode:e?"ask-ai":t.displayMode,width:i.width,height:i.height,hotkey:t.hotkey!==!1}}let l=null,F=[],X=!1,g=null,L=null,x=!1;function J(e){l?e():x||F.push(e)}let Q={},se=!1;function _(e,i){let n=Q[e];!n||!n.length||n.slice().forEach(function(o){try{o(i)}catch(c){}})}function Pe(e){let i=/^#?([0-9a-f]{6})$/i.exec((e||"").trim());if(!i)return"#0B0C10";let n=parseInt(i[1],16);return(.299*(n>>16&255)+.587*(n>>8&255)+.114*(n&255))/255>.6?"#0B0C10":"#FFFFFF"}let le=!1,E=null,de=[];function ee(){if(le)return;le=!0,E&&(clearTimeout(E),E=null);let e=document.getElementById(t.buttonId);e&&e.classList.remove("replypier-pending"),de.splice(0).forEach(i=>{try{i()}catch(n){}})}function O(){let e=document.getElementById(t.buttonId);if(!e)return;let i=e.querySelector(".rp-badge");if(!i)return;let n=t.hasOpened?t.unreadCount:(t.chatInitiationMessages||[]).length;i.textContent=n>99?"99+":String(n),i.style.display=n>0&&!e.classList.contains("active")?"flex":"none"}function H(){let e=t.displayMode,i=e==="sidebar-left"||e==="sidebar-right",n=e==="sidebar-left"?"left":e==="sidebar-right"?"right":t.side,o=e==="search-bar"?48:64,c=q.indexOf(t.chatStyle)===-1,a=V(),d=ae(),v=document.createElement("style");v.id="replypier-styles",v.textContent=`
/* ===== Rounded-square launcher (design comp) ===== */ /* ===== Rounded-square launcher (design comp) ===== */
#${t.buttonId} { #${t.buttonId} {
position: fixed; position: fixed;
@@ -9,7 +9,7 @@
height: 64px; height: 64px;
border-radius: 20px 20px 20px 6px; border-radius: 20px 20px 20px 6px;
background: ${t.chatBubbleColor}; background: ${t.chatBubbleColor};
color: ${ze(t.chatBubbleColor)}; color: ${Pe(t.chatBubbleColor)};
box-shadow: 0 16px 40px -8px ${t.chatBubbleColor}cc, inset 0 0 0 1px rgba(255,255,255,0.08); box-shadow: 0 16px 40px -8px ${t.chatBubbleColor}cc, inset 0 0 0 1px rgba(255,255,255,0.08);
cursor: pointer; cursor: pointer;
z-index: ${t.zIndex}; z-index: ${t.zIndex};
@@ -482,7 +482,7 @@
position: fixed !important; position: fixed !important;
/* Sit above the launcher (+12px gap) and align to its edge on the active side, /* Sit above the launcher (+12px gap) and align to its edge on the active side,
so it never overlaps the icon regardless of the configured position/mode. */ so it never overlaps the icon regardless of the configured position/mode. */
bottom: ${(t.launcherBottom||20)+r+12}px !important; bottom: ${(t.launcherBottom||20)+o+12}px !important;
${n}: ${t.launcherRight||20}px !important; ${n}: ${t.launcherRight||20}px !important;
max-width: 260px !important; max-width: 260px !important;
background: white !important; background: white !important;
@@ -805,11 +805,35 @@
font-size: 14px; font-size: 14px;
line-height: 1.6; line-height: 1.6;
color: #64748b; color: #64748b;
margin: 0 0 24px 0; margin: 0 0 20px 0;
max-width: 280px; max-width: 280px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
} }
.replypier-error-retry {
min-width: 104px;
min-height: 44px;
padding: 10px 24px;
margin: 0 0 22px;
border: 0;
border-radius: 12px;
background: #4f46e5;
color: #fff;
font: 600 14px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
cursor: pointer;
}
.replypier-error-retry:hover:not(:disabled) {
background: #4338ca;
}
.replypier-error-retry:focus-visible {
outline: 3px solid #818cf8;
outline-offset: 3px;
}
.replypier-error-retry:disabled {
opacity: 0.7;
cursor: wait;
}
.replypier-error-footer { .replypier-error-footer {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -854,7 +878,6 @@
} }
.replypier-error-message { .replypier-error-message {
font-size: 13px;
max-width: 260px; max-width: 260px;
} }
} }
@@ -865,11 +888,11 @@
re-show (display: flex !important at equal specificity). */ re-show (display: flex !important at equal specificity). */
#${t.buttonId}, #${t.buttonId}.mobile-closed { display: none !important; } #${t.buttonId}, #${t.buttonId}.mobile-closed { display: none !important; }
`} `}
`;let f=document.getElementById("replypier-styles");f&&f.remove(),document.head.appendChild(w),ve()}function ve(){let e=document.getElementById(t.buttonId);if(!e)return;let i=e.querySelector(".rp-search-placeholder");i&&i.textContent!==t.searchPlaceholder&&(i.textContent=t.searchPlaceholder)}function C(e){if(typeof e!="string")return null;let i=e.trim();return!i||i==="undefined"||i==="null"?null:i}function le(){return C(window.replypierToken)||C(localStorage.getItem(t.tokenKey))}function de(e){let i=C(e);if(!i)return null;let n=i.split(".")[1];if(!n)return null;try{let r=JSON.parse(atob(n.replace(/-/g,"+").replace(/_/g,"/")));return r.sub||r.customer_id||null}catch(r){return null}}function J(e){e&&localStorage.setItem(t.tokenKey,e)}function W(){localStorage.removeItem(t.tokenKey)}function H(){return window.innerWidth<=768}function Be(e){return e?e.replace(/\uFFFD/g,"").replace(/[��]/g,"").replace(/[\x00-\x1F\x7F-\x9F]/g,"").trim():""}function Pe(){if(!t.chatInitiationMessages||t.chatInitiationMessages.length===0)return null;let e=Math.floor(Math.random()*t.chatInitiationMessages.length),i=t.chatInitiationMessages[e];return i?Be(i):null}function Re(){return H()||!t.launcher||!t.chatInitiationMessages||t.chatInitiationMessages.length===0?!1:!sessionStorage.getItem(t.initiationShownKey)}function pe(){sessionStorage.setItem(t.initiationShownKey,"true")}function Ne(){let e=Pe();if(!e)return null;let i=document.createElement("div");return i.id=t.initiationMessageId,i.innerHTML=` `;let m=document.getElementById("replypier-styles");m&&m.remove(),document.head.appendChild(v),$e()}function $e(){let e=document.getElementById(t.buttonId);if(!e)return;let i=e.querySelector(".rp-search-placeholder");i&&i.textContent!==t.searchPlaceholder&&(i.textContent=t.searchPlaceholder)}function M(e){if(typeof e!="string")return null;let i=e.trim();return!i||i==="undefined"||i==="null"?null:i}function pe(){return M(window.replypierToken)||M(localStorage.getItem(t.tokenKey))}function ce(e){let i=M(e);if(!i)return null;let n=i.split(".")[1];if(!n)return null;try{let o=JSON.parse(atob(n.replace(/-/g,"+").replace(/_/g,"/")));return o.sub||o.customer_id||null}catch(o){return null}}function te(e){e&&localStorage.setItem(t.tokenKey,e)}function W(){localStorage.removeItem(t.tokenKey)}function U(){return window.innerWidth<=768}function Re(e){return e?e.replace(/\uFFFD/g,"").replace(/[��]/g,"").replace(/[\x00-\x1F\x7F-\x9F]/g,"").trim():""}function Ne(){if(!t.chatInitiationMessages||t.chatInitiationMessages.length===0)return null;let e=Math.floor(Math.random()*t.chatInitiationMessages.length),i=t.chatInitiationMessages[e];return i?Re(i):null}function Fe(){return U()||!t.launcher||!t.chatInitiationMessages||t.chatInitiationMessages.length===0?!1:!sessionStorage.getItem(t.initiationShownKey)}function he(){sessionStorage.setItem(t.initiationShownKey,"true")}function Oe(){let e=Ne();if(!e)return null;let i=document.createElement("div");return i.id=t.initiationMessageId,i.innerHTML=`
<span class="initiation-orb"></span> <span class="initiation-orb"></span>
<button class="initiation-close" aria-label="\u5173\u95ED"></button> <button class="initiation-close" aria-label="\u5173\u95ED"></button>
<p class="initiation-message-text"></p> <p class="initiation-message-text"></p>
`,i.dataset.fullMessage=e,i}function Fe(e,i,n=50){let r=e.querySelector(".initiation-message-text");if(!r)return;let c=0,a=setInterval(()=>{c<i.length?(r.textContent+=i.charAt(c),c++):(clearInterval(a),r.classList.add("typing-complete"),e.typeInterval=null)},n);e.typeInterval=a}function Oe(e,i){if(!e||!Re())return;document.body.appendChild(e),setTimeout(()=>{e.classList.add("show"),setTimeout(()=>{let r=e.dataset.fullMessage;r&&Fe(e,r,40)},300)},1500),e.addEventListener("click",r=>{r.target.classList.contains("initiation-close")||(e.typeInterval&&clearInterval(e.typeInterval),ce(e),i(),pe())});let n=e.querySelector(".initiation-close");n&&n.addEventListener("click",r=>{r.stopPropagation(),e.typeInterval&&clearInterval(e.typeInterval),ce(e),pe()})}function ce(e){e&&(e.classList.remove("show"),setTimeout(()=>{e.parentNode&&e.parentNode.removeChild(e)},400))}function he(){x=!1,O();let e=document.createElement("div");e.id=t.buttonId,e.classList.add("replypier-pending"),e.innerHTML=` `,i.dataset.fullMessage=e,i}function He(e,i,n=50){let o=e.querySelector(".initiation-message-text");if(!o)return;let c=0,a=setInterval(()=>{c<i.length?(o.textContent+=i.charAt(c),c++):(clearInterval(a),o.classList.add("typing-complete"),e.typeInterval=null)},n);e.typeInterval=a}function We(e,i){if(!e||!Fe())return;document.body.appendChild(e),setTimeout(()=>{e.classList.add("show"),setTimeout(()=>{let o=e.dataset.fullMessage;o&&He(e,o,40)},300)},1500),e.addEventListener("click",o=>{o.target.classList.contains("initiation-close")||(e.typeInterval&&clearInterval(e.typeInterval),ue(e),i(),he())});let n=e.querySelector(".initiation-close");n&&n.addEventListener("click",o=>{o.stopPropagation(),e.typeInterval&&clearInterval(e.typeInterval),ue(e),he()})}function ue(e){e&&(e.classList.remove("show"),setTimeout(()=>{e.parentNode&&e.parentNode.removeChild(e)},400))}function me(){x=!1,H();let e=document.createElement("div");e.id=t.buttonId,e.classList.add("replypier-pending"),e.innerHTML=`
<span class="rp-ring"></span> <span class="rp-ring"></span>
<span class="rp-ring r2"></span> <span class="rp-ring r2"></span>
<div class="rp-dots"><span class="rp-dot"></span><span class="rp-dot"></span><span class="rp-dot"></span></div> <div class="rp-dots"><span class="rp-dot"></span><span class="rp-dot"></span><span class="rp-dot"></span></div>
@@ -879,11 +902,11 @@
<span class="rp-search-placeholder"></span> <span class="rp-search-placeholder"></span>
</span> </span>
<span class="rp-badge"></span> <span class="rp-badge"></span>
`,M=setTimeout(N,8e3);let i=document.createElement("div");i.id=t.containerId;let n=document.createElement("div");n.id="replypier-mobile-close",n.innerHTML=` `,E=setTimeout(ee,8e3);let i=document.createElement("div");i.id=t.containerId;let n=document.createElement("div");n.id="replypier-mobile-close",n.innerHTML=`
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 12H19" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M5 12H19" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
`;let r=document.createElement("div");r.id="replypier-mobile-topbar",r.innerHTML=` `;let o=document.createElement("div");o.id="replypier-mobile-topbar",o.innerHTML=`
<h3 class="topbar-title">\u804A\u5929</h3> <h3 class="topbar-title">\u804A\u5929</h3>
<div style="width: 44px;"></div> <div style="width: 44px;"></div>
`;let c=document.createElement("div");c.id=t.backdropId,document.body.appendChild(e),document.body.appendChild(c),document.body.appendChild(i),document.body.appendChild(n),document.body.appendChild(r),F(),ve();let a=!1,d=null,w=!1,f=null;function b(){a=!a,i.classList.toggle("active"),c.classList.toggle("active",a),e.classList.toggle("active"),n.classList.toggle("active"),a&&(t.hasOpened=!0,t.unreadCount=0),d&&d.contentWindow&&d.contentWindow.postMessage({type:"WIDGET_VISIBILITY",open:a},"*"),F(),a&&f&&(ce(f),f=null,pe()),H()?a?(e.classList.remove("mobile-closed"),e.style.animation=""):(e.classList.add("mobile-closed"),e.style.animation="replypier-float 4s ease-in-out infinite",r.classList.remove("active"),document.body.classList.remove("ask-anything-mobile")):a&&n.classList.remove("active"),a&&d&&d.contentWindow.postMessage({type:"SCROLL_TO_BOTTOM"},"*"),_(a?"open":"close")}let U=null,fe=!1;function Ge(o){let s=String(o||"").slice(0,2e3);s&&(fe&&d&&d.contentWindow?d.contentWindow.postMessage({type:"PREFILL_MESSAGE",text:s},"*"):U=s)}let K=0;function Ce(){for(K++;i.firstChild;)i.removeChild(i.firstChild);d=null,g=null,L=null,fe=!1,re=!1,U=null,w=!1,e.classList.remove("loading"),P=0,q()}function je(){a&&b(),Ce(),t.unreadCount=0,t.hasOpened=!1,F(),Q()}function qe(){a&&b(),Ce(),window.removeEventListener("resize",Le),window.removeEventListener("message",Ae),f&&(f.typeInterval&&clearInterval(f.typeInterval),me(f),f=null),[e,c,i,n,r].forEach(me),me(document.getElementById("replypier-styles")),document.body.classList.remove("ask-anything-mobile"),ae=!1,M&&(clearTimeout(M),M=null),se.length=0,t.unreadCount=0,t.hasOpened=!1,l=null,R.length=0,V=!1,x=!0}function Se(o){let s=document.createElement("div");s.className="replypier-error-ui";let I=document.createElement("div");I.className="replypier-error-card";let v=document.createElement("button");v.type="button",v.className="replypier-error-close",v.setAttribute("aria-label","\u5173\u95ED\u804A\u5929"),v.textContent="\xD7",v.addEventListener("click",function(){a&&b()}),s.appendChild(v);let u=document.createElement("div");u.className="replypier-error-icon-wrapper";let h=document.createElementNS("http://www.w3.org/2000/svg","svg");h.setAttribute("class","replypier-error-icon"),h.setAttribute("viewBox","0 0 24 24"),h.setAttribute("fill","none"),h.setAttribute("stroke","currentColor"),h.setAttribute("stroke-width","1.5");let k=document.createElementNS("http://www.w3.org/2000/svg","path");k.setAttribute("d","M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z");let $=document.createElementNS("http://www.w3.org/2000/svg","path");$.setAttribute("d","M9 12l2 2 4-4"),h.appendChild(k),h.appendChild($),u.appendChild(h);let S=document.createElement("h2");S.className="replypier-error-title",S.textContent="\u5BA2\u670D\u6682\u4E0D\u53EF\u7528";let be=document.createElement("p");be.className="replypier-error-message",be.textContent=o;let te=document.createElement("div");te.className="replypier-error-footer";let ye=document.createElement("span");ye.innerHTML=_e;let xe=ye.querySelector("svg");xe.setAttribute("width","20"),xe.setAttribute("height","16"),xe.setAttribute("aria-hidden","true");let T=document.createElement("a");return T.href="https://replypier.site",T.target="_blank",T.rel="noopener noreferrer",T.className="replypier-error-footer-link",T.textContent="\u7531 ReplyPier \u63D0\u4F9B\u652F\u6301",te.appendChild(ye),te.appendChild(T),I.appendChild(u),I.appendChild(S),I.appendChild(be),I.appendChild(te),s.appendChild(I),s}function Q(){return we(this,null,function*(){if(w||d)return;let o=K;try{w=!0,e.classList.add("loading");let s=le(),I=`${t.baseUrl}/widgets/${window.replypierId}/data?widget_id=${window.replypierId}`,v={method:"GET",mode:"cors",headers:s?{Authorization:`Bearer ${s}`}:{}};fetch(I,v).then(u=>we(null,null,function*(){if(o!==K)return null;if(u.status===401)try{let k=(yield u.json()).detail;if(k&&k.code==="identity_expired")return e.classList.remove("loading"),_("identityExpired"),Ve(),null;let $=typeof k=="string"?k:"";if($.includes("generate-token")||$.includes("API key")||$.includes("Token must be obtained")){e.classList.remove("loading"),W();let S=Se("\u6B64\u804A\u5929\u7EC4\u4EF6\u5C1A\u672A\u914D\u7F6E\u5B8C\u6210\uFF0C\u8BF7\u8054\u7CFB\u7F51\u7AD9\u7BA1\u7406\u5458\u5F00\u901A\u5728\u7EBF\u5BA2\u670D\u3002");return i.appendChild(S),N(),null}}catch(h){}if(!u.ok)throw new Error("HTTP error! status: "+u.status);return u.text()})).then(u=>{u!==null&&o===K&&(d=document.createElement("iframe"),d.className="replypier-iframe",d.title="\u804A\u5929",d.srcdoc=u,d.addEventListener("load",function(){fe=!0,d.contentWindow.postMessage(Ie(),"*"),d.contentWindow.postMessage({type:"WIDGET_VISIBILITY",open:a},"*"),U&&(d.contentWindow.postMessage({type:"PREFILL_MESSAGE",text:U},"*"),U=null),re=!0,Y=!1,_("ready")}),i.appendChild(d),g=d.contentWindow,e.classList.remove("loading"),d.style.opacity="1")}).catch(u=>{if(o!==K)return;console.error("Failed to load widget:",u),e.classList.rem Line truncated `;let c=document.createElement("div");c.id=t.backdropId,document.body.appendChild(e),document.body.appendChild(c),document.body.appendChild(i),document.body.appendChild(n),document.body.appendChild(o),O(),$e();let a=!1,d=null,v=!1,m=null;function b(){a=!a,i.classList.toggle("active"),c.classList.toggle("active",a),e.classList.toggle("active"),n.classList.toggle("active"),a&&(t.hasOpened=!0,t.unreadCount=0),d&&d.contentWindow&&d.contentWindow.postMessage({type:"WIDGET_VISIBILITY",open:a},"*"),O(),a&&m&&(ue(m),m=null,he()),U()?a?(e.classList.remove("mobile-closed"),e.style.animation=""):(e.classList.add("mobile-closed"),e.style.animation="replypier-float 4s ease-in-out infinite",o.classList.remove("active"),document.body.classList.remove("ask-anything-mobile")):a&&n.classList.remove("active"),a&&d&&d.contentWindow.postMessage({type:"SCROLL_TO_BOTTOM"},"*"),_(a?"open":"close")}let K=null,ge=!1;function qe(r){let s=String(r||"").slice(0,2e3);s&&(ge&&d&&d.contentWindow?d.contentWindow.postMessage({type:"PREFILL_MESSAGE",text:s},"*"):K=s)}let Y=0;function Ae(){for(Y++;i.firstChild;)i.removeChild(i.firstChild);d=null,g=null,L=null,ge=!1,se=!1,K=null,v=!1,e.classList.remove("loading"),N=0,Z()}function Ve(){a&&b(),Ae(),t.unreadCount=0,t.hasOpened=!1,O(),D()}function Ze(){a&&b(),Ae(),window.removeEventListener("resize",Te),window.removeEventListener("message",_e),m&&(m.typeInterval&&clearInterval(m.typeInterval),T(m),m=null),[e,c,i,n,o].forEach(T),T(document.getElementById("replypier-styles")),document.body.classList.remove("ask-anything-mobile"),le=!1,E&&(clearTimeout(E),E=null),de.length=0,t.unreadCount=0,t.hasOpened=!1,l=null,F.length=0,X=!1,x=!0}function Xe(r,s){let w=document.createElement("div");w.className="replypier-error-ui";let I=document.createElement("div");I.className="replypier-error-card";let k=document.createElement("button");k.type="button",k.className="replypier-error-close",k.setAttribute("aria-label","\u5173\u95ED\u804A\u5929"),k.textContent="\xD7",k.addEventListener("click",function(){a&&b()}),w.appendChild(k);let u=document.createElement("div");u.className="replypier-error-icon-wrapper";let f=document.createElementNS("http://www.w3.org/2000/svg","svg");f.setAttribute("class","replypier-error-icon"),f.setAttribute("viewBox","0 0 24 24"),f.setAttribute("aria-hidden","true"),f.setAttribute("fill","none"),f.setAttribute("stroke","currentColor"),f.setAttribute("stroke-width","1.5"),f.setAttribute("stroke-linecap","round"),f.setAttribute("stroke-linejoin","round");let C=document.createElementNS("http://www.w3.org/2000/svg","path");C.setAttribute("d","M5 4h14a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-8l-4 3v-3H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z");let z=document.createElementNS("http://www.w3.org/2000/svg","path");z.setAttribute("d","M8 10h8M8 13h5"),f.appendChild(C),f.appendChild(z),u.appendChild(f);let ye=document.createElement("h2");ye.className="replypier-error-title",ye.textContent="\u5BA2\u670D\u6682\u65F6\u65E0\u6CD5\u8FDE\u63A5";let xe=document.createElement("p");xe.className="replypier-error-message",xe.textContent=r;let S=document.createElement("button");S.type="button",S.className="replypier-error-retry",S.textContent="\u91CD\u8BD5",S.addEventListener("click",function(){S.disabled=!0,S.textContent="\u6B63\u5728\u91CD\u8BD5\u2026",s()});let ne=document.createElement("div");ne.className="replypier-error-footer";let we=document.createElement("span");we.innerHTML=ze;let Ie=we.querySelector("svg");Ie.setAttribute("width","20"),Ie.setAttribute("height","16"),Ie.setAttribute("aria-hidden","true");let B=document.createElement("a");return B.href="https://replypier.site",B.target="_blank",B.rel="noopener noreferrer",B.className="replypier-error-footer-link",B.textContent="\u7531 ReplyPier \u63D0\u4F9B\u652F\u6301",ne.appendChild(we),ne.appendChild(B),I.appendChild(u),I.appendChild(ye),I.appendChild(xe),I.appendChild(S),I.appendChild(ne),w.appendChild(I),w}function Le(r){T(r);let s=Xe("\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5\uFF0C\u6216\u7A0D\u540E\u518D\u6765\u3002",function(){D(s)});i.appendChild(s),ee()}function D(r){return ve(this,null,function*(){if(v||d)return;let s=Y;try{v=!0,e.classList.add("loading");let w=pe(),I=`${t.baseUrl}/widgets/${window.replypierId}/data?widget_id=${window.replypierId}`,k={method:"GET",mode:"cors",headers:w?{Authorization:`Bearer ${w}`}:{}};fetch(I,k).then(u=>ve(null,null,function*(){if(s!==Y)return null;if(u.status===401)try{let C=(yield u.json()).detail;if(C&&C.code==="identity_expired")return e.classList.remove("loading"),T(r),_("identityExpired"),Je(),null;let z=typeof C=="string"?C:"";if(z.includes("generate-token")||z.includes("API key")||z.includes("Token must be obtained"))return e.classList.remove("loading"),W(),console.error("Failed to load widget: visitor authentication required (HTTP 401); check the widget token configuration."),Le(r),null}catch(f){}if(!u.ok)throw new Error("HTTP error! status: "+u.status);return u.text()})).then(u=>{u!==null&&s===Y&&(T(r),d Line truncated
@@ -0,0 +1,155 @@
/*
Copyright 2024-2026 ReplyPier
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
describe('ReplyPier widget load errors', () => {
const html = '<!DOCTYPE html><html><body>widget</body></html>'
const okResponse = () => ({ ok: true, status: 200, text: async () => html })
const flush = () => new Promise((resolve) => setTimeout(resolve, 0))
let fetchMock: ReturnType<typeof vi.fn>
async function loadLoader() {
vi.resetModules()
await import('@/webclient/replypier.js')
await flush()
}
function retryButton() {
return document.querySelector('.replypier-error-retry') as HTMLButtonElement | null
}
beforeEach(() => {
document.body.innerHTML = ''
document.head.innerHTML = ''
localStorage.clear()
;(window as any).replypierId = 'w-test'
;(window as any).replypierBaseUrl = 'https://api.example.com/api/v1'
;(window as any).replypierToken = undefined
;(window as any).replypierConfig = undefined
fetchMock = vi.fn().mockResolvedValue(okResponse())
vi.stubGlobal('fetch', fetchMock)
vi.spyOn(console, 'error').mockImplementation(() => {})
})
afterEach(() => {
;(window as any).ReplyPier?.destroy()
delete (window as any).ReplyPier
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
it.each([
['network failure', () => Promise.reject(new TypeError('Failed to fetch'))],
['server error', () => Promise.resolve({ ok: false, status: 503 })],
])('shows actionable visitor-facing copy for a %s', async (_label, fail) => {
fetchMock.mockImplementationOnce(fail)
await loadLoader()
const error = document.querySelector('.replypier-error-ui')
expect(error?.textContent).toContain('客服暂时无法连接')
expect(error?.textContent).toContain('请检查网络后重试')
expect(error?.textContent).not.toMatch(/域名|后台|组织设置/)
expect(retryButton()?.textContent).toBe('重试')
expect(console.error).toHaveBeenCalledWith('Failed to load widget:', expect.any(Error))
})
it('retries in the open window, preserves the visitor token and avoids duplicate requests', async () => {
;(window as any).replypierToken = 'visitor-token'
let resolveRetry: (response: ReturnType<typeof okResponse>) => void = () => {}
fetchMock
.mockRejectedValueOnce(new TypeError('Failed to fetch'))
.mockImplementationOnce(() => new Promise((resolve) => { resolveRetry = resolve }))
await loadLoader()
const api = (window as any).ReplyPier
api.open()
const retry = retryButton()!
retry.click()
retry.click()
expect(retry.disabled).toBe(true)
expect(retry.textContent).toBe('正在重试…')
expect(fetchMock).toHaveBeenCalledTimes(2)
expect((fetchMock.mock.calls[1][1] as RequestInit).headers).toEqual({ Authorization: 'Bearer visitor-token' })
resolveRetry(okResponse())
await flush()
expect(api.isOpen()).toBe(true)
expect(document.querySelector('.replypier-error-ui')).toBeNull()
expect(document.querySelectorAll('.replypier-iframe')).toHaveLength(1)
})
it('lets the visitor retry again after another failure', async () => {
fetchMock.mockRejectedValueOnce(new TypeError('offline')).mockRejectedValueOnce(new TypeError('still offline'))
await loadLoader()
retryButton()!.click()
await flush()
expect(document.querySelectorAll('.replypier-error-ui')).toHaveLength(1)
expect(retryButton()?.disabled).toBe(false)
retryButton()!.click()
await flush()
expect(fetchMock).toHaveBeenCalledTimes(3)
expect(document.querySelector('.replypier-error-ui')).toBeNull()
expect(document.querySelector('.replypier-iframe')).not.toBeNull()
})
it('recovers an expired identity during a retry without leaving the error card over the chat', async () => {
;(window as any).replypierToken = 'expired-token'
fetchMock
.mockRejectedValueOnce(new TypeError('offline'))
.mockResolvedValueOnce({
ok: false,
status: 401,
json: async () => ({ detail: { code: 'identity_expired' } }),
})
await loadLoader()
retryButton()!.click()
await flush()
await flush()
expect(fetchMock).toHaveBeenCalledTimes(3)
expect((fetchMock.mock.calls[2][1] as RequestInit).headers).toEqual({})
expect(document.querySelector('.replypier-error-ui')).toBeNull()
expect(document.querySelector('.replypier-iframe')).not.toBeNull()
})
it('keeps the close control usable on the error screen', async () => {
fetchMock.mockRejectedValueOnce(new TypeError('offline'))
await loadLoader()
const api = (window as any).ReplyPier
api.open()
;(document.querySelector('.replypier-error-close') as HTMLButtonElement).click()
expect(api.isOpen()).toBe(false)
})
it('does not expose configuration details for a token-required widget', async () => {
fetchMock.mockResolvedValueOnce({
ok: false,
status: 401,
json: async () => ({ detail: 'Token must be obtained from /generate-token' }),
})
await loadLoader()
const error = document.querySelector('.replypier-error-ui')
expect(error?.textContent).toContain('客服暂时无法连接')
expect(error?.textContent).not.toMatch(/管理员|配置|token|API/i)
expect(retryButton()).not.toBeNull()
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('HTTP 401'))
})
})
+59 -26
View File
@@ -1092,11 +1092,35 @@ window.replypierConfig;
font-size: 14px; font-size: 14px;
line-height: 1.6; line-height: 1.6;
color: #64748b; color: #64748b;
margin: 0 0 24px 0; margin: 0 0 20px 0;
max-width: 280px; max-width: 280px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
} }
.replypier-error-retry {
min-width: 104px;
min-height: 44px;
padding: 10px 24px;
margin: 0 0 22px;
border: 0;
border-radius: 12px;
background: #4f46e5;
color: #fff;
font: 600 14px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
cursor: pointer;
}
.replypier-error-retry:hover:not(:disabled) {
background: #4338ca;
}
.replypier-error-retry:focus-visible {
outline: 3px solid #818cf8;
outline-offset: 3px;
}
.replypier-error-retry:disabled {
opacity: 0.7;
cursor: wait;
}
.replypier-error-footer { .replypier-error-footer {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1141,7 +1165,6 @@ window.replypierConfig;
} }
.replypier-error-message { .replypier-error-message {
font-size: 13px;
max-width: 260px; max-width: 260px;
} }
} }
@@ -1575,8 +1598,8 @@ window.replypierConfig;
isDestroyed = true isDestroyed = true
} }
// Create error UI for authentication failures using safe DOM methods // Keep technical failure details in the console, not in the visitor's UI.
function createErrorUI(message) { function createErrorUI(message, onRetry) {
const errorDiv = document.createElement('div'); const errorDiv = document.createElement('div');
errorDiv.className = 'replypier-error-ui'; errorDiv.className = 'replypier-error-ui';
@@ -1603,13 +1626,16 @@ window.replypierConfig;
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
iconSvg.setAttribute('class', 'replypier-error-icon'); iconSvg.setAttribute('class', 'replypier-error-icon');
iconSvg.setAttribute('viewBox', '0 0 24 24'); iconSvg.setAttribute('viewBox', '0 0 24 24');
iconSvg.setAttribute('aria-hidden', 'true');
iconSvg.setAttribute('fill', 'none'); iconSvg.setAttribute('fill', 'none');
iconSvg.setAttribute('stroke', 'currentColor'); iconSvg.setAttribute('stroke', 'currentColor');
iconSvg.setAttribute('stroke-width', '1.5'); iconSvg.setAttribute('stroke-width', '1.5');
iconSvg.setAttribute('stroke-linecap', 'round');
iconSvg.setAttribute('stroke-linejoin', 'round');
const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); const path1 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path1.setAttribute('d', 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z'); path1.setAttribute('d', 'M5 4h14a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-8l-4 3v-3H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z');
const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path'); const path2 = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path2.setAttribute('d', 'M9 12l2 2 4-4'); path2.setAttribute('d', 'M8 10h8M8 13h5');
iconSvg.appendChild(path1); iconSvg.appendChild(path1);
iconSvg.appendChild(path2); iconSvg.appendChild(path2);
iconWrapper.appendChild(iconSvg); iconWrapper.appendChild(iconSvg);
@@ -1617,13 +1643,23 @@ window.replypierConfig;
// Title // Title
const title = document.createElement('h2'); const title = document.createElement('h2');
title.className = 'replypier-error-title'; title.className = 'replypier-error-title';
title.textContent = '客服暂不可用'; title.textContent = '客服暂时无法连接';
// Message // Message
const messageEl = document.createElement('p'); const messageEl = document.createElement('p');
messageEl.className = 'replypier-error-message'; messageEl.className = 'replypier-error-message';
messageEl.textContent = message; messageEl.textContent = message;
const retryButton = document.createElement('button');
retryButton.type = 'button';
retryButton.className = 'replypier-error-retry';
retryButton.textContent = '重试';
retryButton.addEventListener('click', function () {
retryButton.disabled = true;
retryButton.textContent = '正在重试…';
onRetry();
});
// Footer // Footer
const footer = document.createElement('div'); const footer = document.createElement('div');
footer.className = 'replypier-error-footer'; footer.className = 'replypier-error-footer';
@@ -1647,14 +1683,24 @@ window.replypierConfig;
card.appendChild(iconWrapper); card.appendChild(iconWrapper);
card.appendChild(title); card.appendChild(title);
card.appendChild(messageEl); card.appendChild(messageEl);
card.appendChild(retryButton);
card.appendChild(footer); card.appendChild(footer);
errorDiv.appendChild(card); errorDiv.appendChild(card);
return errorDiv; return errorDiv;
} }
function showLoadError(previousError) {
removeNode(previousError)
const errorUI = createErrorUI('请检查网络后重试,或稍后再来。', function () {
prefetchWidget(errorUI)
})
container.appendChild(errorUI)
revealButton()
}
// Start prefetching the widget data // Start prefetching the widget data
async function prefetchWidget() { async function prefetchWidget(previousError) {
if (isLoading || iframe) return if (isLoading || iframe) return
// A reload for a new identity bumps loadGeneration, so a response that was // A reload for a new identity bumps loadGeneration, so a response that was
@@ -1690,6 +1736,7 @@ window.replypierConfig;
// error or silently starting over as a stranger. // error or silently starting over as a stranger.
if (detail && detail.code === 'identity_expired') { if (detail && detail.code === 'identity_expired') {
button.classList.remove('loading') button.classList.remove('loading')
removeNode(previousError)
emitEvent('identityExpired') emitEvent('identityExpired')
recoverIdentityAndLoad() recoverIdentityAndLoad()
return null; return null;
@@ -1699,11 +1746,8 @@ window.replypierConfig;
if (errorDetail.includes('generate-token') || errorDetail.includes('API key') || errorDetail.includes('Token must be obtained')) { if (errorDetail.includes('generate-token') || errorDetail.includes('API key') || errorDetail.includes('Token must be obtained')) {
button.classList.remove('loading') button.classList.remove('loading')
removeToken(); removeToken();
const errorUI = createErrorUI('此聊天组件尚未配置完成,请联系网站管理员开通在线客服。'); console.error('Failed to load widget: visitor authentication required (HTTP 401); check the widget token configuration.')
container.appendChild(errorUI); showLoadError(previousError)
// Reveal (with the default color) so this error is reachable —
// it'll never get a real color since the widget never loaded.
revealButton();
return null; return null;
} }
} catch { } catch {
@@ -1720,6 +1764,7 @@ window.replypierConfig;
if (html === null) return; // Error was already handled if (html === null) return; // Error was already handled
if (generation !== loadGeneration) return; // superseded by a reload if (generation !== loadGeneration) return; // superseded by a reload
removeNode(previousError)
iframe = document.createElement('iframe') iframe = document.createElement('iframe')
iframe.className = 'replypier-iframe' iframe.className = 'replypier-iframe'
// Screen readers announce a frame by its title. // Screen readers announce a frame by its title.
@@ -1753,19 +1798,7 @@ window.replypierConfig;
if (generation !== loadGeneration) return; // superseded by a reload if (generation !== loadGeneration) return; // superseded by a reload
console.error('Failed to load widget:', error) console.error('Failed to load widget:', error)
button.classList.remove('loading') button.classList.remove('loading')
// A failed fetch here is almost always the browser blocking the request showLoadError(previousError)
// because this domain isn't in the organization's allowed domains (CORS).
// Point the site owner at the fix instead of a dead-end "try again later".
const host = window.location.hostname || '当前域名';
const isLocal = /^(localhost|127\.0\.0\.1|0\.0\.0\.0|\[?::1\]?)$/i.test(host) || /\.local$/i.test(host);
const message = isLocal
? '聊天窗口无法在“' + host + '”上加载。本地测试时,请在 ReplyPier 后台(组织设置)中把组织域名设为“' + host + '”。'
: '聊天窗口无法在“' + host + '”上加载。请在 ReplyPier 后台(组织设置)中将此域名加入组织,即可在此启用聊天。';
const errorUI = createErrorUI(message);
container.appendChild(errorUI);
// Reveal (with the default color) so this error is reachable — it'll
// never get a real color since the widget never loaded.
revealButton();
}); });
} catch (error) { } catch (error) {
+3 -3
View File
@@ -60,9 +60,9 @@ npm start
## Troubleshooting ## Troubleshooting
- **"Chat can't load on 'localhost'"** — the origin isn't in your organization's allowed - **“客服暂时无法连接”** — check the browser console and the widget-data request in
domains (Organization settings). The port matters: `localhost:3000` allowed does not Network for network, HTTP, or CORS failures. If the request was rejected by CORS,
permit `localhost:3010`. check the page origin against the organization's allowed domain, including its port.
- **Dashboard placement settings not applying** — almost always a stale loader. Check the - **Dashboard placement settings not applying** — almost always a stale loader. Check the
served file has the newer features: `curl -s <loader-url> | grep -c sidebar-left`. served file has the newer features: `curl -s <loader-url> | grep -c sidebar-left`.
- **Buttons do nothing on the gallery pages** — the loader failed to load; the page shows - **Buttons do nothing on the gallery pages** — the loader failed to load; the page shows