@shenyin/embedded-call-widget 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,7 +26,7 @@ createCallButton({
26
26
 
27
27
  ```html
28
28
  <div id="call-button"></div>
29
- <script src="https://cdn.jsdelivr.net/npm/@shenyin/embedded-call-widget@3.0.0/embedded-call-widget.iife.js"></script>
29
+ <script src="https://cdn.jsdelivr.net/npm/@shenyin/embedded-call-widget@3.0.1/embedded-call-widget.iife.js"></script>
30
30
  <script>
31
31
  EmbeddedCallWidget.createCallButton({
32
32
  mount: '#call-button',
package/checksums.txt CHANGED
@@ -1,5 +1,5 @@
1
1
  742604b728025e5eab204cc78821d3504487f9b355c395db21c99d9f50b0da93 embedded-call-widget.js
2
- c39b8f09da1553f53238ac3b2460afef6bf22d6284cd04810cb600768ce34efe embedded-call-widget-runtime.js
2
+ 8ef265e594383cce14b511cecfc3611508a187cc6d133dbb66a3c84330720db5 embedded-call-widget-runtime.js
3
3
  410eabc93a3c197059dafc571928046daddc88066b0263c3ce9e071885a8d242 embedded-call-widget.iife.js
4
- 42e75ecc62f3beb612bd2537f4951a47a3d4f525d89347ef08982f7157f29317 embedded-call-widget-runtime.iife.js
5
- ff3912afaf2eb248f5e2f3c14108ef79beae269eb66992f221e06443e87cf55d manifest.json
4
+ 203b997a290a0c7a01d3e4a30f9f941148ff9b95c3c8f2b5729fc599c061f0fd embedded-call-widget-runtime.iife.js
5
+ ce057b3af063236af158e7fc877abf986af95ebd554298236a0c54f125b91e09 manifest.json
@@ -318,4 +318,4 @@ ${JSON.stringify(e,null,2)}`:a}function Oi(a){if(typeof window>"u"||!a)return!1;
318
318
  </section>
319
319
  <audio class="audio" data-role="audio" autoplay playsinline></audio>
320
320
  </section>
321
- `,this.refs.trigger=this.shadowRoot.querySelector('[data-role="trigger"]'),this.refs.triggerBadge=this.shadowRoot.querySelector('[data-role="trigger-badge"]'),this.refs.panelShell=this.shadowRoot.querySelector('[data-role="panel-shell"]'),this.refs.panel=this.shadowRoot.querySelector('[data-role="panel"]'),this.refs.panelEyebrow=this.shadowRoot.querySelector('[data-role="panel-eyebrow"]'),this.refs.panelTitle=this.shadowRoot.querySelector('[data-role="panel-title"]'),this.refs.panelDetail=this.shadowRoot.querySelector('[data-role="panel-detail"]'),this.refs.actions=this.shadowRoot.querySelector('[data-role="actions"]'),this.refs.status=this.shadowRoot.querySelector('[data-role="status"]'),this.refs.debugPanel=this.shadowRoot.querySelector('[data-role="debug-panel"]'),this.refs.availability=this.shadowRoot.querySelector('[data-role="availability"]'),this.refs.hint=this.shadowRoot.querySelector('[data-role="hint"]'),this.refs.meta=this.shadowRoot.querySelector('[data-role="meta"]'),this.refs.primary=this.shadowRoot.querySelector('[data-role="primary"]'),this.refs.secondary=this.shadowRoot.querySelector('[data-role="secondary"]'),this.refs.logs=this.shadowRoot.querySelector('[data-role="logs"]'),this.refs.audio=this.shadowRoot.querySelector('[data-role="audio"]'),this.refs.trigger.addEventListener("click",()=>{this.handleTriggerClick()}),this.refs.primary.addEventListener("click",()=>{this.handlePrimaryAction()}),this.refs.secondary.addEventListener("click",()=>{this.handleSecondaryAction()}),this.bindPageLifecycle(),this.registerE2EBridge(),this.startPresenceTouch(),this.render()}bindPageLifecycle(){typeof document>"u"||(this.visibilityHandler=()=>{if(this.pageVisible=document.visibilityState==="visible",document.visibilityState==="hidden"){this.reportIssuedSessionEvent("visibility-hidden",{visible:!1}),this.touchWidgetPresence("visibility-hidden"),this.issuedSession?.session_id&&this.touchIssuedSession(this.state,{trigger:"visibility-hidden"}),this.appendLog("页面已切到后台",{state:this.state,advice:"系统会继续自动保活;回到前台后请确认页面仍保持活动。"}),this.isMobileViewport&&["requesting","registering","calling","connected"].includes(this.state)&&(this.stateDetail="页面已切到后台;手机网页回到前台后请确认通话仍在继续。",this.render());return}if(this.isMobileViewport&&(this.appendLog("页面已回到前台",{state:this.state}),["requesting","registering","calling","connected"].includes(this.state)&&(this.stateDetail=ie[this.state]||ie.idle,this.render())),this.touchWidgetPresence("visibility-visible"),!this.legacyOutboundOnly&&["standby","registering","standbyRequesting","failed","idle","missed","rejected","ended"].includes(this.state)){const e=this.client?.getConnectionSnapshot?.()||null,t=!!(e?.connected&&(this.state!=="standby"||e?.registered));this.issuedSession?.session_id&&t?this.touchIssuedSession(this.state,{trigger:"visibility-visible"}):(this.appendLog("页面回到前台后检测到待机链路未处于健康状态,准备自动重建。",{state:this.state,has_session:!!this.issuedSession?.session_id,client_snapshot:e}),this.client||this.issuedSession?.session_id?this.disconnectAndCleanup("visibility-visible-unhealthy-client"):this.startStandby({force:!0}))}this.reportIssuedSessionEvent("visibility-visible",{visible:!0}),this.render()},document.addEventListener("visibilitychange",this.visibilityHandler),typeof window<"u"&&(this.onlineHandler=()=>{this.networkOnline=!0,this.setAvailabilityState("recovering","网络已恢复,正在重新连接网页电话。"),this.touchWidgetPresence("browser-online"),this.legacyOutboundOnly||this.restartStandbyAfterConnectivity("browser-online")},this.offlineHandler=()=>{this.networkOnline=!1,this.setAvailabilityState("offline","当前网络连接已断开,正在等待恢复。"),["incoming","connected"].includes(this.state)?this.render():this.setState("failed","当前网络连接已断开,正在等待恢复。")},window.addEventListener("online",this.onlineHandler),window.addEventListener("offline",this.offlineHandler),this.beforeUnloadHandler=()=>{this.flushUnloadCleanup("beforeunload")},this.pageHideHandler=e=>{e?.persisted||this.flushUnloadCleanup("pagehide")},window.addEventListener("beforeunload",this.beforeUnloadHandler),window.addEventListener("pagehide",this.pageHideHandler)))}open(){this.mountNode.scrollIntoView({block:"nearest",inline:"nearest"})}beginLifecycleRun(){return this.lifecycleRunId+=1,this.lifecycleRunId}assertLifecycleRunCurrent(e){if(e!==this.lifecycleRunId)throw Gi("网页电话已切换到新的连接流程,旧流程不再继续。")}emitHostCallback(e,t){const i=this.options?.[e];if(Bi(i))try{i(t)}catch(s){this.appendLog("宿主回调执行失败",{callback:e,message:s?.message||String(s)})}}getSnapshot(){const e=this.getVisibleState();return{mode:this.mode,state:this.state,stateDetail:this.stateDetail,visibleState:e,panelOpen:this.isPanelVisible(),sessionMode:this.issuedSession?.session_mode||null,sessionId:this.issuedSession?.session_id||null,siteKey:this.options.siteKey||this.bootstrap?.default_site_key||null,businessKey:this.issuedSession?.resolved_business_key||le(this.options,this.bootstrap)||null,standbyMode:this.issuedSession?.standby_mode||null,standbyState:this.issuedSession?.standby_state||this.options.standbyState||null,widgetSipNumber:this.issuedSession?.widget_anchor_number||null,browserSipUsername:this.issuedSession?.browser_sip_username||this.issuedSession?.sip_username||null,routeNote:this.issuedSession?.route_note||null,incomingAnchors:Array.isArray(this.issuedSession?.incoming_anchors)?this.issuedSession.incoming_anchors:[],hasClient:!!this.client,hasIssuedSession:!!this.issuedSession?.session_id,primaryText:this.refs.primary?.textContent?.trim()||"",secondaryText:this.refs.secondary?.textContent?.trim()||"",statusText:this.refs.status?.textContent?.trim()||"",availabilityState:this.availabilityState,availabilityLabel:this.refs.availability?.textContent?.trim()||"",triggerBadge:this.refs.triggerBadge?.textContent?.trim()||"",missedIncomingCount:this.missedIncomingCount,triggerHintDismissed:this.triggerHintDismissed,debugRequested:this.debugRequested,debugPanelEnabled:this.debugPanelEnabled,presenceId:this.presenceId,networkOnline:this.networkOnline,pendingIncomingCall:this.pendingIncomingCall}}getVisibleState(){return this.transientPanelState?this.transientPanelState:this.state==="incoming"?"ringing":["requesting","registering","calling"].includes(this.state)?"dialing":this.state==="connected"?"connected":"idle"}isPanelVisible(){return this.getVisibleState()!=="idle"}clearPanelAutoClose(){this.panelAutoCloseTimer&&(window.clearTimeout(this.panelAutoCloseTimer),this.panelAutoCloseTimer=null)}clearTransientPanelState(){this.clearPanelAutoClose(),this.transientPanelState=null,this.transientPanelDetail="",this.render()}setTransientPanelState(e,t="",i=Ii){this.clearPanelAutoClose(),this.transientPanelState=e,this.transientPanelDetail=t,this.render(),i>0&&(this.panelAutoCloseTimer=window.setTimeout(()=>{this.panelAutoCloseTimer=null,this.transientPanelState=null,this.transientPanelDetail="",this.render()},i))}markDebugPanelEnabled(e){!this.debugRequested||!this.hasAccessToken||this.debugPanelEnabled||(this.debugPanelEnabled=!0,this.appendLog("调试模式门禁已成立,开始展示调试面板。",{source:e}),this.render())}dismissTriggerHint(){this.triggerHintDismissed||(this.triggerHintDismissed=!0,Fi(this.triggerHintStorageKey,!0))}clearMissedIncomingCount(){this.missedIncomingCount&&(this.missedIncomingCount=0)}incrementMissedIncomingCount(){this.missedIncomingCount+=1}async handleTriggerClick(){if(this.dismissTriggerHint(),this.clearMissedIncomingCount(),this.clearTransientPanelState(),this.open(),["requesting","registering","calling","connected","incoming"].includes(this.state)){this.render();return}await this.connectAndCall()}registerE2EBridge(){if(!this.e2eBridgeEnabled||typeof window>"u")return;const e=window.location.hostname||"example.invalid";window.__embeddedCallWidgetE2E__={getSnapshot:()=>this.getSnapshot(),simulateIncomingCall:(t={})=>{const i=t.host||e,s=t.user||"2001";return this.pendingIncomingCall={displayName:t.displayName||"测试来电",user:s,host:i,direction:"inbound",uri:t.uri||`sip:${s}@${i}`,e2eSimulated:!0},this.clearPanelAutoClose(),this.transientPanelState=null,this.transientPanelDetail="",this.setAvailabilityState("working","已有新来电,请直接接听或挂断。"),this.setState("incoming"),this.emitHostCallback("onIncomingCall",this.pendingIncomingCall),this.getSnapshot()},simulateOutboundDialing:(t={})=>(this.dismissTriggerHint(),this.clearMissedIncomingCount(),this.clearPanelAutoClose(),this.transientPanelState=null,this.transientPanelDetail="",this.pendingIncomingCall=t&&typeof t=="object"?{...t,direction:"outbound",e2eSimulated:!0}:null,this.setAvailabilityState("working","测试桥已模拟发起外呼。"),this.setState("calling",t?.message||"测试桥已模拟拨打管理员。"),this.getSnapshot()),simulateOutboundConnected:(t={})=>(this.clearPanelAutoClose(),this.transientPanelState=null,this.transientPanelDetail="",t&&typeof t=="object"&&(this.pendingIncomingCall={...this.pendingIncomingCall||{},...t,direction:"outbound",e2eSimulated:!0}),this.setAvailabilityState("working","测试桥已模拟外呼接通。"),this.setState("connected",t?.message||"测试桥已模拟通话接通。"),this.getSnapshot()),answerIncomingCall:async()=>(await this.answerIncomingCall(),this.getSnapshot()),declineIncomingCall:async()=>(await this.declineIncomingCall(),this.getSnapshot()),hangupCall:async()=>(await this.hangupCall(),this.getSnapshot()),destroyWidget:async()=>(await this.destroy(),null)}}async handlePrimaryAction(){if(this.state==="incoming"){await this.answerIncomingCall();return}["calling","connected"].includes(this.state)||await this.handleTriggerClick()}async handleSecondaryAction(){if(this.state==="incoming"){await this.declineIncomingCall();return}if(["calling","connected"].includes(this.state)&&this.client){await this.hangupCall();return}await this.disconnectAndCleanup("manual-disconnect")}async connectAndCall(){if(["requesting","registering","calling","connected"].includes(this.state))return;const e=this.beginLifecycleRun();try{this.terminalFailureOverride="",this.clearAutoStandbyRecovery(),this.pendingLifecycleMode="outbound",this.setAvailabilityState("working","正在建立呼叫链路。");const t=this.issuedSession?.session_mode==="widget_standby"&&(this.issuedSession?.browser_sip_username||this.issuedSession?.sip_username)||null;!this.legacyOutboundOnly&&this.issuedSession?.session_mode==="widget_standby"&&(this.appendLog("访客主动呼叫前,先释放当前待机会话并切换到呼叫链路。",{widget_anchor_number:this.issuedSession?.widget_anchor_number||null,resolved_business_key:this.issuedSession?.resolved_business_key||le(this.options)||null}),await this.disconnectAndCleanup("switch-to-outbound-call",{preserveLifecycleRun:!0,suppressStateReset:!0,suppressClientTeardownState:"switch-to-outbound-call"}),this.assertLifecycleRunCurrent(e)),this.setState("requesting"),await this.loadBootstrap(),this.assertLifecycleRunCurrent(e),this.turnIceServers=await this.fetchTurnIceServers(),this.assertLifecycleRunCurrent(e);const i=await this.fetchIssuedSession({preferredBrowserSipUsername:t});this.assertLifecycleRunCurrent(e),await this.ensureClient(i),this.assertLifecycleRunCurrent(e),this.setState("registering"),await this.client.ensureReady(),this.assertLifecycleRunCurrent(e),this.setState("calling"),this.syncAvailabilityState(),await this.client.call(i.target),this.assertLifecycleRunCurrent(e),this.appendLog("已发起呼叫",{target:i.target,route_note:i.routeNote||null})}catch(t){if(this.pendingLifecycleMode=null,_t(t)){this.appendLog("已忽略过期的网页电话呼叫流程",{message:t?.message||String(t)});return}if(Oe(t)){await this.handleProtocolViolation(t);return}const i=t?.message||String(t);if(this.terminalFailureOverride=i,this.setState("failed",i),this.setTransientPanelState("failed","当前暂时无法接通,请稍后再试。"),Qe(t)||this.networkOnline===!1?this.setAvailabilityState("offline",this.networkOnline===!1?"当前网络连接已断开,正在等待恢复。":"当前无法连接通话服务,正在自动尝试恢复。"):this.setAvailabilityState("recovering","呼叫失败,正在准备恢复网页电话待机。"),this.appendLog("连接并呼叫失败",{message:i}),await this.releaseIssuedSession("failed","connect-and-call-failed"),!this.legacyOutboundOnly){this.appendLog("呼叫失败后,网页电话将恢复待机。",{message:i}),await this.startStandby({force:!0});return}this.setState("failed",i)}}async startStandby(e={}){if(this.legacyOutboundOnly)throw new Error("当前 widget 仍处于 legacy outbound 兼容模式");const{force:t=!1}=e;if(!t&&["standbyRequesting","registering","incoming","connected"].includes(this.state))return;const i=this.beginLifecycleRun();try{this.clearAutoStandbyRecovery(),this.pendingLifecycleMode="standby",this.terminalFailureOverride="",this.setState("standbyRequesting"),this.setAvailabilityState("preparing","正在申请新的待机会话。"),await this.loadBootstrap(),this.assertLifecycleRunCurrent(i),this.turnIceServers=await this.fetchTurnIceServers(),this.assertLifecycleRunCurrent(i);const s=await this.fetchStandbySession();this.assertLifecycleRunCurrent(i),await this.ensureClient(s),this.assertLifecycleRunCurrent(i),this.setState("registering","待机会话已拿到,正在连接并执行 REGISTER。"),await this.client.ensureReady(),this.assertLifecycleRunCurrent(i),this.pendingLifecycleMode=null,this.setState("standby"),this.setAvailabilityState("ready","当前可被呼叫,也可由访客主动发起呼叫。"),this.appendLog("widget 已进入待机",{session_id:this.issuedSession?.session_id||null,route_business_key:this.issuedSession?.resolved_business_key||le(this.options)||null,standby_mode:this.issuedSession?.standby_mode||null,widget_anchor_number:this.issuedSession?.widget_anchor_number||null,browser_sip_username:this.issuedSession?.browser_sip_username||this.issuedSession?.sip_username||null}),this.emitHostCallback("onStandbyReady",this.getSnapshot())}catch(s){if(_t(s)){this.appendLog("已忽略过期的网页电话待机流程",{message:s?.message||String(s)});return}if(this.pendingLifecycleMode=null,Oe(s)){await this.handleProtocolViolation(s);return}const r=s?.message||String(s);this.terminalFailureOverride=r,this.setState("failed",r),Qe(s)||this.networkOnline===!1?this.setAvailabilityState("offline",this.networkOnline===!1?"当前网络连接已断开,正在等待恢复。":"当前无法连接通话服务,正在自动尝试恢复。"):this.setAvailabilityState("recovering","当前未能完成待机注册,系统会继续自动恢复。"),this.appendLog("进入待机失败",{message:r}),await this.releaseIssuedSession("failed","standby-start-failed"),this.setState("failed",r),!this.legacyOutboundOnly&&this.networkOnline!==!1&&this.scheduleAutoStandbyRecovery("standby-start-failed")}}async ensureClient(e){const t={sip_username:e.sipUsername,sip_password:e.password,sip_domain:e.sipDomain,webrtc_url:e.transport,ice_servers:this.turnIceServers};this.client?.matchesAccount(t)||(this.client&&await this.client.destroy(),this.client=new Si(t,{remoteAudioElement:this.refs.audio,onStateChange:(i,s)=>{this.handleClientState(i,s)},onError:i=>this.appendLog("SIP 客户端错误",{message:i?.message||String(i)})}))}async handleClientState(e,t={}){if(this.syncSessionTouch(e),this.reportIssuedSessionEvent(e,t,e),(e==="connected"||e==="registered"||e==="ringing"||e==="dialing"||e==="answered")&&await this.clearTransportDisconnectGrace(!0),e==="registered"){this.pendingLifecycleMode=null,this.issuedSession?.session_mode==="widget_standby"?(this.setState("standby"),this.setAvailabilityState("ready","当前可被呼叫,也可由访客主动发起呼叫。")):this.setState("registering","REGISTER 已完成,正在准备发起呼叫。");return}if(e==="incoming"){this.pendingLifecycleMode=null,this.pendingIncomingCall=t&&typeof t=="object"?{...t}:null,this.clearPanelAutoClose(),this.transientPanelState=null,this.transientPanelDetail="",this.setState("incoming"),this.setAvailabilityState("working","已有新来电,请直接接听或拒接。"),this.appendLog("收到新的来电",this.pendingIncomingCall),this.emitHostCallback("onIncomingCall",this.pendingIncomingCall);return}if(e==="ringing"||e==="dialing"){this.setState("calling",t?.message||ie.calling);return}if(e==="answered"){this.pendingLifecycleMode=null,this.terminalFailureOverride="";const i=this.pendingIncomingCall||t||null;this.clearPanelAutoClose(),this.transientPanelState=null,this.transientPanelDetail="",this.setState("connected"),this.setAvailabilityState("working","当前通话已接通。"),this.emitHostCallback("onCallAnswered",i);return}if(e==="terminated"){if(this.issuedSession?.session_mode==="widget_standby"){await this.handleStandbyTermination(t);return}t&&typeof t=="object"&&(t.source==="invite-response"||t.source==="invite-exception"||t.code||t.status||t.stage)&&(this.terminalFailureOverride=t?.message||ie.failed,this.setState("failed",this.terminalFailureOverride)),await this.finalizeAfterTermination(t?.hangupSource||"terminated");return}if(e==="failed"){if(this.isExpectedClientTeardown())return;this.pendingLifecycleMode=null,await this.clearTransportDisconnectGrace(!1),this.stopSessionTouch(),this.terminalFailureOverride=t?.message||ie.failed,this.setState("failed",this.terminalFailureOverride),this.setTransientPanelState("failed","当前暂时无法接通,请稍后再试。"),this.setAvailabilityState("recovering","通话链路异常断开,系统会继续自动恢复。"),await this.releaseIssuedSession("failed","sip-client-failed");return}if(e==="disconnected"||e==="unregistered"){if(this.isExpectedClientTeardown()||this.isExpectedTermination()||!this.issuedSession?.session_id&&this.pendingLifecycleMode==="standby")return;if(this.stopSessionTouch(),["calling","connected"].includes(this.state)){this.setState("connected","SIP 信令短暂中断,正在等待恢复。"),this.setAvailabilityState("recovering","通话信令短暂中断,正在等待恢复。"),this.scheduleTransportDisconnectGrace(e);return}const s=this.terminalFailureOverride||this.client?.lastFailureMeta?.message||null;if(s){this.terminalFailureOverride=s,this.setState("failed",s),this.setTransientPanelState("failed","当前暂时无法接通,请稍后再试。");return}if(this.state==="failed")return;this.state!=="idle"&&this.state!=="ended"&&(this.pendingLifecycleMode=null,this.setState("ended"),this.setAvailabilityState("preparing","通话已结束,正在自动返回网页电话待机。"),this.scheduleAutoStandbyRecovery(`client-${e}`))}}stopTransportDisconnectGrace(){this.transportDisconnectTimer&&(window.clearTimeout(this.transportDisconnectTimer),this.transportDisconnectTimer=null),this.transportDisconnectContext=null}sameTransportDisconnectContext(e){return!e||!this.issuedSession?.session_id?!1:e.sessionId===this.issuedSession.session_id}async clearTransportDisconnectGrace(e){const t=this.transportDisconnectContext,i=!!(e&&t?.reported&&this.sameTransportDisconnectContext(t));this.stopTransportDisconnectGrace(),i&&(await this.reportIssuedSessionEvent("transport-restored",{disconnectDurationMs:Date.now()-(t.startedAt||Date.now()),graceMs:Re},this.state),this.appendLog("网页通话信令已恢复",{disconnectDurationMs:Date.now()-(t.startedAt||Date.now()),graceMs:Re}))}scheduleTransportDisconnectGrace(e){if(!this.issuedSession?.session_id)return;this.stopTransportDisconnectGrace();const t={sessionId:this.issuedSession.session_id,startedAt:Date.now(),reported:!1,state:e};this.transportDisconnectContext=t,this.transportDisconnectTimer=window.setTimeout(()=>{this.transportDisconnectTimer=null,this.sameTransportDisconnectContext(t)&&(t.reported=!0,this.reportIssuedSessionEvent("transport-disconnected",{state:e,graceMs:Re},this.state),this.appendLog("网页通话信令断开超过宽限窗口",{graceMs:Re,state:e}))},Re)}async finalizeAfterTermination(e){if(!this.finalizing){this.beginLifecycleRun(),this.finalizing=!0;try{this.pendingLifecycleMode=null,this.stopSessionTouch(),this.stopTransportDisconnectGrace(),await this.releaseIssuedSession("released",e),this.client&&(await this.client.destroy(),this.client=null);const t=this.terminalFailureOverride||(this.state==="failed"?this.stateDetail||ie.failed:"");if(this.setTransientPanelState(t?"failed":"ended",t?"当前暂时无法接通,请稍后再试。":"本次通话已结束。"),!this.legacyOutboundOnly){t?this.setState("failed",t):this.setState("ended"),this.setAvailabilityState("preparing","页面仍在线,正在自动回到待机。"),this.appendLog("通话已结束,网页电话正在自动回到待机。",{reason:e}),await this.startStandby({force:!0});return}t?this.setState("failed",t):this.setState("ended"),this.appendLog("通话已结束并完成清理",{reason:e})}finally{this.finalizing=!1}}}async handleStandbyTermination(e={}){this.stopTransportDisconnectGrace();const t=this.state,i=e?.hangupSource||"terminated";t==="incoming"?(this.incrementMissedIncomingCount(),i==="local"?(this.setState("standby","当前来电已拒接,widget 仍保持可用。"),this.setAvailabilityState("ready","当前来电已拒接,widget 仍保持可用。"),this.emitHostCallback("onCallRejected",this.pendingIncomingCall||e||null)):(this.setState("standby","当前来电未接听,widget 仍保持可用。"),this.setAvailabilityState("ready","当前来电未接听,widget 仍保持可用。"),this.emitHostCallback("onCallMissed",this.pendingIncomingCall||e||null)),this.setTransientPanelState("ended","本次来电已结束。")):(this.setState("standby","上一通来电已结束,继续等待下一通来电。"),this.setAvailabilityState("ready","当前可被呼叫,也可由访客主动发起呼叫。"),this.setTransientPanelState("ended","本次通话已结束。")),this.appendLog("待机会话中的来电已结束",{hangupSource:i,previousState:t}),this.pendingIncomingCall=null}async answerIncomingCall(){if(this.state==="incoming"){if(this.clearPanelAutoClose(),this.transientPanelState=null,this.transientPanelDetail="",this.e2eBridgeEnabled&&this.pendingIncomingCall?.e2eSimulated){this.setState("connected","测试桥已模拟接听当前来电。"),this.emitHostCallback("onCallAnswered",this.pendingIncomingCall);return}if(!this.client&&this.e2eBridgeEnabled){this.setState("connected","测试桥已模拟接听当前来电。"),this.emitHostCallback("onCallAnswered",this.pendingIncomingCall);return}if(!this.client)throw new Error("当前没有可接听的 SIP 会话");await this.client.answer()}}async declineIncomingCall(){if(this.state==="incoming"){if(this.e2eBridgeEnabled&&this.pendingIncomingCall?.e2eSimulated){this.incrementMissedIncomingCount(),this.setState("standby","测试桥已模拟拒接当前来电,网页电话已恢复待机。"),this.setTransientPanelState("ended","本次来电已结束。"),this.setAvailabilityState("ready","当前可被呼叫,也可由访客主动发起呼叫。"),this.emitHostCallback("onCallRejected",this.pendingIncomingCall),this.pendingIncomingCall=null;return}if(!this.client&&this.e2eBridgeEnabled){this.incrementMissedIncomingCount(),this.setState("standby","测试桥已模拟拒接当前来电,网页电话已恢复待机。"),this.setTransientPanelState("ended","本次来电已结束。"),this.setAvailabilityState("ready","当前可被呼叫,也可由访客主动发起呼叫。"),this.emitHostCallback("onCallRejected",this.pendingIncomingCall),this.pendingIncomingCall=null;return}if(!this.client)throw new Error("当前没有可拒接的 SIP 会话");await this.client.decline()}}async hangupCall(){if(this.e2eBridgeEnabled&&this.pendingIncomingCall?.e2eSimulated){this.legacyOutboundOnly?(this.setState("ended"),this.setTransientPanelState("ended","本次通话已结束。")):(this.setState("standby","测试桥已模拟挂断,网页电话已恢复待机。"),this.setTransientPanelState("ended","本次通话已结束。")),this.pendingIncomingCall=null;return}if(!this.client&&this.e2eBridgeEnabled){this.legacyOutboundOnly?(this.setState("ended"),this.setTransientPanelState("ended","本次通话已结束。")):(this.setState("standby","测试桥已模拟挂断,网页电话已恢复待机。"),this.setTransientPanelState("ended","本次通话已结束。")),this.pendingIncomingCall=null;return}this.client&&(this.expectTermination("hangup-call"),this.suppressExpectedClientTeardown("hangup-call"),await this.client.hangup())}async disconnectAndCleanup(e,t={}){const{preserveLifecycleRun:i=!1,suppressStateReset:s=!1,suppressClientTeardownState:r=null}=t;i||this.beginLifecycleRun(),r&&this.suppressExpectedClientTeardown(r),this.stopSessionTouch(),this.stopTransportDisconnectGrace(),this.reportIssuedSessionEvent("manual-disconnect",{reason:e},this.state),this.pendingIncomingCall=null,this.terminalFailureOverride="",this.client&&(await this.client.destroy(),this.client=null),await this.releaseIssuedSession("released",e),s||(this.pendingLifecycleMode=null,this.setState("idle"),this.setAvailabilityState("preparing","网页电话已断开,系统正在自动返回待机。"),this.appendLog("连接已断开",{reason:e}),!this.legacyOutboundOnly&&this.networkOnline!==!1&&this.scheduleAutoStandbyRecovery(e))}async loadBootstrap(){if(this.bootstrap)return this.bootstrap;const e=await fetch(X(this.apiBaseUrl,"api/sip/embedded-call-demo-bootstrap"),{credentials:"include",cache:"no-store"});if(!e.ok)throw new Error(`默认模式读取失败:${e.status}`);return this.bootstrap=await e.json(),this.bootstrap}async fetchTurnIceServers(){const e=await fetch(X(this.apiBaseUrl,"api/sip/webrtc-turn-credentials"),{credentials:"include",cache:"no-store"});if(!e.ok)return this.appendLog("TURN 凭证获取失败,继续尝试直连",{status:e.status}),[];const t=await e.json();return Array.isArray(t?.ice_servers)?t.ice_servers:[]}async fetchIssuedSession(e={}){const{preferredBrowserSipUsername:t=null}=e,i=le(this.options),s={page_url:window.location.href};A(s,"site_key",this.options.siteKey),A(s,"standby_state",this.options.standbyState),A(s,"selected_primary_account",this.options.selectedPrimaryAccount),A(s,"selected_middle_layer_account",this.options.selectedMiddleLayerAccount),A(s,"preferred_browser_sip_username",t),A(s,"display_name",this.options.displayName),A(s,"access_token",this.options.accessToken),i&&(s.business_key=i);const r=await fetch(X(this.apiBaseUrl,"api/sip/issue-call-session"),{method:"POST",credentials:"include",cache:"no-store",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok){const d=await r.json().catch(()=>({}));throw new Error(d?.detail||`会话签发失败:${r.status}`)}const n=await r.json(),o=xt(n,Ie.outbound,"issue-call-session");return this.markDebugPanelEnabled("issue-call-session"),this.issuedSession=n,this.reportIssuedSessionEvent("issued-session-fetched",{sessionId:n.session_id,targetExtension:n.target_extension,dialTargetExtension:n.dial_target_extension||n.target_extension,resolvedTargetInternalNumbers:n.resolved_target_internal_numbers||[],resolvedBusinessKey:n.resolved_business_key||null},"requesting"),this.appendLog("已获取服务端签发会话",{session_id:n.session_id,target_extension:n.target_extension,dial_target_extension:n.dial_target_extension||n.target_extension||null,resolved_target_internal_numbers:n.resolved_target_internal_numbers||[],resolved_business_key:n.resolved_business_key||null,resolved_route_mode:n.resolved_route_mode||null,route_note:n.route_note||null}),{sessionMode:o,sipUsername:n.sip_username,sipDomain:n.sip_domain,password:n.sip_password,transport:Tt({apiBaseUrl:this.apiBaseUrl,transportBaseUrl:this.options.transportBaseUrl||"",transportUrl:n.ws_server}),target:n.dial_target_extension||n.target_extension,routeNote:n.route_note||""}}async fetchStandbySession(){const e=le(this.options)||null,t={page_url:window.location.href};A(t,"site_key",this.options.siteKey),A(t,"standby_state",this.options.standbyState),A(t,"selected_primary_account",this.options.selectedPrimaryAccount),A(t,"selected_middle_layer_account",this.options.selectedMiddleLayerAccount),A(t,"display_name",this.options.displayName),A(t,"access_token",this.options.accessToken),e&&(t.business_key=e);const i=await fetch(X(this.apiBaseUrl,"api/sip/issue-widget-standby-session"),{method:"POST",credentials:"include",cache:"no-store",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!i.ok){const n=await i.json().catch(()=>({}));throw new Error(n?.detail||`待机会话签发失败:${i.status}`)}const s=await i.json(),r=xt(s,Ie.standby,"issue-widget-standby-session");return this.markDebugPanelEnabled("issue-widget-standby-session"),this.issuedSession=s,this.reportIssuedSessionEvent("standby-session-fetched",{sessionId:s.session_id,resolvedBusinessKey:s.resolved_business_key||null,standbyMode:s.standby_mode||null},"standbyRequesting"),this.appendLog("已获取 widget 待机会话",{session_id:s.session_id,resolved_business_key:s.resolved_business_key||null,standby_mode:s.standby_mode||null,standby_state:s.standby_state||null,widget_anchor_number:s.widget_anchor_number||null,browser_sip_username:s.browser_sip_username||s.sip_username||null,route_note:s.route_note||null}),{sessionMode:r,sipUsername:s.sip_username,sipDomain:s.sip_domain,password:s.sip_password,transport:Tt({apiBaseUrl:this.apiBaseUrl,transportBaseUrl:this.options.transportBaseUrl||"",transportUrl:s.ws_server}),target:null,routeNote:s.route_note||""}}async releaseIssuedSession(e,t){if(!this.issuedSession?.session_id)return;const i=this.issuedSession.session_id;this.markRecentlyReleasedSession(i,t),this.issuedSession=null,await fetch(X(this.apiBaseUrl,"api/sip/release-call-session"),{method:"POST",credentials:"include",cache:"no-store",headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:i,status:e,reason:t,client_summary:this.buildClientEvidenceSummary(e,{releaseReason:t})})}).catch(()=>{})}postLifecycleBeacon(e,t){const i=JSON.stringify(t),s=X(this.apiBaseUrl,e);if(typeof navigator<"u"&&typeof navigator.sendBeacon=="function")try{const r=new Blob([i],{type:"application/json"});if(navigator.sendBeacon(s,r))return!0}catch{}return fetch(s,{method:"POST",credentials:"include",cache:"no-store",keepalive:!0,headers:{"Content-Type":"application/json"},body:i}).catch(()=>{}),!0}flushUnloadCleanup(e){if(this.unloadCleanupStarted||(this.unloadCleanupStarted=!0,this.stopSessionTouch(),this.stopPresenceTouch(),this.stopTransportDisconnectGrace(),!this.issuedSession?.session_id))return;const t=this.issuedSession.session_id;this.markRecentlyReleasedSession(t,e),this.postLifecycleBeacon("api/sip/release-call-session",{session_id:t,status:"released",reason:e,client_summary:this.buildClientEvidenceSummary("released",{releaseReason:e})}),this.issuedSession=null}buildClientEvidenceSummary(e=null,t=null){const i={runtimeMode:"embedded-widget-runtime",widgetVersion:"2026-03-29-runtime-evidence",widgetMode:this.mode,sessionMode:this.issuedSession?.session_mode||null,state:e||this.state,visible:typeof document>"u"?!0:document.visibilityState==="visible",pageUrl:typeof window>"u"?null:window.location.href,connectionSnapshot:this.client?.getConnectionSnapshot?.()||null};return t&&typeof t=="object"&&(i.eventDetail=t),i}buildPresenceEvidenceSummary(e=null,t=null){return{...this.buildClientEvidenceSummary(e,t),presenceId:this.presenceId,availabilityState:this.availabilityState,availabilityDetail:this.availabilityDetail,networkOnline:this.networkOnline,pageVisible:this.pageVisible}}setAvailabilityState(e,t=""){this.availabilityState=e,this.availabilityDetail=t||this.availabilityDetail,this.refs.availability&&(this.refs.availability.textContent=t?`${de[e]||de.recovering} · ${t}`:de[e]||de.recovering,this.refs.availability.className=`availability availability--${e}`),this.render()}suppressExpectedClientTeardown(e,t=ki){this.expectedClientTeardown={reason:e,expiresAt:Date.now()+t}}expectTermination(e,t=1e4){this.expectedTermination={reason:e,expiresAt:Date.now()+t}}clearExpectedTermination(){this.expectedTermination=null}isExpectedTermination(){const e=this.expectedTermination;return e?e.expiresAt<=Date.now()?(this.expectedTermination=null,!1):!0:!1}isExpectedClientTeardown(){const e=this.expectedClientTeardown;return e?e.expiresAt<=Date.now()?(this.expectedClientTeardown=null,!1):!0:!1}clearAutoStandbyRecovery(){this.autoStandbyRecoveryTimer&&(window.clearTimeout(this.autoStandbyRecoveryTimer),this.autoStandbyRecoveryTimer=null)}scheduleAutoStandbyRecovery(e,t=Di){this.legacyOutboundOnly||this.networkOnline===!1||this.autoStandbyRecoveryTimer||this.client||this.issuedSession?.session_id||this.finalizing||this.pendingLifecycleMode==="outbound"||this.sessionRecoveryInFlight||this.connectivityRecoveryInFlight||["requesting","standbyRequesting","registering","calling","incoming","connected"].includes(this.state)||(this.autoStandbyRecoveryTimer=window.setTimeout(()=>{this.autoStandbyRecoveryTimer=null,!(this.legacyOutboundOnly||this.networkOnline===!1)&&(this.client||this.issuedSession?.session_id||this.finalizing||this.pendingLifecycleMode==="outbound"||(this.appendLog("检测到页面当前不在通话中,开始自动进入待机。",{reason:e}),this.startStandby({force:!0})))},t))}pruneRecentlyReleasedSessions(){const e=Date.now();for(const[t,i]of this.recentlyReleasedSessions.entries())(i?.expiresAt||0)<=e&&this.recentlyReleasedSessions.delete(t)}markRecentlyReleasedSession(e,t){e&&(this.pruneRecentlyReleasedSessions(),this.recentlyReleasedSessions.set(e,{reason:t,expiresAt:Date.now()+Pi}))}wasSessionRecentlyReleased(e){return e?(this.pruneRecentlyReleasedSessions(),this.recentlyReleasedSessions.has(e)):!1}syncAvailabilityState(){if(this.networkOnline===!1){this.setAvailabilityState("offline","当前网络连接已断开,正在等待恢复。");return}if(this.state==="incoming"){this.setAvailabilityState("working","已有新来电,请直接接听或拒接。");return}if(this.state==="connected"){this.setAvailabilityState("working","当前通话已接通。");return}if(this.state==="calling"){this.setAvailabilityState("working","正在等待对端振铃或接听。");return}if(this.pendingLifecycleMode==="outbound"&&["requesting","registering"].includes(this.state)){this.setAvailabilityState("working","正在建立呼叫链路。");return}if(["requesting","standbyRequesting","registering"].includes(this.state)){this.setAvailabilityState("preparing","页面仍在线,正在建立网页电话连接。");return}if(!this.legacyOutboundOnly&&!this.issuedSession?.session_id&&!this.pendingLifecycleMode&&["idle","ended","missed","rejected"].includes(this.state)){this.setAvailabilityState("preparing","页面仍在线,正在自动进入网页电话待机。"),this.scheduleAutoStandbyRecovery("missing-issued-session");return}if(!this.legacyOutboundOnly&&!this.issuedSession?.session_id&&!this.pendingLifecycleMode&&!["calling","connected","incoming"].includes(this.state)){this.setAvailabilityState("preparing","页面仍在线,正在重新建立网页电话待机。"),this.scheduleAutoStandbyRecovery("missing-issued-session");return}this.setAvailabilityState("ready","当前可被呼叫,也可由访客主动发起呼叫。")}stopPresenceTouch(){this.presenceTouchTimer&&(window.clearInterval(this.presenceTouchTimer),this.presenceTouchTimer=null)}async restartStandbyAfterConnectivity(e){if(!(this.legacyOutboundOnly||this.connectivityRecoveryInFlight)&&!["connected","incoming","calling","requesting","standbyRequesting","registering"].includes(this.state)){this.connectivityRecoveryInFlight=(async()=>{this.appendLog("检测到连通性已恢复,准备重建网页电话待机。",{trigger:e,state:this.state,hasSession:!!this.issuedSession?.session_id}),(this.client||this.issuedSession?.session_id)&&await this.disconnectAndCleanup(`connectivity-restored-${e}`),await this.startStandby({force:!0})})();try{await this.connectivityRecoveryInFlight}finally{this.connectivityRecoveryInFlight=null}}}async handleServerSessionAction(e,t={}){if(this.legacyOutboundOnly)return!1;const i=Ui(e,t);if(i.action!==Xe.restartStandby)return!1;const s=se(this.issuedSession?.session_mode),r=se(this.pendingLifecycleMode),n=s===Ie.outbound||r===Et.outbound,o=s===Ie.standby||r===Et.standby;if(n||!o)throw L("非待机协议上下文收到了非法的 restart_standby 指令。",{source:e,state:this.state,local_session_mode:s||null,local_lifecycle_mode:r||null,session_action:i.action,session_action_reason:i.reason,session_action_detail:i.detail,resolved_session_id:i.resolvedSessionId,resolved_widget_anchor_number:i.resolvedWidgetAnchorNumber,resolved_browser_sip_username:i.resolvedBrowserSipUsername});if(this.sessionRecoveryInFlight||this.connectivityRecoveryInFlight)return!1;this.sessionRecoveryInFlight=(async()=>{this.appendLog("服务端要求当前页面立即放弃旧锚点并重建待机。",{source:e,state:this.state,local_session_mode:s||null,local_lifecycle_mode:r||null,session_action_reason:i.reason,session_action_detail:i.detail,resolved_session_id:i.resolvedSessionId,resolved_widget_anchor_number:i.resolvedWidgetAnchorNumber,resolved_browser_sip_username:i.resolvedBrowserSipUsername}),this.setAvailabilityState("recovering",i.detail||"当前锚点已失效,正在自动切换到新的待机线路。"),(this.client||this.issuedSession?.session_id)&&await this.disconnectAndCleanup(`server-session-action-${i.reason||e}`,{preserveLifecycleRun:!0,suppressStateReset:!0}),await this.startStandby({force:!0})})();try{await this.sessionRecoveryInFlight}finally{this.sessionRecoveryInFlight=null}return!0}async handleProtocolViolation(e){const t=e?.detail&&typeof e.detail=="object"?e.detail:{},i={violation_code:e?.code||"widget_protocol_violation",violation_name:e?.name||"EmbeddedCallWidgetProtocolViolationError",violation_message:e?.message||String(e),...t};this.appendLog("协议错误:当前页面收到了非法的运行态指令,已拒绝执行并保留现有通话状态。",i),typeof console<"u"&&typeof console.error=="function"&&console.error("[embedded-call-widget] protocol violation",i),await this.reportIssuedSessionEvent("protocol-violation",i),!["calling","connected","incoming"].includes(this.state)&&(this.pendingLifecycleMode=null,this.clearAutoStandbyRecovery(),this.stopSessionTouch(),this.stopPresenceTouch(),this.terminalFailureOverride=i.violation_message,this.setState("failed",i.violation_message),this.setAvailabilityState("conflicted","检测到前后端协议不一致,已停止自动恢复,请尽快排查。"))}async touchWidgetPresence(e="interval"){if(this.legacyOutboundOnly)return;this.networkOnline=typeof navigator>"u"?!0:navigator.onLine!==!1,this.pageVisible=typeof document>"u"?!0:document.visibilityState==="visible";const t=this.availabilityState==="offline";try{const i=await fetch(X(this.apiBaseUrl,"api/sip/touch-widget-presence"),{method:"POST",credentials:"include",cache:"no-store",keepalive:!0,headers:{"Content-Type":"application/json"},body:JSON.stringify((()=>{const n={presence_id:this.presenceId,state:this.state,page_url:typeof window>"u"?void 0:window.location.href,visible:this.pageVisible,network_online:this.networkOnline,client_summary:this.buildPresenceEvidenceSummary(this.state,{trigger:e})};return A(n,"site_key",this.options.siteKey),A(n,"requested_business_key",le(this.options)),A(n,"resolved_business_key",this.issuedSession?.resolved_business_key),A(n,"display_name",this.options.displayName),A(n,"session_id",this.issuedSession?.session_id),A(n,"session_mode",this.issuedSession?.session_mode),A(n,"widget_anchor_number",this.issuedSession?.widget_anchor_number),A(n,"browser_sip_username",this.issuedSession?.browser_sip_username||this.issuedSession?.sip_username),A(n,"standby_mode",this.issuedSession?.standby_mode),A(n,"standby_state",this.issuedSession?.standby_state||this.options.standbyState),A(n,"selected_primary_account",this.options.selectedPrimaryAccount),A(n,"selected_middle_layer_account",this.options.selectedMiddleLayerAccount),A(n,"route_note",this.issuedSession?.route_note),n})())});if(!i.ok)throw new Error(`运行态心跳失败:${i.status}`);const s=await i.json().catch(()=>({}));this.presenceFailureCount=0,await this.handleServerSessionAction("touch-widget-presence",s)||this.syncAvailabilityState(),t&&await this.restartStandbyAfterConnectivity(`presence-${e}`)}catch(i){if(Oe(i)){await this.handleProtocolViolation(i),this.syncAvailabilityState();return}if(this.presenceFailureCount+=1,this.networkOnline===!1||Qe(i)||this.presenceFailureCount>=Ct){const r=this.networkOnline===!1?"当前网络连接已断开,正在等待恢复。":"当前无法连接通话服务,正在自动尝试恢复。";this.setAvailabilityState("offline",r)}else this.setAvailabilityState("recovering","正在确认后台连接状态。");this.presenceFailureCount<=Ct&&this.appendLog("widget 运行态心跳失败",{trigger:e,failure_count:this.presenceFailureCount,message:i?.message||String(i)})}}startPresenceTouch(){this.legacyOutboundOnly||(this.touchWidgetPresence("mount"),this.presenceTouchTimer||(this.presenceTouchTimer=window.setInterval(()=>{this.touchWidgetPresence("interval")},$i)))}async touchIssuedSession(e,t=null){if(!this.issuedSession?.session_id)return;const i=this.issuedSession.session_id;try{const s=await fetch(X(this.apiBaseUrl,"api/sip/touch-call-session"),{method:"POST",credentials:"include",cache:"no-store",keepalive:!0,headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:i,state:e,client_summary:this.buildClientEvidenceSummary(e,t)})});if(!s.ok){const n=new Error(`会话心跳失败:${s.status}`);throw n.status=s.status,n}const r=await s.json().catch(()=>({}));await this.handleServerSessionAction("touch-call-session",r)}catch(s){if(Oe(s)){await this.handleProtocolViolation(s);return}if(s?.status===404&&this.wasSessionRecentlyReleased(i))return;this.appendLog("网页通话会话心跳失败",{session_id:i,state:e,message:s?.message||String(s)}),s?.status===404&&this.handleMissingIssuedSession(i,e)}}async handleMissingIssuedSession(e,t){if(!(this.sessionRecoveryInFlight||this.issuedSession?.session_id!==e)){this.sessionRecoveryInFlight=(async()=>{this.stopSessionTouch(),this.appendLog("网页电话待机会话已失效,准备自动恢复。",{session_id:e,state:t}),this.setAvailabilityState("recovering","原待机会话已失效,正在自动恢复。"),this.issuedSession?.session_id===e&&(this.issuedSession=null),!this.legacyOutboundOnly&&["standby","registering","standbyRequesting","failed","idle","missed","rejected","ended"].includes(this.state)&&(this.setState("standbyRequesting","待机会话已失效,正在自动重新进入待机。"),await this.startStandby({force:!0}))})();try{await this.sessionRecoveryInFlight}finally{this.sessionRecoveryInFlight=null}}}async reportIssuedSessionEvent(e,t=null,i=null){if(!this.issuedSession?.session_id)return;const s=this.issuedSession.session_id,r=i||this.state;let n=null,o="";try{const d=await fetch(X(this.apiBaseUrl,"api/sip/report-call-session-event"),{method:"POST",credentials:"include",cache:"no-store",keepalive:!0,headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:s,source:"widget-runtime",event:e,state:r,detail:this.buildClientEvidenceSummary(r,t)})});if(!d.ok){n=d.status;const u=await d.json().catch(()=>({}));throw o=u?.detail?String(u.detail):"",new Error(o||`事件留证失败:${d.status}`)}}catch(d){await fetch(X(this.apiBaseUrl,"api/sip/report-call-session-event-delivery-failure"),{method:"POST",credentials:"include",cache:"no-store",keepalive:!0,headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:s,source:"widget-runtime",event:e,state:r,detail:this.buildClientEvidenceSummary(r,t),delivery_error:{status:typeof n=="number"?n:null,detail:o||"",message:d?.message||String(d)}})}).catch(()=>{}),this.appendLog("网页通话事件留证失败",{session_id:s,event:e,state:r,message:d?.message||String(d)})}}stopSessionTouch(){this.sessionTouchTimer&&(window.clearInterval(this.sessionTouchTimer),this.sessionTouchTimer=null)}syncSessionTouch(e){if(!(!!this.issuedSession?.session_id&&["connected","registered","dialing","ringing","answered","standby","incoming"].includes(e))){this.stopSessionTouch();return}this.touchIssuedSession(e),this.sessionTouchTimer||(this.sessionTouchTimer=window.setInterval(()=>{this.touchIssuedSession(this.state)},Ai))}setState(e,t=""){this.state=e,this.stateDetail=t||ie[e]||ie.idle,this.syncAvailabilityState(),this.render(),this.emitHostCallback("onStateChange",this.getSnapshot())}appendLog(e,t){if(!this.refs.logs)return;const i=document.createElement("div");for(i.className="log",i.textContent=qi(e,t),this.refs.logs.prepend(i);this.refs.logs.children.length>8;)this.refs.logs.lastElementChild?.remove()}render(){const e=this.getVisibleState(),t=e!=="idle",i=this.pendingIncomingCall?.displayName||this.pendingIncomingCall?.user||"管理员",s=["requesting","registering","calling","connected","incoming"].includes(this.state);let r="网页通话",n=this.options.text||"联系管理员",o=this.transientPanelDetail||"",d=null,u=null;e==="ringing"?(r="网页来电",n=i,o=o||"对方正在呼叫你,请选择接听或挂断。",d={text:"接听",className:"btn btn-primary",disabled:!1},u={text:"挂断",className:"btn btn-danger",disabled:!1}):e==="dialing"?(r="正在呼叫",o=o||"正在为你接通管理员,请稍候。",u={text:"挂断",className:"btn btn-danger",disabled:!1}):e==="connected"?(r="通话进行中",n=this.pendingIncomingCall?.displayName||n,o=o||"通话已接通,如需结束请点击挂断。",u={text:"挂断",className:"btn btn-danger",disabled:!1}):e==="ended"?(r="通话已结束",o=o||"本次通话已结束。"):e==="failed"&&(r="暂未接通",o=o||"当前暂时无法接通,请稍后再试。");const h=this.missedIncomingCount>0?String(this.missedIncomingCount):this.triggerHintDismissed?"":" ";this.refs.trigger.disabled=s,this.refs.triggerBadge.hidden=!h,this.refs.triggerBadge.textContent=h,this.refs.triggerBadge.className=this.missedIncomingCount>0?"trigger-badge":"trigger-badge trigger-badge--dot",this.refs.panelShell.className=t?"panel-shell panel-shell--visible":"panel-shell",this.refs.panel.setAttribute("aria-hidden",t?"false":"true"),this.refs.panelEyebrow.textContent=r,this.refs.panelTitle.textContent=n,this.refs.status.textContent=Ze[e]||Ze.idle,this.refs.status.className=e==="failed"?"status status--failed":"status",this.refs.panelDetail.textContent=o,this.refs.primary.hidden=!d,d?(this.refs.primary.textContent=d.text,this.refs.primary.className=d.className,this.refs.primary.disabled=d.disabled):this.refs.primary.textContent="",this.refs.secondary.hidden=!u,u?(this.refs.secondary.textContent=u.text,this.refs.secondary.className=u.className,this.refs.secondary.disabled=u.disabled):this.refs.secondary.textContent="";const m=+!!d+ +!!u;this.refs.actions.className=m<=1?"actions actions--single":"actions";const S=String(this.options.siteKey||"").trim(),x=String(this.bootstrap?.default_site_key||"").trim(),_=String(le(this.options)||"").trim(),C=String(this.bootstrap?.default_business_key||"").trim(),Q=S||(x?`${x}(服务端默认站点)`:"等待服务端解析"),p=this.issuedSession?.resolved_business_key||_||(C?`${C}(服务端默认规则)`:"等待服务端解析");this.legacyOutboundOnly?this.refs.meta.textContent=`入站规则:${p};站点键:${Q}`:this.refs.meta.textContent=`站点键:${Q};网页电话会先进入可被叫待机;访客主动呼叫默认规则:${p}`,this.refs.availability.textContent=this.availabilityDetail?`${de[this.availabilityState]||de.recovering} · ${this.availabilityDetail}`:de[this.availabilityState]||de.recovering,this.refs.availability.className=`availability availability--${this.availabilityState}`,this.availabilityState==="offline"?this.refs.hint.textContent=this.networkOnline===!1?"当前浏览器网络已断开。系统会在网络恢复后自动尝试重连网页电话。":"当前无法连接通话服务。系统会继续自动恢复,暂时不要重复点击。":this.availabilityState==="preparing"?this.refs.hint.textContent="当前正在准备待机能力。只要页面在线,系统会自动进入可呼入、可呼出的网页电话状态。":this.availabilityState==="working"?this.state==="incoming"?this.refs.hint.textContent="当前已有来电,直接接听或拒接即可;处理完成后会自动回到待机。":this.state==="connected"?this.refs.hint.textContent="当前通话已接通,挂断后会自动回到待机,不需要额外刷新页面。":this.refs.hint.textContent="当前正在切换到外呼链路,请等待振铃、接通或自动回到待机。":this.availabilityState==="recovering"?this.refs.hint.textContent="当前页面仍在线,系统正在自动恢复可被叫待机,不需要刷新页面或手动切换模式。":this.availabilityState==="conflicted"?this.refs.hint.textContent="检测到前后端协议不一致,当前已停止自动恢复。请先排查版本和协议口径,再继续使用。":this.refs.hint.textContent=this.isMobileViewport?Ri:"桌面网页可直接使用;若宿主站点启用了来源白名单或接入令牌,请按站点配置提供对应参数。",this.refs.debugPanel.hidden=!this.debugPanelEnabled}async destroy(){this.clearAutoStandbyRecovery(),this.clearPanelAutoClose(),await this.disconnectAndCleanup("destroy-widget",{suppressStateReset:!0}),typeof document<"u"&&this.visibilityHandler&&(document.removeEventListener("visibilitychange",this.visibilityHandler),this.visibilityHandler=null),this.stopSessionTouch(),this.stopPresenceTouch(),this.stopTransportDisconnectGrace(),this.pendingLifecycleMode=null,this.pendingIncomingCall=null,typeof window<"u"&&(this.onlineHandler&&(window.removeEventListener("online",this.onlineHandler),this.onlineHandler=null),this.offlineHandler&&(window.removeEventListener("offline",this.offlineHandler),this.offlineHandler=null),this.beforeUnloadHandler&&(window.removeEventListener("beforeunload",this.beforeUnloadHandler),this.beforeUnloadHandler=null),this.pageHideHandler&&(window.removeEventListener("pagehide",this.pageHideHandler),this.pageHideHandler=null)),this.e2eBridgeEnabled&&typeof window<"u"&&window.__embeddedCallWidgetE2E__&&delete window.__embeddedCallWidgetE2E__,this.shadowRoot.innerHTML=""}}function Rt(a){return new Wi(a)}return typeof window<"u"&&(window.__EmbeddedCallWidgetRuntime__={createEmbeddedCallWidgetRuntime:Rt}),Fe.createEmbeddedCallWidgetRuntime=Rt,Object.defineProperty(Fe,Symbol.toStringTag,{value:"Module"}),Fe})({});
321
+ `,this.refs.trigger=this.shadowRoot.querySelector('[data-role="trigger"]'),this.refs.triggerBadge=this.shadowRoot.querySelector('[data-role="trigger-badge"]'),this.refs.panelShell=this.shadowRoot.querySelector('[data-role="panel-shell"]'),this.refs.panel=this.shadowRoot.querySelector('[data-role="panel"]'),this.refs.panelEyebrow=this.shadowRoot.querySelector('[data-role="panel-eyebrow"]'),this.refs.panelTitle=this.shadowRoot.querySelector('[data-role="panel-title"]'),this.refs.panelDetail=this.shadowRoot.querySelector('[data-role="panel-detail"]'),this.refs.actions=this.shadowRoot.querySelector('[data-role="actions"]'),this.refs.status=this.shadowRoot.querySelector('[data-role="status"]'),this.refs.debugPanel=this.shadowRoot.querySelector('[data-role="debug-panel"]'),this.refs.availability=this.shadowRoot.querySelector('[data-role="availability"]'),this.refs.hint=this.shadowRoot.querySelector('[data-role="hint"]'),this.refs.meta=this.shadowRoot.querySelector('[data-role="meta"]'),this.refs.primary=this.shadowRoot.querySelector('[data-role="primary"]'),this.refs.secondary=this.shadowRoot.querySelector('[data-role="secondary"]'),this.refs.logs=this.shadowRoot.querySelector('[data-role="logs"]'),this.refs.audio=this.shadowRoot.querySelector('[data-role="audio"]'),this.refs.trigger.addEventListener("click",()=>{this.handleTriggerClick()}),this.refs.primary.addEventListener("click",()=>{this.handlePrimaryAction()}),this.refs.secondary.addEventListener("click",()=>{this.handleSecondaryAction()}),this.bindPageLifecycle(),this.registerE2EBridge(),this.startPresenceTouch(),this.render()}bindPageLifecycle(){typeof document>"u"||(this.visibilityHandler=()=>{if(this.pageVisible=document.visibilityState==="visible",document.visibilityState==="hidden"){this.reportIssuedSessionEvent("visibility-hidden",{visible:!1}),this.touchWidgetPresence("visibility-hidden"),this.issuedSession?.session_id&&this.touchIssuedSession(this.state,{trigger:"visibility-hidden"}),this.appendLog("页面已切到后台",{state:this.state,advice:"系统会继续自动保活;回到前台后请确认页面仍保持活动。"}),this.isMobileViewport&&["requesting","registering","calling","connected"].includes(this.state)&&(this.stateDetail="页面已切到后台;手机网页回到前台后请确认通话仍在继续。",this.render());return}if(this.isMobileViewport&&(this.appendLog("页面已回到前台",{state:this.state}),["requesting","registering","calling","connected"].includes(this.state)&&(this.stateDetail=ie[this.state]||ie.idle,this.render())),this.touchWidgetPresence("visibility-visible"),!this.legacyOutboundOnly&&["standby","registering","standbyRequesting","failed","idle","missed","rejected","ended"].includes(this.state)){const e=this.client?.getConnectionSnapshot?.()||null,t=!!(e?.connected&&(this.state!=="standby"||e?.registered));this.issuedSession?.session_id&&t?this.touchIssuedSession(this.state,{trigger:"visibility-visible"}):(this.appendLog("页面回到前台后检测到待机链路未处于健康状态,准备自动重建。",{state:this.state,has_session:!!this.issuedSession?.session_id,client_snapshot:e}),this.client||this.issuedSession?.session_id?this.disconnectAndCleanup("visibility-visible-unhealthy-client"):this.startStandby({force:!0}))}this.reportIssuedSessionEvent("visibility-visible",{visible:!0}),this.render()},document.addEventListener("visibilitychange",this.visibilityHandler),typeof window<"u"&&(this.onlineHandler=()=>{this.networkOnline=!0,this.setAvailabilityState("recovering","网络已恢复,正在重新连接网页电话。"),this.touchWidgetPresence("browser-online"),this.legacyOutboundOnly||this.restartStandbyAfterConnectivity("browser-online")},this.offlineHandler=()=>{this.networkOnline=!1,this.setAvailabilityState("offline","当前网络连接已断开,正在等待恢复。"),["incoming","connected"].includes(this.state)?this.render():this.setState("failed","当前网络连接已断开,正在等待恢复。")},window.addEventListener("online",this.onlineHandler),window.addEventListener("offline",this.offlineHandler),this.beforeUnloadHandler=()=>{this.flushUnloadCleanup("beforeunload")},this.pageHideHandler=e=>{e?.persisted||this.flushUnloadCleanup("pagehide")},window.addEventListener("beforeunload",this.beforeUnloadHandler),window.addEventListener("pagehide",this.pageHideHandler)))}open(){this.mountNode.scrollIntoView({block:"nearest",inline:"nearest"})}beginLifecycleRun(){return this.lifecycleRunId+=1,this.lifecycleRunId}assertLifecycleRunCurrent(e){if(e!==this.lifecycleRunId)throw Gi("网页电话已切换到新的连接流程,旧流程不再继续。")}emitHostCallback(e,t){const i=this.options?.[e];if(Bi(i))try{i(t)}catch(s){this.appendLog("宿主回调执行失败",{callback:e,message:s?.message||String(s)})}}getSnapshot(){const e=this.getVisibleState();return{mode:this.mode,state:this.state,stateDetail:this.stateDetail,visibleState:e,panelOpen:this.isPanelVisible(),sessionMode:this.issuedSession?.session_mode||null,sessionId:this.issuedSession?.session_id||null,siteKey:this.options.siteKey||this.bootstrap?.default_site_key||null,businessKey:this.issuedSession?.resolved_business_key||le(this.options,this.bootstrap)||null,standbyMode:this.issuedSession?.standby_mode||null,standbyState:this.issuedSession?.standby_state||this.options.standbyState||null,widgetSipNumber:this.issuedSession?.widget_anchor_number||null,browserSipUsername:this.issuedSession?.browser_sip_username||this.issuedSession?.sip_username||null,routeNote:this.issuedSession?.route_note||null,incomingAnchors:Array.isArray(this.issuedSession?.incoming_anchors)?this.issuedSession.incoming_anchors:[],hasClient:!!this.client,hasIssuedSession:!!this.issuedSession?.session_id,primaryText:this.refs.primary?.textContent?.trim()||"",secondaryText:this.refs.secondary?.textContent?.trim()||"",statusText:this.refs.status?.textContent?.trim()||"",availabilityState:this.availabilityState,availabilityLabel:this.refs.availability?.textContent?.trim()||"",triggerBadge:this.refs.triggerBadge?.textContent?.trim()||"",missedIncomingCount:this.missedIncomingCount,triggerHintDismissed:this.triggerHintDismissed,debugRequested:this.debugRequested,debugPanelEnabled:this.debugPanelEnabled,presenceId:this.presenceId,networkOnline:this.networkOnline,pendingIncomingCall:this.pendingIncomingCall}}getVisibleState(){return this.transientPanelState?this.transientPanelState:this.state==="incoming"?"ringing":["requesting","registering","calling"].includes(this.state)?"dialing":this.state==="connected"?"connected":"idle"}isPanelVisible(){return this.getVisibleState()!=="idle"}clearPanelAutoClose(){this.panelAutoCloseTimer&&(window.clearTimeout(this.panelAutoCloseTimer),this.panelAutoCloseTimer=null)}clearTransientPanelState(){this.clearPanelAutoClose(),this.transientPanelState=null,this.transientPanelDetail="",this.render()}setTransientPanelState(e,t="",i=Ii){this.clearPanelAutoClose(),this.transientPanelState=e,this.transientPanelDetail=t,this.render(),i>0&&(this.panelAutoCloseTimer=window.setTimeout(()=>{this.panelAutoCloseTimer=null,this.transientPanelState=null,this.transientPanelDetail="",this.render()},i))}markDebugPanelEnabled(e,t){!this.debugRequested||!this.hasAccessToken||!t||this.debugPanelEnabled||(this.debugPanelEnabled=!0,this.appendLog("调试模式门禁已成立,开始展示调试面板。",{source:e}),this.render())}dismissTriggerHint(){this.triggerHintDismissed||(this.triggerHintDismissed=!0,Fi(this.triggerHintStorageKey,!0))}clearMissedIncomingCount(){this.missedIncomingCount&&(this.missedIncomingCount=0)}incrementMissedIncomingCount(){this.missedIncomingCount+=1}async handleTriggerClick(){if(this.dismissTriggerHint(),this.clearMissedIncomingCount(),this.clearTransientPanelState(),this.open(),["requesting","registering","calling","connected","incoming"].includes(this.state)){this.render();return}await this.connectAndCall()}registerE2EBridge(){if(!this.e2eBridgeEnabled||typeof window>"u")return;const e=window.location.hostname||"example.invalid";window.__embeddedCallWidgetE2E__={getSnapshot:()=>this.getSnapshot(),simulateIncomingCall:(t={})=>{const i=t.host||e,s=t.user||"2001";return this.pendingIncomingCall={displayName:t.displayName||"测试来电",user:s,host:i,direction:"inbound",uri:t.uri||`sip:${s}@${i}`,e2eSimulated:!0},this.clearPanelAutoClose(),this.transientPanelState=null,this.transientPanelDetail="",this.setAvailabilityState("working","已有新来电,请直接接听或挂断。"),this.setState("incoming"),this.emitHostCallback("onIncomingCall",this.pendingIncomingCall),this.getSnapshot()},simulateOutboundDialing:(t={})=>(this.dismissTriggerHint(),this.clearMissedIncomingCount(),this.clearPanelAutoClose(),this.transientPanelState=null,this.transientPanelDetail="",this.pendingIncomingCall=t&&typeof t=="object"?{...t,direction:"outbound",e2eSimulated:!0}:null,this.setAvailabilityState("working","测试桥已模拟发起外呼。"),this.setState("calling",t?.message||"测试桥已模拟拨打管理员。"),this.getSnapshot()),simulateOutboundConnected:(t={})=>(this.clearPanelAutoClose(),this.transientPanelState=null,this.transientPanelDetail="",t&&typeof t=="object"&&(this.pendingIncomingCall={...this.pendingIncomingCall||{},...t,direction:"outbound",e2eSimulated:!0}),this.setAvailabilityState("working","测试桥已模拟外呼接通。"),this.setState("connected",t?.message||"测试桥已模拟通话接通。"),this.getSnapshot()),answerIncomingCall:async()=>(await this.answerIncomingCall(),this.getSnapshot()),declineIncomingCall:async()=>(await this.declineIncomingCall(),this.getSnapshot()),hangupCall:async()=>(await this.hangupCall(),this.getSnapshot()),destroyWidget:async()=>(await this.destroy(),null)}}async handlePrimaryAction(){if(this.state==="incoming"){await this.answerIncomingCall();return}["calling","connected"].includes(this.state)||await this.handleTriggerClick()}async handleSecondaryAction(){if(this.state==="incoming"){await this.declineIncomingCall();return}if(["calling","connected"].includes(this.state)&&this.client){await this.hangupCall();return}await this.disconnectAndCleanup("manual-disconnect")}async connectAndCall(){if(["requesting","registering","calling","connected"].includes(this.state))return;const e=this.beginLifecycleRun();try{this.terminalFailureOverride="",this.clearAutoStandbyRecovery(),this.pendingLifecycleMode="outbound",this.setAvailabilityState("working","正在建立呼叫链路。");const t=this.issuedSession?.session_mode==="widget_standby"&&(this.issuedSession?.browser_sip_username||this.issuedSession?.sip_username)||null;!this.legacyOutboundOnly&&this.issuedSession?.session_mode==="widget_standby"&&(this.appendLog("访客主动呼叫前,先释放当前待机会话并切换到呼叫链路。",{widget_anchor_number:this.issuedSession?.widget_anchor_number||null,resolved_business_key:this.issuedSession?.resolved_business_key||le(this.options)||null}),await this.disconnectAndCleanup("switch-to-outbound-call",{preserveLifecycleRun:!0,suppressStateReset:!0,suppressClientTeardownState:"switch-to-outbound-call"}),this.assertLifecycleRunCurrent(e)),this.setState("requesting"),await this.loadBootstrap(),this.assertLifecycleRunCurrent(e),this.turnIceServers=await this.fetchTurnIceServers(),this.assertLifecycleRunCurrent(e);const i=await this.fetchIssuedSession({preferredBrowserSipUsername:t});this.assertLifecycleRunCurrent(e),await this.ensureClient(i),this.assertLifecycleRunCurrent(e),this.setState("registering"),await this.client.ensureReady(),this.assertLifecycleRunCurrent(e),this.setState("calling"),this.syncAvailabilityState(),await this.client.call(i.target),this.assertLifecycleRunCurrent(e),this.appendLog("已发起呼叫",{target:i.target,route_note:i.routeNote||null})}catch(t){if(this.pendingLifecycleMode=null,_t(t)){this.appendLog("已忽略过期的网页电话呼叫流程",{message:t?.message||String(t)});return}if(Oe(t)){await this.handleProtocolViolation(t);return}const i=t?.message||String(t);if(this.terminalFailureOverride=i,this.setState("failed",i),this.setTransientPanelState("failed","当前暂时无法接通,请稍后再试。"),Qe(t)||this.networkOnline===!1?this.setAvailabilityState("offline",this.networkOnline===!1?"当前网络连接已断开,正在等待恢复。":"当前无法连接通话服务,正在自动尝试恢复。"):this.setAvailabilityState("recovering","呼叫失败,正在准备恢复网页电话待机。"),this.appendLog("连接并呼叫失败",{message:i}),await this.releaseIssuedSession("failed","connect-and-call-failed"),!this.legacyOutboundOnly){this.appendLog("呼叫失败后,网页电话将恢复待机。",{message:i}),await this.startStandby({force:!0});return}this.setState("failed",i)}}async startStandby(e={}){if(this.legacyOutboundOnly)throw new Error("当前 widget 仍处于 legacy outbound 兼容模式");const{force:t=!1}=e;if(!t&&["standbyRequesting","registering","incoming","connected"].includes(this.state))return;const i=this.beginLifecycleRun();try{this.clearAutoStandbyRecovery(),this.pendingLifecycleMode="standby",this.terminalFailureOverride="",this.setState("standbyRequesting"),this.setAvailabilityState("preparing","正在申请新的待机会话。"),await this.loadBootstrap(),this.assertLifecycleRunCurrent(i),this.turnIceServers=await this.fetchTurnIceServers(),this.assertLifecycleRunCurrent(i);const s=await this.fetchStandbySession();this.assertLifecycleRunCurrent(i),await this.ensureClient(s),this.assertLifecycleRunCurrent(i),this.setState("registering","待机会话已拿到,正在连接并执行 REGISTER。"),await this.client.ensureReady(),this.assertLifecycleRunCurrent(i),this.pendingLifecycleMode=null,this.setState("standby"),this.setAvailabilityState("ready","当前可被呼叫,也可由访客主动发起呼叫。"),this.appendLog("widget 已进入待机",{session_id:this.issuedSession?.session_id||null,route_business_key:this.issuedSession?.resolved_business_key||le(this.options)||null,standby_mode:this.issuedSession?.standby_mode||null,widget_anchor_number:this.issuedSession?.widget_anchor_number||null,browser_sip_username:this.issuedSession?.browser_sip_username||this.issuedSession?.sip_username||null}),this.emitHostCallback("onStandbyReady",this.getSnapshot())}catch(s){if(_t(s)){this.appendLog("已忽略过期的网页电话待机流程",{message:s?.message||String(s)});return}if(this.pendingLifecycleMode=null,Oe(s)){await this.handleProtocolViolation(s);return}const r=s?.message||String(s);this.terminalFailureOverride=r,this.setState("failed",r),Qe(s)||this.networkOnline===!1?this.setAvailabilityState("offline",this.networkOnline===!1?"当前网络连接已断开,正在等待恢复。":"当前无法连接通话服务,正在自动尝试恢复。"):this.setAvailabilityState("recovering","当前未能完成待机注册,系统会继续自动恢复。"),this.appendLog("进入待机失败",{message:r}),await this.releaseIssuedSession("failed","standby-start-failed"),this.setState("failed",r),!this.legacyOutboundOnly&&this.networkOnline!==!1&&this.scheduleAutoStandbyRecovery("standby-start-failed")}}async ensureClient(e){const t={sip_username:e.sipUsername,sip_password:e.password,sip_domain:e.sipDomain,webrtc_url:e.transport,ice_servers:this.turnIceServers};this.client?.matchesAccount(t)||(this.client&&await this.client.destroy(),this.client=new Si(t,{remoteAudioElement:this.refs.audio,onStateChange:(i,s)=>{this.handleClientState(i,s)},onError:i=>this.appendLog("SIP 客户端错误",{message:i?.message||String(i)})}))}async handleClientState(e,t={}){if(this.syncSessionTouch(e),this.reportIssuedSessionEvent(e,t,e),(e==="connected"||e==="registered"||e==="ringing"||e==="dialing"||e==="answered")&&await this.clearTransportDisconnectGrace(!0),e==="registered"){this.pendingLifecycleMode=null,this.issuedSession?.session_mode==="widget_standby"?(this.setState("standby"),this.setAvailabilityState("ready","当前可被呼叫,也可由访客主动发起呼叫。")):this.setState("registering","REGISTER 已完成,正在准备发起呼叫。");return}if(e==="incoming"){this.pendingLifecycleMode=null,this.pendingIncomingCall=t&&typeof t=="object"?{...t}:null,this.clearPanelAutoClose(),this.transientPanelState=null,this.transientPanelDetail="",this.setState("incoming"),this.setAvailabilityState("working","已有新来电,请直接接听或拒接。"),this.appendLog("收到新的来电",this.pendingIncomingCall),this.emitHostCallback("onIncomingCall",this.pendingIncomingCall);return}if(e==="ringing"||e==="dialing"){this.setState("calling",t?.message||ie.calling);return}if(e==="answered"){this.pendingLifecycleMode=null,this.terminalFailureOverride="";const i=this.pendingIncomingCall||t||null;this.clearPanelAutoClose(),this.transientPanelState=null,this.transientPanelDetail="",this.setState("connected"),this.setAvailabilityState("working","当前通话已接通。"),this.emitHostCallback("onCallAnswered",i);return}if(e==="terminated"){if(this.issuedSession?.session_mode==="widget_standby"){await this.handleStandbyTermination(t);return}t&&typeof t=="object"&&(t.source==="invite-response"||t.source==="invite-exception"||t.code||t.status||t.stage)&&(this.terminalFailureOverride=t?.message||ie.failed,this.setState("failed",this.terminalFailureOverride)),await this.finalizeAfterTermination(t?.hangupSource||"terminated");return}if(e==="failed"){if(this.isExpectedClientTeardown())return;this.pendingLifecycleMode=null,await this.clearTransportDisconnectGrace(!1),this.stopSessionTouch(),this.terminalFailureOverride=t?.message||ie.failed,this.setState("failed",this.terminalFailureOverride),this.setTransientPanelState("failed","当前暂时无法接通,请稍后再试。"),this.setAvailabilityState("recovering","通话链路异常断开,系统会继续自动恢复。"),await this.releaseIssuedSession("failed","sip-client-failed");return}if(e==="disconnected"||e==="unregistered"){if(this.isExpectedClientTeardown()||this.isExpectedTermination()||!this.issuedSession?.session_id&&this.pendingLifecycleMode==="standby")return;if(this.stopSessionTouch(),["calling","connected"].includes(this.state)){this.setState("connected","SIP 信令短暂中断,正在等待恢复。"),this.setAvailabilityState("recovering","通话信令短暂中断,正在等待恢复。"),this.scheduleTransportDisconnectGrace(e);return}const s=this.terminalFailureOverride||this.client?.lastFailureMeta?.message||null;if(s){this.terminalFailureOverride=s,this.setState("failed",s),this.setTransientPanelState("failed","当前暂时无法接通,请稍后再试。");return}if(this.state==="failed")return;this.state!=="idle"&&this.state!=="ended"&&(this.pendingLifecycleMode=null,this.setState("ended"),this.setAvailabilityState("preparing","通话已结束,正在自动返回网页电话待机。"),this.scheduleAutoStandbyRecovery(`client-${e}`))}}stopTransportDisconnectGrace(){this.transportDisconnectTimer&&(window.clearTimeout(this.transportDisconnectTimer),this.transportDisconnectTimer=null),this.transportDisconnectContext=null}sameTransportDisconnectContext(e){return!e||!this.issuedSession?.session_id?!1:e.sessionId===this.issuedSession.session_id}async clearTransportDisconnectGrace(e){const t=this.transportDisconnectContext,i=!!(e&&t?.reported&&this.sameTransportDisconnectContext(t));this.stopTransportDisconnectGrace(),i&&(await this.reportIssuedSessionEvent("transport-restored",{disconnectDurationMs:Date.now()-(t.startedAt||Date.now()),graceMs:Re},this.state),this.appendLog("网页通话信令已恢复",{disconnectDurationMs:Date.now()-(t.startedAt||Date.now()),graceMs:Re}))}scheduleTransportDisconnectGrace(e){if(!this.issuedSession?.session_id)return;this.stopTransportDisconnectGrace();const t={sessionId:this.issuedSession.session_id,startedAt:Date.now(),reported:!1,state:e};this.transportDisconnectContext=t,this.transportDisconnectTimer=window.setTimeout(()=>{this.transportDisconnectTimer=null,this.sameTransportDisconnectContext(t)&&(t.reported=!0,this.reportIssuedSessionEvent("transport-disconnected",{state:e,graceMs:Re},this.state),this.appendLog("网页通话信令断开超过宽限窗口",{graceMs:Re,state:e}))},Re)}async finalizeAfterTermination(e){if(!this.finalizing){this.beginLifecycleRun(),this.finalizing=!0;try{this.pendingLifecycleMode=null,this.stopSessionTouch(),this.stopTransportDisconnectGrace(),await this.releaseIssuedSession("released",e),this.client&&(await this.client.destroy(),this.client=null);const t=this.terminalFailureOverride||(this.state==="failed"?this.stateDetail||ie.failed:"");if(this.setTransientPanelState(t?"failed":"ended",t?"当前暂时无法接通,请稍后再试。":"本次通话已结束。"),!this.legacyOutboundOnly){t?this.setState("failed",t):this.setState("ended"),this.setAvailabilityState("preparing","页面仍在线,正在自动回到待机。"),this.appendLog("通话已结束,网页电话正在自动回到待机。",{reason:e}),await this.startStandby({force:!0});return}t?this.setState("failed",t):this.setState("ended"),this.appendLog("通话已结束并完成清理",{reason:e})}finally{this.finalizing=!1}}}async handleStandbyTermination(e={}){this.stopTransportDisconnectGrace();const t=this.state,i=e?.hangupSource||"terminated";t==="incoming"?(this.incrementMissedIncomingCount(),i==="local"?(this.setState("standby","当前来电已拒接,widget 仍保持可用。"),this.setAvailabilityState("ready","当前来电已拒接,widget 仍保持可用。"),this.emitHostCallback("onCallRejected",this.pendingIncomingCall||e||null)):(this.setState("standby","当前来电未接听,widget 仍保持可用。"),this.setAvailabilityState("ready","当前来电未接听,widget 仍保持可用。"),this.emitHostCallback("onCallMissed",this.pendingIncomingCall||e||null)),this.setTransientPanelState("ended","本次来电已结束。")):(this.setState("standby","上一通来电已结束,继续等待下一通来电。"),this.setAvailabilityState("ready","当前可被呼叫,也可由访客主动发起呼叫。"),this.setTransientPanelState("ended","本次通话已结束。")),this.appendLog("待机会话中的来电已结束",{hangupSource:i,previousState:t}),this.pendingIncomingCall=null}async answerIncomingCall(){if(this.state==="incoming"){if(this.clearPanelAutoClose(),this.transientPanelState=null,this.transientPanelDetail="",this.e2eBridgeEnabled&&this.pendingIncomingCall?.e2eSimulated){this.setState("connected","测试桥已模拟接听当前来电。"),this.emitHostCallback("onCallAnswered",this.pendingIncomingCall);return}if(!this.client&&this.e2eBridgeEnabled){this.setState("connected","测试桥已模拟接听当前来电。"),this.emitHostCallback("onCallAnswered",this.pendingIncomingCall);return}if(!this.client)throw new Error("当前没有可接听的 SIP 会话");await this.client.answer()}}async declineIncomingCall(){if(this.state==="incoming"){if(this.e2eBridgeEnabled&&this.pendingIncomingCall?.e2eSimulated){this.incrementMissedIncomingCount(),this.setState("standby","测试桥已模拟拒接当前来电,网页电话已恢复待机。"),this.setTransientPanelState("ended","本次来电已结束。"),this.setAvailabilityState("ready","当前可被呼叫,也可由访客主动发起呼叫。"),this.emitHostCallback("onCallRejected",this.pendingIncomingCall),this.pendingIncomingCall=null;return}if(!this.client&&this.e2eBridgeEnabled){this.incrementMissedIncomingCount(),this.setState("standby","测试桥已模拟拒接当前来电,网页电话已恢复待机。"),this.setTransientPanelState("ended","本次来电已结束。"),this.setAvailabilityState("ready","当前可被呼叫,也可由访客主动发起呼叫。"),this.emitHostCallback("onCallRejected",this.pendingIncomingCall),this.pendingIncomingCall=null;return}if(!this.client)throw new Error("当前没有可拒接的 SIP 会话");await this.client.decline()}}async hangupCall(){if(this.e2eBridgeEnabled&&this.pendingIncomingCall?.e2eSimulated){this.legacyOutboundOnly?(this.setState("ended"),this.setTransientPanelState("ended","本次通话已结束。")):(this.setState("standby","测试桥已模拟挂断,网页电话已恢复待机。"),this.setTransientPanelState("ended","本次通话已结束。")),this.pendingIncomingCall=null;return}if(!this.client&&this.e2eBridgeEnabled){this.legacyOutboundOnly?(this.setState("ended"),this.setTransientPanelState("ended","本次通话已结束。")):(this.setState("standby","测试桥已模拟挂断,网页电话已恢复待机。"),this.setTransientPanelState("ended","本次通话已结束。")),this.pendingIncomingCall=null;return}this.client&&(this.expectTermination("hangup-call"),this.suppressExpectedClientTeardown("hangup-call"),await this.client.hangup())}async disconnectAndCleanup(e,t={}){const{preserveLifecycleRun:i=!1,suppressStateReset:s=!1,suppressClientTeardownState:r=null}=t;i||this.beginLifecycleRun(),r&&this.suppressExpectedClientTeardown(r),this.stopSessionTouch(),this.stopTransportDisconnectGrace(),this.reportIssuedSessionEvent("manual-disconnect",{reason:e},this.state),this.pendingIncomingCall=null,this.terminalFailureOverride="",this.client&&(await this.client.destroy(),this.client=null),await this.releaseIssuedSession("released",e),s||(this.pendingLifecycleMode=null,this.setState("idle"),this.setAvailabilityState("preparing","网页电话已断开,系统正在自动返回待机。"),this.appendLog("连接已断开",{reason:e}),!this.legacyOutboundOnly&&this.networkOnline!==!1&&this.scheduleAutoStandbyRecovery(e))}async loadBootstrap(){if(this.bootstrap)return this.bootstrap;const e=await fetch(X(this.apiBaseUrl,"api/sip/embedded-call-demo-bootstrap"),{credentials:"include",cache:"no-store"});if(!e.ok)throw new Error(`默认模式读取失败:${e.status}`);return this.bootstrap=await e.json(),this.bootstrap}async fetchTurnIceServers(){const e=await fetch(X(this.apiBaseUrl,"api/sip/webrtc-turn-credentials"),{credentials:"include",cache:"no-store"});if(!e.ok)return this.appendLog("TURN 凭证获取失败,继续尝试直连",{status:e.status}),[];const t=await e.json();return Array.isArray(t?.ice_servers)?t.ice_servers:[]}async fetchIssuedSession(e={}){const{preferredBrowserSipUsername:t=null}=e,i=le(this.options),s={page_url:window.location.href};A(s,"site_key",this.options.siteKey),A(s,"standby_state",this.options.standbyState),A(s,"selected_primary_account",this.options.selectedPrimaryAccount),A(s,"selected_middle_layer_account",this.options.selectedMiddleLayerAccount),A(s,"preferred_browser_sip_username",t),A(s,"display_name",this.options.displayName),A(s,"access_token",this.options.accessToken),i&&(s.business_key=i);const r=await fetch(X(this.apiBaseUrl,"api/sip/issue-call-session"),{method:"POST",credentials:"include",cache:"no-store",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok){const d=await r.json().catch(()=>({}));throw new Error(d?.detail||`会话签发失败:${r.status}`)}const n=await r.json(),o=xt(n,Ie.outbound,"issue-call-session");return this.markDebugPanelEnabled("issue-call-session",n?.debug_access_granted===!0),this.issuedSession=n,this.reportIssuedSessionEvent("issued-session-fetched",{sessionId:n.session_id,targetExtension:n.target_extension,dialTargetExtension:n.dial_target_extension||n.target_extension,resolvedTargetInternalNumbers:n.resolved_target_internal_numbers||[],resolvedBusinessKey:n.resolved_business_key||null},"requesting"),this.appendLog("已获取服务端签发会话",{session_id:n.session_id,target_extension:n.target_extension,dial_target_extension:n.dial_target_extension||n.target_extension||null,resolved_target_internal_numbers:n.resolved_target_internal_numbers||[],resolved_business_key:n.resolved_business_key||null,resolved_route_mode:n.resolved_route_mode||null,route_note:n.route_note||null}),{sessionMode:o,sipUsername:n.sip_username,sipDomain:n.sip_domain,password:n.sip_password,transport:Tt({apiBaseUrl:this.apiBaseUrl,transportBaseUrl:this.options.transportBaseUrl||"",transportUrl:n.ws_server}),target:n.dial_target_extension||n.target_extension,routeNote:n.route_note||""}}async fetchStandbySession(){const e=le(this.options)||null,t={page_url:window.location.href};A(t,"site_key",this.options.siteKey),A(t,"standby_state",this.options.standbyState),A(t,"selected_primary_account",this.options.selectedPrimaryAccount),A(t,"selected_middle_layer_account",this.options.selectedMiddleLayerAccount),A(t,"display_name",this.options.displayName),A(t,"access_token",this.options.accessToken),e&&(t.business_key=e);const i=await fetch(X(this.apiBaseUrl,"api/sip/issue-widget-standby-session"),{method:"POST",credentials:"include",cache:"no-store",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!i.ok){const n=await i.json().catch(()=>({}));throw new Error(n?.detail||`待机会话签发失败:${i.status}`)}const s=await i.json(),r=xt(s,Ie.standby,"issue-widget-standby-session");return this.markDebugPanelEnabled("issue-widget-standby-session",s?.debug_access_granted===!0),this.issuedSession=s,this.reportIssuedSessionEvent("standby-session-fetched",{sessionId:s.session_id,resolvedBusinessKey:s.resolved_business_key||null,standbyMode:s.standby_mode||null},"standbyRequesting"),this.appendLog("已获取 widget 待机会话",{session_id:s.session_id,resolved_business_key:s.resolved_business_key||null,standby_mode:s.standby_mode||null,standby_state:s.standby_state||null,widget_anchor_number:s.widget_anchor_number||null,browser_sip_username:s.browser_sip_username||s.sip_username||null,route_note:s.route_note||null}),{sessionMode:r,sipUsername:s.sip_username,sipDomain:s.sip_domain,password:s.sip_password,transport:Tt({apiBaseUrl:this.apiBaseUrl,transportBaseUrl:this.options.transportBaseUrl||"",transportUrl:s.ws_server}),target:null,routeNote:s.route_note||""}}async releaseIssuedSession(e,t){if(!this.issuedSession?.session_id)return;const i=this.issuedSession.session_id;this.markRecentlyReleasedSession(i,t),this.issuedSession=null,await fetch(X(this.apiBaseUrl,"api/sip/release-call-session"),{method:"POST",credentials:"include",cache:"no-store",headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:i,status:e,reason:t,client_summary:this.buildClientEvidenceSummary(e,{releaseReason:t})})}).catch(()=>{})}postLifecycleBeacon(e,t){const i=JSON.stringify(t),s=X(this.apiBaseUrl,e);if(typeof navigator<"u"&&typeof navigator.sendBeacon=="function")try{const r=new Blob([i],{type:"application/json"});if(navigator.sendBeacon(s,r))return!0}catch{}return fetch(s,{method:"POST",credentials:"include",cache:"no-store",keepalive:!0,headers:{"Content-Type":"application/json"},body:i}).catch(()=>{}),!0}flushUnloadCleanup(e){if(this.unloadCleanupStarted||(this.unloadCleanupStarted=!0,this.stopSessionTouch(),this.stopPresenceTouch(),this.stopTransportDisconnectGrace(),!this.issuedSession?.session_id))return;const t=this.issuedSession.session_id;this.markRecentlyReleasedSession(t,e),this.postLifecycleBeacon("api/sip/release-call-session",{session_id:t,status:"released",reason:e,client_summary:this.buildClientEvidenceSummary("released",{releaseReason:e})}),this.issuedSession=null}buildClientEvidenceSummary(e=null,t=null){const i={runtimeMode:"embedded-widget-runtime",widgetVersion:"2026-03-29-runtime-evidence",widgetMode:this.mode,sessionMode:this.issuedSession?.session_mode||null,state:e||this.state,visible:typeof document>"u"?!0:document.visibilityState==="visible",pageUrl:typeof window>"u"?null:window.location.href,connectionSnapshot:this.client?.getConnectionSnapshot?.()||null};return t&&typeof t=="object"&&(i.eventDetail=t),i}buildPresenceEvidenceSummary(e=null,t=null){return{...this.buildClientEvidenceSummary(e,t),presenceId:this.presenceId,availabilityState:this.availabilityState,availabilityDetail:this.availabilityDetail,networkOnline:this.networkOnline,pageVisible:this.pageVisible}}setAvailabilityState(e,t=""){this.availabilityState=e,this.availabilityDetail=t||this.availabilityDetail,this.refs.availability&&(this.refs.availability.textContent=t?`${de[e]||de.recovering} · ${t}`:de[e]||de.recovering,this.refs.availability.className=`availability availability--${e}`),this.render()}suppressExpectedClientTeardown(e,t=ki){this.expectedClientTeardown={reason:e,expiresAt:Date.now()+t}}expectTermination(e,t=1e4){this.expectedTermination={reason:e,expiresAt:Date.now()+t}}clearExpectedTermination(){this.expectedTermination=null}isExpectedTermination(){const e=this.expectedTermination;return e?e.expiresAt<=Date.now()?(this.expectedTermination=null,!1):!0:!1}isExpectedClientTeardown(){const e=this.expectedClientTeardown;return e?e.expiresAt<=Date.now()?(this.expectedClientTeardown=null,!1):!0:!1}clearAutoStandbyRecovery(){this.autoStandbyRecoveryTimer&&(window.clearTimeout(this.autoStandbyRecoveryTimer),this.autoStandbyRecoveryTimer=null)}scheduleAutoStandbyRecovery(e,t=Di){this.legacyOutboundOnly||this.networkOnline===!1||this.autoStandbyRecoveryTimer||this.client||this.issuedSession?.session_id||this.finalizing||this.pendingLifecycleMode==="outbound"||this.sessionRecoveryInFlight||this.connectivityRecoveryInFlight||["requesting","standbyRequesting","registering","calling","incoming","connected"].includes(this.state)||(this.autoStandbyRecoveryTimer=window.setTimeout(()=>{this.autoStandbyRecoveryTimer=null,!(this.legacyOutboundOnly||this.networkOnline===!1)&&(this.client||this.issuedSession?.session_id||this.finalizing||this.pendingLifecycleMode==="outbound"||(this.appendLog("检测到页面当前不在通话中,开始自动进入待机。",{reason:e}),this.startStandby({force:!0})))},t))}pruneRecentlyReleasedSessions(){const e=Date.now();for(const[t,i]of this.recentlyReleasedSessions.entries())(i?.expiresAt||0)<=e&&this.recentlyReleasedSessions.delete(t)}markRecentlyReleasedSession(e,t){e&&(this.pruneRecentlyReleasedSessions(),this.recentlyReleasedSessions.set(e,{reason:t,expiresAt:Date.now()+Pi}))}wasSessionRecentlyReleased(e){return e?(this.pruneRecentlyReleasedSessions(),this.recentlyReleasedSessions.has(e)):!1}syncAvailabilityState(){if(this.networkOnline===!1){this.setAvailabilityState("offline","当前网络连接已断开,正在等待恢复。");return}if(this.state==="incoming"){this.setAvailabilityState("working","已有新来电,请直接接听或拒接。");return}if(this.state==="connected"){this.setAvailabilityState("working","当前通话已接通。");return}if(this.state==="calling"){this.setAvailabilityState("working","正在等待对端振铃或接听。");return}if(this.pendingLifecycleMode==="outbound"&&["requesting","registering"].includes(this.state)){this.setAvailabilityState("working","正在建立呼叫链路。");return}if(["requesting","standbyRequesting","registering"].includes(this.state)){this.setAvailabilityState("preparing","页面仍在线,正在建立网页电话连接。");return}if(!this.legacyOutboundOnly&&!this.issuedSession?.session_id&&!this.pendingLifecycleMode&&["idle","ended","missed","rejected"].includes(this.state)){this.setAvailabilityState("preparing","页面仍在线,正在自动进入网页电话待机。"),this.scheduleAutoStandbyRecovery("missing-issued-session");return}if(!this.legacyOutboundOnly&&!this.issuedSession?.session_id&&!this.pendingLifecycleMode&&!["calling","connected","incoming"].includes(this.state)){this.setAvailabilityState("preparing","页面仍在线,正在重新建立网页电话待机。"),this.scheduleAutoStandbyRecovery("missing-issued-session");return}this.setAvailabilityState("ready","当前可被呼叫,也可由访客主动发起呼叫。")}stopPresenceTouch(){this.presenceTouchTimer&&(window.clearInterval(this.presenceTouchTimer),this.presenceTouchTimer=null)}async restartStandbyAfterConnectivity(e){if(!(this.legacyOutboundOnly||this.connectivityRecoveryInFlight)&&!["connected","incoming","calling","requesting","standbyRequesting","registering"].includes(this.state)){this.connectivityRecoveryInFlight=(async()=>{this.appendLog("检测到连通性已恢复,准备重建网页电话待机。",{trigger:e,state:this.state,hasSession:!!this.issuedSession?.session_id}),(this.client||this.issuedSession?.session_id)&&await this.disconnectAndCleanup(`connectivity-restored-${e}`),await this.startStandby({force:!0})})();try{await this.connectivityRecoveryInFlight}finally{this.connectivityRecoveryInFlight=null}}}async handleServerSessionAction(e,t={}){if(this.legacyOutboundOnly)return!1;const i=Ui(e,t);if(i.action!==Xe.restartStandby)return!1;const s=se(this.issuedSession?.session_mode),r=se(this.pendingLifecycleMode),n=s===Ie.outbound||r===Et.outbound,o=s===Ie.standby||r===Et.standby;if(n||!o)throw L("非待机协议上下文收到了非法的 restart_standby 指令。",{source:e,state:this.state,local_session_mode:s||null,local_lifecycle_mode:r||null,session_action:i.action,session_action_reason:i.reason,session_action_detail:i.detail,resolved_session_id:i.resolvedSessionId,resolved_widget_anchor_number:i.resolvedWidgetAnchorNumber,resolved_browser_sip_username:i.resolvedBrowserSipUsername});if(this.sessionRecoveryInFlight||this.connectivityRecoveryInFlight)return!1;this.sessionRecoveryInFlight=(async()=>{this.appendLog("服务端要求当前页面立即放弃旧锚点并重建待机。",{source:e,state:this.state,local_session_mode:s||null,local_lifecycle_mode:r||null,session_action_reason:i.reason,session_action_detail:i.detail,resolved_session_id:i.resolvedSessionId,resolved_widget_anchor_number:i.resolvedWidgetAnchorNumber,resolved_browser_sip_username:i.resolvedBrowserSipUsername}),this.setAvailabilityState("recovering",i.detail||"当前锚点已失效,正在自动切换到新的待机线路。"),(this.client||this.issuedSession?.session_id)&&await this.disconnectAndCleanup(`server-session-action-${i.reason||e}`,{preserveLifecycleRun:!0,suppressStateReset:!0}),await this.startStandby({force:!0})})();try{await this.sessionRecoveryInFlight}finally{this.sessionRecoveryInFlight=null}return!0}async handleProtocolViolation(e){const t=e?.detail&&typeof e.detail=="object"?e.detail:{},i={violation_code:e?.code||"widget_protocol_violation",violation_name:e?.name||"EmbeddedCallWidgetProtocolViolationError",violation_message:e?.message||String(e),...t};this.appendLog("协议错误:当前页面收到了非法的运行态指令,已拒绝执行并保留现有通话状态。",i),typeof console<"u"&&typeof console.error=="function"&&console.error("[embedded-call-widget] protocol violation",i),await this.reportIssuedSessionEvent("protocol-violation",i),!["calling","connected","incoming"].includes(this.state)&&(this.pendingLifecycleMode=null,this.clearAutoStandbyRecovery(),this.stopSessionTouch(),this.stopPresenceTouch(),this.terminalFailureOverride=i.violation_message,this.setState("failed",i.violation_message),this.setAvailabilityState("conflicted","检测到前后端协议不一致,已停止自动恢复,请尽快排查。"))}async touchWidgetPresence(e="interval"){if(this.legacyOutboundOnly)return;this.networkOnline=typeof navigator>"u"?!0:navigator.onLine!==!1,this.pageVisible=typeof document>"u"?!0:document.visibilityState==="visible";const t=this.availabilityState==="offline";try{const i=await fetch(X(this.apiBaseUrl,"api/sip/touch-widget-presence"),{method:"POST",credentials:"include",cache:"no-store",keepalive:!0,headers:{"Content-Type":"application/json"},body:JSON.stringify((()=>{const n={presence_id:this.presenceId,state:this.state,page_url:typeof window>"u"?void 0:window.location.href,visible:this.pageVisible,network_online:this.networkOnline,client_summary:this.buildPresenceEvidenceSummary(this.state,{trigger:e})};return A(n,"site_key",this.options.siteKey),A(n,"requested_business_key",le(this.options)),A(n,"resolved_business_key",this.issuedSession?.resolved_business_key),A(n,"display_name",this.options.displayName),A(n,"session_id",this.issuedSession?.session_id),A(n,"session_mode",this.issuedSession?.session_mode),A(n,"widget_anchor_number",this.issuedSession?.widget_anchor_number),A(n,"browser_sip_username",this.issuedSession?.browser_sip_username||this.issuedSession?.sip_username),A(n,"standby_mode",this.issuedSession?.standby_mode),A(n,"standby_state",this.issuedSession?.standby_state||this.options.standbyState),A(n,"selected_primary_account",this.options.selectedPrimaryAccount),A(n,"selected_middle_layer_account",this.options.selectedMiddleLayerAccount),A(n,"route_note",this.issuedSession?.route_note),n})())});if(!i.ok)throw new Error(`运行态心跳失败:${i.status}`);const s=await i.json().catch(()=>({}));this.presenceFailureCount=0,await this.handleServerSessionAction("touch-widget-presence",s)||this.syncAvailabilityState(),t&&await this.restartStandbyAfterConnectivity(`presence-${e}`)}catch(i){if(Oe(i)){await this.handleProtocolViolation(i),this.syncAvailabilityState();return}if(this.presenceFailureCount+=1,this.networkOnline===!1||Qe(i)||this.presenceFailureCount>=Ct){const r=this.networkOnline===!1?"当前网络连接已断开,正在等待恢复。":"当前无法连接通话服务,正在自动尝试恢复。";this.setAvailabilityState("offline",r)}else this.setAvailabilityState("recovering","正在确认后台连接状态。");this.presenceFailureCount<=Ct&&this.appendLog("widget 运行态心跳失败",{trigger:e,failure_count:this.presenceFailureCount,message:i?.message||String(i)})}}startPresenceTouch(){this.legacyOutboundOnly||(this.touchWidgetPresence("mount"),this.presenceTouchTimer||(this.presenceTouchTimer=window.setInterval(()=>{this.touchWidgetPresence("interval")},$i)))}async touchIssuedSession(e,t=null){if(!this.issuedSession?.session_id)return;const i=this.issuedSession.session_id;try{const s=await fetch(X(this.apiBaseUrl,"api/sip/touch-call-session"),{method:"POST",credentials:"include",cache:"no-store",keepalive:!0,headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:i,state:e,client_summary:this.buildClientEvidenceSummary(e,t)})});if(!s.ok){const n=new Error(`会话心跳失败:${s.status}`);throw n.status=s.status,n}const r=await s.json().catch(()=>({}));await this.handleServerSessionAction("touch-call-session",r)}catch(s){if(Oe(s)){await this.handleProtocolViolation(s);return}if(s?.status===404&&this.wasSessionRecentlyReleased(i))return;this.appendLog("网页通话会话心跳失败",{session_id:i,state:e,message:s?.message||String(s)}),s?.status===404&&this.handleMissingIssuedSession(i,e)}}async handleMissingIssuedSession(e,t){if(!(this.sessionRecoveryInFlight||this.issuedSession?.session_id!==e)){this.sessionRecoveryInFlight=(async()=>{this.stopSessionTouch(),this.appendLog("网页电话待机会话已失效,准备自动恢复。",{session_id:e,state:t}),this.setAvailabilityState("recovering","原待机会话已失效,正在自动恢复。"),this.issuedSession?.session_id===e&&(this.issuedSession=null),!this.legacyOutboundOnly&&["standby","registering","standbyRequesting","failed","idle","missed","rejected","ended"].includes(this.state)&&(this.setState("standbyRequesting","待机会话已失效,正在自动重新进入待机。"),await this.startStandby({force:!0}))})();try{await this.sessionRecoveryInFlight}finally{this.sessionRecoveryInFlight=null}}}async reportIssuedSessionEvent(e,t=null,i=null){if(!this.issuedSession?.session_id)return;const s=this.issuedSession.session_id,r=i||this.state;let n=null,o="";try{const d=await fetch(X(this.apiBaseUrl,"api/sip/report-call-session-event"),{method:"POST",credentials:"include",cache:"no-store",keepalive:!0,headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:s,source:"widget-runtime",event:e,state:r,detail:this.buildClientEvidenceSummary(r,t)})});if(!d.ok){n=d.status;const u=await d.json().catch(()=>({}));throw o=u?.detail?String(u.detail):"",new Error(o||`事件留证失败:${d.status}`)}}catch(d){await fetch(X(this.apiBaseUrl,"api/sip/report-call-session-event-delivery-failure"),{method:"POST",credentials:"include",cache:"no-store",keepalive:!0,headers:{"Content-Type":"application/json"},body:JSON.stringify({session_id:s,source:"widget-runtime",event:e,state:r,detail:this.buildClientEvidenceSummary(r,t),delivery_error:{status:typeof n=="number"?n:null,detail:o||"",message:d?.message||String(d)}})}).catch(()=>{}),this.appendLog("网页通话事件留证失败",{session_id:s,event:e,state:r,message:d?.message||String(d)})}}stopSessionTouch(){this.sessionTouchTimer&&(window.clearInterval(this.sessionTouchTimer),this.sessionTouchTimer=null)}syncSessionTouch(e){if(!(!!this.issuedSession?.session_id&&["connected","registered","dialing","ringing","answered","standby","incoming"].includes(e))){this.stopSessionTouch();return}this.touchIssuedSession(e),this.sessionTouchTimer||(this.sessionTouchTimer=window.setInterval(()=>{this.touchIssuedSession(this.state)},Ai))}setState(e,t=""){this.state=e,this.stateDetail=t||ie[e]||ie.idle,this.syncAvailabilityState(),this.render(),this.emitHostCallback("onStateChange",this.getSnapshot())}appendLog(e,t){if(!this.refs.logs)return;const i=document.createElement("div");for(i.className="log",i.textContent=qi(e,t),this.refs.logs.prepend(i);this.refs.logs.children.length>8;)this.refs.logs.lastElementChild?.remove()}render(){const e=this.getVisibleState(),t=e!=="idle",i=this.pendingIncomingCall?.displayName||this.pendingIncomingCall?.user||"管理员",s=["requesting","registering","calling","connected","incoming"].includes(this.state);let r="网页通话",n=this.options.text||"联系管理员",o=this.transientPanelDetail||"",d=null,u=null;e==="ringing"?(r="网页来电",n=i,o=o||"对方正在呼叫你,请选择接听或挂断。",d={text:"接听",className:"btn btn-primary",disabled:!1},u={text:"挂断",className:"btn btn-danger",disabled:!1}):e==="dialing"?(r="正在呼叫",o=o||"正在为你接通管理员,请稍候。",u={text:"挂断",className:"btn btn-danger",disabled:!1}):e==="connected"?(r="通话进行中",n=this.pendingIncomingCall?.displayName||n,o=o||"通话已接通,如需结束请点击挂断。",u={text:"挂断",className:"btn btn-danger",disabled:!1}):e==="ended"?(r="通话已结束",o=o||"本次通话已结束。"):e==="failed"&&(r="暂未接通",o=o||"当前暂时无法接通,请稍后再试。");const h=this.missedIncomingCount>0?String(this.missedIncomingCount):this.triggerHintDismissed?"":" ";this.refs.trigger.disabled=s,this.refs.triggerBadge.hidden=!h,this.refs.triggerBadge.textContent=h,this.refs.triggerBadge.className=this.missedIncomingCount>0?"trigger-badge":"trigger-badge trigger-badge--dot",this.refs.panelShell.className=t?"panel-shell panel-shell--visible":"panel-shell",this.refs.panel.setAttribute("aria-hidden",t?"false":"true"),this.refs.panelEyebrow.textContent=r,this.refs.panelTitle.textContent=n,this.refs.status.textContent=Ze[e]||Ze.idle,this.refs.status.className=e==="failed"?"status status--failed":"status",this.refs.panelDetail.textContent=o,this.refs.primary.hidden=!d,d?(this.refs.primary.textContent=d.text,this.refs.primary.className=d.className,this.refs.primary.disabled=d.disabled):this.refs.primary.textContent="",this.refs.secondary.hidden=!u,u?(this.refs.secondary.textContent=u.text,this.refs.secondary.className=u.className,this.refs.secondary.disabled=u.disabled):this.refs.secondary.textContent="";const m=+!!d+ +!!u;this.refs.actions.className=m<=1?"actions actions--single":"actions";const S=String(this.options.siteKey||"").trim(),x=String(this.bootstrap?.default_site_key||"").trim(),_=String(le(this.options)||"").trim(),C=String(this.bootstrap?.default_business_key||"").trim(),Q=S||(x?`${x}(服务端默认站点)`:"等待服务端解析"),p=this.issuedSession?.resolved_business_key||_||(C?`${C}(服务端默认规则)`:"等待服务端解析");this.legacyOutboundOnly?this.refs.meta.textContent=`入站规则:${p};站点键:${Q}`:this.refs.meta.textContent=`站点键:${Q};网页电话会先进入可被叫待机;访客主动呼叫默认规则:${p}`,this.refs.availability.textContent=this.availabilityDetail?`${de[this.availabilityState]||de.recovering} · ${this.availabilityDetail}`:de[this.availabilityState]||de.recovering,this.refs.availability.className=`availability availability--${this.availabilityState}`,this.availabilityState==="offline"?this.refs.hint.textContent=this.networkOnline===!1?"当前浏览器网络已断开。系统会在网络恢复后自动尝试重连网页电话。":"当前无法连接通话服务。系统会继续自动恢复,暂时不要重复点击。":this.availabilityState==="preparing"?this.refs.hint.textContent="当前正在准备待机能力。只要页面在线,系统会自动进入可呼入、可呼出的网页电话状态。":this.availabilityState==="working"?this.state==="incoming"?this.refs.hint.textContent="当前已有来电,直接接听或拒接即可;处理完成后会自动回到待机。":this.state==="connected"?this.refs.hint.textContent="当前通话已接通,挂断后会自动回到待机,不需要额外刷新页面。":this.refs.hint.textContent="当前正在切换到外呼链路,请等待振铃、接通或自动回到待机。":this.availabilityState==="recovering"?this.refs.hint.textContent="当前页面仍在线,系统正在自动恢复可被叫待机,不需要刷新页面或手动切换模式。":this.availabilityState==="conflicted"?this.refs.hint.textContent="检测到前后端协议不一致,当前已停止自动恢复。请先排查版本和协议口径,再继续使用。":this.refs.hint.textContent=this.isMobileViewport?Ri:"桌面网页可直接使用;若宿主站点启用了来源白名单或接入令牌,请按站点配置提供对应参数。",this.refs.debugPanel.hidden=!this.debugPanelEnabled}async destroy(){this.clearAutoStandbyRecovery(),this.clearPanelAutoClose(),await this.disconnectAndCleanup("destroy-widget",{suppressStateReset:!0}),typeof document<"u"&&this.visibilityHandler&&(document.removeEventListener("visibilitychange",this.visibilityHandler),this.visibilityHandler=null),this.stopSessionTouch(),this.stopPresenceTouch(),this.stopTransportDisconnectGrace(),this.pendingLifecycleMode=null,this.pendingIncomingCall=null,typeof window<"u"&&(this.onlineHandler&&(window.removeEventListener("online",this.onlineHandler),this.onlineHandler=null),this.offlineHandler&&(window.removeEventListener("offline",this.offlineHandler),this.offlineHandler=null),this.beforeUnloadHandler&&(window.removeEventListener("beforeunload",this.beforeUnloadHandler),this.beforeUnloadHandler=null),this.pageHideHandler&&(window.removeEventListener("pagehide",this.pageHideHandler),this.pageHideHandler=null)),this.e2eBridgeEnabled&&typeof window<"u"&&window.__embeddedCallWidgetE2E__&&delete window.__embeddedCallWidgetE2E__,this.shadowRoot.innerHTML=""}}function Rt(a){return new Wi(a)}return typeof window<"u"&&(window.__EmbeddedCallWidgetRuntime__={createEmbeddedCallWidgetRuntime:Rt}),Fe.createEmbeddedCallWidgetRuntime=Rt,Object.defineProperty(Fe,Symbol.toStringTag,{value:"Module"}),Fe})({});
@@ -11676,8 +11676,8 @@ class Ki {
11676
11676
  this.panelAutoCloseTimer = null, this.transientPanelState = null, this.transientPanelDetail = "", this.render();
11677
11677
  }, i));
11678
11678
  }
11679
- markDebugPanelEnabled(e) {
11680
- !this.debugRequested || !this.hasAccessToken || this.debugPanelEnabled || (this.debugPanelEnabled = !0, this.appendLog("调试模式门禁已成立,开始展示调试面板。", { source: e }), this.render());
11679
+ markDebugPanelEnabled(e, t) {
11680
+ !this.debugRequested || !this.hasAccessToken || !t || this.debugPanelEnabled || (this.debugPanelEnabled = !0, this.appendLog("调试模式门禁已成立,开始展示调试面板。", { source: e }), this.render());
11681
11681
  }
11682
11682
  dismissTriggerHint() {
11683
11683
  this.triggerHintDismissed || (this.triggerHintDismissed = !0, Ni(this.triggerHintStorageKey, !0));
@@ -12036,7 +12036,7 @@ class Ki {
12036
12036
  throw new Error(d?.detail || `会话签发失败:${r.status}`);
12037
12037
  }
12038
12038
  const n = await r.json(), o = ft(n, Te.outbound, "issue-call-session");
12039
- return this.markDebugPanelEnabled("issue-call-session"), this.issuedSession = n, this.reportIssuedSessionEvent("issued-session-fetched", {
12039
+ return this.markDebugPanelEnabled("issue-call-session", n?.debug_access_granted === !0), this.issuedSession = n, this.reportIssuedSessionEvent("issued-session-fetched", {
12040
12040
  sessionId: n.session_id,
12041
12041
  targetExtension: n.target_extension,
12042
12042
  dialTargetExtension: n.dial_target_extension || n.target_extension,
@@ -12081,7 +12081,7 @@ class Ki {
12081
12081
  throw new Error(n?.detail || `待机会话签发失败:${i.status}`);
12082
12082
  }
12083
12083
  const s = await i.json(), r = ft(s, Te.standby, "issue-widget-standby-session");
12084
- return this.markDebugPanelEnabled("issue-widget-standby-session"), this.issuedSession = s, this.reportIssuedSessionEvent("standby-session-fetched", {
12084
+ return this.markDebugPanelEnabled("issue-widget-standby-session", s?.debug_access_granted === !0), this.issuedSession = s, this.reportIssuedSessionEvent("standby-session-fetched", {
12085
12085
  sessionId: s.session_id,
12086
12086
  resolvedBusinessKey: s.resolved_business_key || null,
12087
12087
  standbyMode: s.standby_mode || null
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "package_name": "@shenyin/embedded-call-widget",
3
- "source_package_version": "3.0.0",
4
- "built_at": "2026-04-09T00:08:38.543Z",
3
+ "source_package_version": "3.0.1",
4
+ "built_at": "2026-04-09T01:03:56.397Z",
5
5
  "outputs": {
6
6
  "esm_shell": "embedded-call-widget.js",
7
7
  "esm_runtime": "embedded-call-widget-runtime.js",
@@ -55,9 +55,9 @@
55
55
  },
56
56
  "release_metadata": {
57
57
  "npm_package_name": "@shenyin/embedded-call-widget",
58
- "git_tag": "embedded-call-widget-v3.0.0",
59
- "git_tag_alias": "v3.0.0",
60
- "cdn_version_path": "embedded-call-widget/3.0.0/",
58
+ "git_tag": "embedded-call-widget-v3.0.1",
59
+ "git_tag_alias": "v3.0.1",
60
+ "cdn_version_path": "embedded-call-widget/3.0.1/",
61
61
  "cdn_latest_path": "embedded-call-widget/latest/",
62
62
  "semver_channel": "stable"
63
63
  },
@@ -70,9 +70,9 @@
70
70
  },
71
71
  "esm_runtime": {
72
72
  "file": "embedded-call-widget-runtime.js",
73
- "size_bytes": 536446,
74
- "sha256": "c39b8f09da1553f53238ac3b2460afef6bf22d6284cd04810cb600768ce34efe",
75
- "sri_sha384": "sha384-k0ux4gmWFEjjh/p3e+JXNsruNS6d3OSJXEpTCRc2V8a9iHk/DM6yxYNLz2/TeXwM"
73
+ "size_bytes": 536519,
74
+ "sha256": "8ef265e594383cce14b511cecfc3611508a187cc6d133dbb66a3c84330720db5",
75
+ "sri_sha384": "sha384-Y88gMCdP7x5XrNjJRruLHW1YG5RkwKrVFQH9gr+cSMB9OSpK5OvEmb9DfG3Y9Chm"
76
76
  },
77
77
  "iife_shell": {
78
78
  "file": "embedded-call-widget.iife.js",
@@ -82,9 +82,9 @@
82
82
  },
83
83
  "iife_runtime": {
84
84
  "file": "embedded-call-widget-runtime.iife.js",
85
- "size_bytes": 350688,
86
- "sha256": "42e75ecc62f3beb612bd2537f4951a47a3d4f525d89347ef08982f7157f29317",
87
- "sri_sha384": "sha384-wOqc+xU3zqQidqWCO9Mb1rRDuMj17vlRJGysX14yy40pqtJKbHTbRWglmygXYd0j"
85
+ "size_bytes": 350752,
86
+ "sha256": "203b997a290a0c7a01d3e4a30f9f941148ff9b95c3c8f2b5729fc599c061f0fd",
87
+ "sri_sha384": "sha384-+fypdWEKc3W42EGbtYbxsCIoE+XE282pnYglPUjjmVzp0AGBKVl1cT72mqaic+CX"
88
88
  }
89
89
  }
90
90
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shenyin/embedded-call-widget",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "files": [