@saiboniuma/realtime-core 0.1.0

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.
@@ -0,0 +1 @@
1
+ (function(o,v){typeof exports=="object"&&typeof module<"u"?v(exports,require("@microsoft/signalr")):typeof define=="function"&&define.amd?define(["exports","@microsoft/signalr"],v):(o=typeof globalThis<"u"?globalThis:o||self,v(o.RealtimeCore={},o.signalr))})(this,(function(o,v){"use strict";const g={CONNECTING:0,CONNECTED:1,DISCONNECTED:2,FAILED:3,RECONNECTING:4};class N{constructor(){this.listeners=new Map}on(e,t){this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t)}off(e,t){const n=this.listeners.get(e);n&&n.delete(t)}emit(e,t){const n=this.listeners.get(e),i=n?n.size:0;e==="message:received"&&console.log("[EventManager] emit message:received, 监听器数量:",i),n&&n.forEach(a=>a(t))}clear(){this.listeners.clear()}clearEvent(e){this.listeners.delete(e)}}let p=2;function _(s){p=s}function z(){return p}const r={verbose:(s,...e)=>p>=4&&console.log(`[${s}]`,...e),debug:(s,...e)=>p>=3&&console.log(`[${s}]`,...e),info:(s,...e)=>p>=2&&console.log(`[${s}]`,...e),warn:(s,...e)=>p>=1&&console.warn(`[${s}]`,...e),error:(s,...e)=>p>=0&&console.error(`[${s}]`,...e)};class U{constructor(e,t,n){this._currentAttempt=0,this._isRetrying=!1,this.retryTimer=null,this.stopped=!1,this.eventListeners=new Map,this.maxRetries=e.maxRetries??10,this.baseDelay=e.baseDelay??1e3,this.maxDelay=e.maxDelay??3e4,this.jitterFactor=e.jitterFactor??.2,this.useExponentialBackoff=e.useExponentialBackoff??!0,this.onReconnect=t,this.onFinalFail=n}get currentAttempt(){return this._currentAttempt}get isRetrying(){return this._isRetrying}on(e,t){this.eventListeners.has(e)||this.eventListeners.set(e,new Set),this.eventListeners.get(e).add(t)}off(e,t){const n=this.eventListeners.get(e);n&&n.delete(t)}emit(e,t){const n=this.eventListeners.get(e);n&&n.forEach(i=>i(t))}calculateDelay(e){let t;this.useExponentialBackoff?t=this.baseDelay*Math.pow(2,e):t=this.baseDelay*(e+1),t=Math.min(t,this.maxDelay);const n=t*this.jitterFactor*(Math.random()*2-1);return t=t+n,Math.max(0,Math.floor(t))}start(){if(this._isRetrying){r.warn("ReconnectManager","重连已在进行中,忽略重复调用");return}this._isRetrying=!0,this.stopped=!1,this._currentAttempt=0,r.info("ReconnectManager",`开始重连流程,最大重试次数: ${this.maxRetries}`),this.scheduleNextRetry()}scheduleNextRetry(){if(this.stopped)return;if(this._currentAttempt>=this.maxRetries){r.error("ReconnectManager",`已达到最大重试次数 ${this.maxRetries},重连失败`),this._isRetrying=!1,this.emit("failed",this._currentAttempt),this.onFinalFail();return}const e=this.calculateDelay(this._currentAttempt),t=this._currentAttempt+1;r.info("ReconnectManager",`第 ${t} 次重连将在 ${e}ms 后执行`),this.emit("reconnecting",t),this.retryTimer=setTimeout(async()=>{if(!this.stopped){this._currentAttempt=t;try{await this.onReconnect(t),r.info("ReconnectManager",`第 ${t} 次重连成功`),this._isRetrying=!1,this.emit("reconnected",t)}catch(n){r.warn("ReconnectManager",`第 ${t} 次重连失败:`,n),this.scheduleNextRetry()}}},e)}stop(){this.stopped=!0,this._isRetrying=!1,this.retryTimer&&(clearTimeout(this.retryTimer),this.retryTimer=null),r.debug("ReconnectManager","重连已停止")}reset(){this._currentAttempt=0,this._isRetrying=!1,this.stopped=!1,this.retryTimer&&(clearTimeout(this.retryTimer),this.retryTimer=null),r.debug("ReconnectManager","重连计数器已重置")}}var M=(s=>(s.Closed="Closed",s.Open="Open",s.HalfOpen="HalfOpen",s))(M||{});class H{constructor(e={}){this._state="Closed",this.failureCount=0,this.successCount=0,this.openTimestamp=0,this.stateChangeListeners=new Set,this.failureThreshold=e.failureThreshold??10,this.resetTimeout=e.resetTimeout??6e4,this.successThreshold=e.successThreshold??3}get state(){return this._state==="Open"&&Date.now()-this.openTimestamp>=this.resetTimeout&&this.transitionTo("HalfOpen"),this._state}onStateChanged(e){this.stateChangeListeners.add(e)}offStateChanged(e){this.stateChangeListeners.delete(e)}transitionTo(e){if(this._state===e)return;const t=this._state;this._state=e,r.info("CircuitBreaker",`状态变化: ${t} -> ${e}`),e==="HalfOpen"&&(this.successCount=0),e==="Closed"&&(this.failureCount=0),e==="Open"&&(this.openTimestamp=Date.now()),this.stateChangeListeners.forEach(n=>n({from:t,to:e}))}recordSuccess(){const e=this.state;e==="HalfOpen"?(this.successCount++,r.debug("CircuitBreaker",`半开状态记录成功: ${this.successCount}/${this.successThreshold}`),this.successCount>=this.successThreshold&&this.transitionTo("Closed")):e==="Closed"&&this.failureCount>0&&(this.failureCount=0,r.debug("CircuitBreaker","关闭状态记录成功,失败计数已重置"))}recordFailure(){const e=this.state;e==="HalfOpen"?(r.warn("CircuitBreaker","半开状态记录失败,重新打开熔断器"),this.transitionTo("Open")):e==="Closed"&&(this.failureCount++,r.debug("CircuitBreaker",`关闭状态记录失败: ${this.failureCount}/${this.failureThreshold}`),this.failureCount>=this.failureThreshold&&(r.warn("CircuitBreaker",`失败次数达到阈值 ${this.failureThreshold},熔断器打开`),this.transitionTo("Open")))}canAttempt(){return this.state!=="Open"}reset(){this.failureCount=0,this.successCount=0,this.openTimestamp=0,this._state!=="Closed"?this.transitionTo("Closed"):r.debug("CircuitBreaker","熔断器已重置")}}const w="sb_client_id";function b(){const s=Date.now().toString(36),e=Math.random().toString(36).substring(2,15),t=Math.random().toString(36).substring(2,15);return`cid_${s}_${e}${t}`}function P(){try{let s=localStorage.getItem(w);return s||(s=b(),localStorage.setItem(w,s)),s}catch{return b()}}function Y(){const s=b();try{localStorage.setItem(w,s)}catch{}return s}function q(){try{return!!localStorage.getItem(w)}catch{return!1}}function l(s,e){try{return s()??e}catch{return e}}function $(){const s=navigator,e=s.connection||s.mozConnection||s.webkitConnection;return{userAgent:l(()=>navigator.userAgent,""),platform:l(()=>navigator.platform,""),screenWidth:l(()=>screen.width,0),screenHeight:l(()=>screen.height,0),viewportWidth:l(()=>window.innerWidth,0),viewportHeight:l(()=>window.innerHeight,0),devicePixelRatio:l(()=>window.devicePixelRatio,1),colorDepth:l(()=>screen.colorDepth,0),connectionType:l(()=>e==null?void 0:e.type),effectiveType:l(()=>e==null?void 0:e.effectiveType),downlink:l(()=>e==null?void 0:e.downlink),rtt:l(()=>e==null?void 0:e.rtt),language:l(()=>navigator.language,""),languages:l(()=>[...navigator.languages||[]],[]),timezone:l(()=>Intl.DateTimeFormat().resolvedOptions().timeZone,""),timezoneOffset:l(()=>new Date().getTimezoneOffset(),0),currentUrl:l(()=>window.location.href,""),pageTitle:l(()=>document.title,""),referrer:l(()=>document.referrer,""),touchSupport:l(()=>"ontouchstart"in window||navigator.maxTouchPoints>0,!1),maxTouchPoints:l(()=>navigator.maxTouchPoints,0),deviceMemory:l(()=>s.deviceMemory),hardwareConcurrency:l(()=>navigator.hardwareConcurrency),cookieEnabled:l(()=>navigator.cookieEnabled,!0),prefersDarkMode:l(()=>window.matchMedia("(prefers-color-scheme: dark)").matches,!1),online:l(()=>navigator.onLine,!0)}}class F{constructor(e){this.baseURL=e}async getToken(e){const t=new FormData;t.append("platformKey",e.platformKey),e.deviceId&&t.append("deviceId",e.deviceId),e.userId&&t.append("userId",e.userId),e.nickname&&t.append("nickname",e.nickname),e.avatar&&t.append("avatar",e.avatar),e.metadata&&t.append("metadata",e.metadata),t.append("platformType",(e.platformType??0).toString()),e.source&&t.append("source",e.source);const n=e.clientId||P();t.append("clientId",n);try{const c=e.deviceInfo||JSON.stringify($());t.append("deviceInfo",c)}catch{}const i=await fetch(`${this.baseURL}/api/ChatAuth/GetToken`,{method:"POST",body:t});if(!i.ok)throw new Error(`HTTP 错误: ${i.status}`);const a=await i.json();if(a.code!==0)throw new Error(a.message||"获取 Token 失败");return a.data}async getTokenSimple(e,t,n=0,i){return this.getToken({platformKey:e,userId:t,platformType:n,nickname:i==null?void 0:i.nickname,avatar:i==null?void 0:i.avatar,deviceId:i==null?void 0:i.deviceId,metadata:i==null?void 0:i.metadata,source:i==null?void 0:i.source})}async updateUserInfo(e,t,n,i,a){const c=new FormData;c.append("PlatformKey",e),c.append("DeviceId",t),n&&c.append("UserId",n),i&&c.append("Nickname",i),a&&c.append("Avatar",a);const u=await fetch(`${this.baseURL}/api/ChatAuth/UpdateUserInfo`,{method:"POST",body:c});if(!u.ok)throw new Error(`HTTP 错误: ${u.status}`);const d=await u.json();if(d.code!==0)throw new Error(d.message||"更新用户信息失败")}}const C="chat_auth_data";class B{saveAuthData(e){try{const t={...e,expireAt:Date.now()+e.expireTime*1e3};localStorage.setItem(C,JSON.stringify(t))}catch(t){r.warn("Session","保存认证数据失败:",t)}}loadAuthData(){try{const e=localStorage.getItem(C);if(!e)return null;const t=JSON.parse(e);return t.expireAt&&t.expireAt-300*1e3<Date.now()?(localStorage.removeItem(C),null):t}catch(e){return r.warn("Session","加载认证数据失败:",e),null}}clearAuthData(){localStorage.removeItem(C)}updateUserProfile(e){try{const t=localStorage.getItem(C);if(t){const n=JSON.parse(t);n.user&&(e.nickname!==void 0&&(n.user.nickname=e.nickname),e.avatar!==void 0&&(n.user.avatar=e.avatar),localStorage.setItem(C,JSON.stringify(n)))}}catch(t){r.warn("Session","更新本地存储失败:",t)}}}class x{constructor(e,t){this.lastLoginRequest=null,this.active=!1,this.baseURL="",this.chatUserId="",this.clientId="",this.platformKey="",this.onAgentChanged=null,this.authService=e,this.client=t}setLastLoginRequest(e){this.lastLoginRequest=e,this.active=!0}setSessionInfo(e){this.baseURL=e.baseURL,this.chatUserId=e.chatUserId,this.clientId=e.clientId,this.platformKey=e.platformKey}async refresh(){if(!this.lastLoginRequest)throw new Error("无法刷新:缺少登录参数");r.info("TokenRefresher","正在自动刷新 Token...");try{const e=await this.authService.getToken(this.lastLoginRequest);await this.client.login(e.userID,e.token),r.info("TokenRefresher","Token 刷新成功")}catch{r.warn("TokenRefresher","标准刷新失败,尝试 RefreshSession 容灾恢复..."),await this.refreshSession()}}async refreshSession(){if(!this.baseURL||!this.chatUserId)throw new Error("无法容灾恢复:缺少 session 参数");r.info("TokenRefresher","正在调用 RefreshSession...");const e=await fetch(`${this.baseURL}/api/ChatAuth/RefreshSession`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({chatUserId:this.chatUserId,clientId:this.clientId,platformKey:this.platformKey})});if(!e.ok)throw new Error(`RefreshSession HTTP 错误: ${e.status}`);const t=await e.json();if(t.code!==0)throw new Error(t.message||"RefreshSession 失败");const n=t.data;return await this.client.login(this.chatUserId,n.token),r.info("TokenRefresher","RefreshSession 成功"),n.agentUserId&&this.onAgentChanged&&this.onAgentChanged(n.agentUserId),n}isActive(){return this.active}stop(){this.lastLoginRequest=null,this.active=!1,this.onAgentChanged=null}}const Q={Sending:1,Sent:2,Success:2,Delivered:3,Read:4,Failed:5,Withdrawn:6,Edited:7,Deleted:8},h={TextMessage:101,PictureMessage:102,VoiceMessage:103,VideoMessage:104,FileMessage:105,AtTextMessage:106,MergeMessage:107,CardMessage:108,LocationMessage:109,CustomMessage:110,TypingMessage:113,QuoteMessage:114,FaceMessage:115,SystemNotice:"system.notice",SystemMember:"system.member",SystemMute:"system.mute",CallVoice:"call.voice",CallVideo:"call.video",CallMissed:"call.missed",InteractPoke:"interact.poke",InteractShake:"interact.shake",InteractFlash:"interact.flash"};class R{static parse(e){if(typeof e=="string")try{return JSON.parse(e)}catch{return e}return e}}function X(s){if(typeof s=="number")return s;switch(s){case"text":return h.TextMessage;case"image":return h.PictureMessage;case"voice":return h.VoiceMessage;case"video":return h.VideoMessage;case"file":return h.FileMessage;case"custom":return h.CustomMessage;case"face":return h.FaceMessage;case"quote":return h.QuoteMessage;default:return s.startsWith("sys.")?900:s.includes(".")?s:h.TextMessage}}function Z(s){if(!s)return"";try{const e=JSON.parse(s);return typeof e=="string"?e:e.content?e.content:s}catch{return s}}function ee(s){if(s)try{const e=JSON.parse(s),t=e.url||"",n=e.width||0,i=e.height||0,a=e.size||0,c=e.type||"image/jpeg";if(!t)return;const u={uuid:"",type:c,size:a,width:n,height:i,url:t};return{sourcePath:t,sourcePicture:{...u},bigPicture:{...u},snapshotPicture:{...u}}}catch{return}}function I(s,e,t){if(s[e]!==void 0)return s[e];if(s[t]!==void 0)return s[t]}function y(s){if(!s)return null;if(s.sendID!==void 0)return s;const e=X(s.contentType||s.ContentType||"text"),t=s.content??s.Content??"",n=Z(t),i=s.timestamp||s.Timestamp,a=i?new Date(i).getTime():Date.now(),c=s.seq??s.Seq??0,u=s.id??s.Id??"",d=s.senderId??s.SenderId??"",m=String(s.conversationId??s.ConversationId??""),T=I(s,"status","Status")??2,ue=I(s,"deliveredAt","DeliveredAt"),de=I(s,"readAt","ReadAt"),ge=I(s,"editedAt","EditedAt"),fe=I(s,"previousContent","PreviousContent"),me=I(s,"isWithdrawn","IsWithdrawn"),ve=typeof e=="number"&&e===h.TextMessage?{content:n}:void 0,ye=typeof e=="number"&&e===h.PictureMessage?ee(t):void 0,pe=typeof e=="number"&&e===h.CustomMessage?{data:t,extension:"",description:""}:void 0;return{clientMsgID:u||`seq_${c}`,serverMsgID:String(c),createTime:a,sendTime:a,sendID:d,recvID:"",msgFrom:1,contentType:e,senderNickname:"",senderFaceUrl:"",content:n,textElem:ve,pictureElem:ye,customElem:pe,conversationID:m,isRead:!1,status:T,deliveredAt:ue,readAt:de,editedAt:ge,previousContent:fe,isWithdrawn:me}}function D(s){if(!s)return null;if(s.conversationID!==void 0)return s;const e=String(s.id??s.Id??"");return e?{conversationID:e,conversationType:(s.type??s.Type??"direct")==="group"?3:1,userID:"",groupID:(s.type??s.Type)==="group"?e:"",showName:s.title??s.Title??"",faceURL:"",recvMsgOpt:0,unreadCount:s.unreadCount??s.UnreadCount??0,latestMsg:s.lastMsgPreview??s.LastMsgPreview??"",latestMsgSendTime:0,draftText:"",draftTextTime:0,isPinned:!1,isPrivateChat:!1}:null}function k(s){return s?(Array.isArray(s)?s:[s]).map(D).filter(t=>t!==null):[]}function O(s){if(!s)return null;if(s.userID!==void 0)return s;const e=s.userId??s.UserId??"";return e?{userID:e,nickname:s.nickname??s.Nickname??"",faceURL:s.avatar??s.Avatar??"",createTime:0,ex:""}:null}const f={SessionInit:"session_init",VisitorOnline:"visitor_online",VisitorOffline:"visitor_offline",UserInfoUpdated:"user_info_updated",MessageRead:"message_read"},E=Object.values(f),L={[f.VisitorOnline]:"客户已上线",[f.VisitorOffline]:"客户已离线",[f.UserInfoUpdated]:"客户更新用户信息",[f.SessionInit]:"会话已建立",[f.MessageRead]:"对方已读"};function A(s){var e;if(Number(s.contentType)!==h.CustomMessage||!((e=s.customElem)!=null&&e.data))return!1;try{const t=JSON.parse(s.customElem.data);return E.includes(t.type)}catch{return!1}}function S(s){var e;if(Number(s.contentType)!==h.CustomMessage||!((e=s.customElem)!=null&&e.data))return null;try{const t=JSON.parse(s.customElem.data);return E.includes(t.type)?t.type:null}catch{return null}}function te(s){const e=S(s);return e&&L[e]||""}function j(s){const e=S(s);return e!==null&&e!==f.SessionInit&&e!==f.MessageRead}function se(s){var i;const e=S(s);if(e===f.MessageRead)return"";const t=e&&L[e]||"";if(t)return`[${t}]`;if(e)return"";const n=Number(s.contentType);if(n===h.TextMessage){if((i=s.textElem)!=null&&i.content)return s.textElem.content;try{return JSON.parse(s.content).content||s.content}catch{return s.content||""}}return n===h.PictureMessage?"[图片]":n===h.VoiceMessage?"[语音]":n===h.VideoMessage?"[视频]":n===h.FileMessage?"[文件]":"[消息]"}const ne=1501,ie=1599;function re(s){const e=Number(s.contentType);if(e>=ne&&e<=ie)return{showInChat:!1,showInListPreview:!1,previewText:"",systemType:null,systemLabel:""};const t=S(s);if(t!==null){const i=L[t]||"";return t===f.SessionInit||t===f.MessageRead?{showInChat:!1,showInListPreview:!1,previewText:"",systemType:t,systemLabel:i}:t===f.UserInfoUpdated?{showInChat:!0,showInListPreview:!1,previewText:"",systemType:t,systemLabel:i}:{showInChat:!0,showInListPreview:!0,previewText:i?`[${i}]`:"",systemType:t,systemLabel:i}}return{showInChat:!0,showInListPreview:!0,previewText:ae(s,e),systemType:null,systemLabel:""}}function ae(s,e){var t;if(e===h.TextMessage){if((t=s.textElem)!=null&&t.content)return s.textElem.content;try{return JSON.parse(s.content).content||s.content}catch{return s.content||""}}return e===h.PictureMessage?"[图片]":e===h.VoiceMessage?"[语音]":e===h.VideoMessage?"[视频]":e===h.FileMessage?"[文件]":(e===h.CustomMessage,"[消息]")}class J{constructor(){this.types=new Map}register(e,t,n={}){this.types.set(e,{renderer:t,handler:n})}tryResolve(e){const t=this.types.get(e);return t?{...t}:null}isRegistered(e){return this.types.has(e)}}class V{constructor(e){this.client=e,this.types=new J}static isSystemMessage(e){return A(e)}async sendText(e,t,n=!1){const i=this.client.getHub(),a=this.client.getConversationId();if(console.log("[MessageService] sendText 调用, conversationId:",a,"text:",t),!a)throw new Error("会话 ID 为空,无法发送消息。请先通过 setConversationId 设置会话 ID。");try{const c=JSON.stringify({content:t}),u=await i.invoke("SendMessage",a,h.TextMessage,c);console.log("[MessageService] sendText 服务端返回:",u);const d=y(u);if(!d)throw r.warn("Message","SendMessage 返回 null,可能是 conversationId 无效"),new Error("消息发送失败:服务端未返回消息对象");return console.log("[MessageService] sendText 映射成功, clientMsgID:",d.clientMsgID),d}catch(c){throw console.error("[MessageService] sendText 失败:",c),r.error("Message","发送消息失败:",c),c}}async sendImage(e,t,n,i,a=!1){const c=this.client.getHub(),u=this.client.getConversationId();try{const d=JSON.stringify({type:"image",url:n,width:i.width,height:i.height,size:i.size}),m=await c.invoke("SendMessage",u,h.PictureMessage,d);return y(m)??R.parse(m)}catch(d){throw r.error("Message","发送图片失败:",d),d}}async sendCustomMessage(e,t,n,i){const a=this.client.getHub(),c=this.client.getConversationId();try{const u=JSON.stringify({data:t,extension:n,description:i}),d=await a.invoke("SendMessage",c,h.CustomMessage,u);return y(d)??R.parse(d)}catch(u){throw r.error("Message","发送自定义消息失败:",u),u}}async sendSystemMessage(e,t){const n=this.client.getHub(),i=this.client.getConversationId();try{const a=JSON.stringify({...t,type:e,timestamp:Date.now()});await n.invoke("SendSystemMessage",i,e,a)}catch(a){r.error("Message",`发送系统消息(${e})失败:`,a)}}async sendUserInfoUpdated(e,t,n){await this.sendSystemMessage(f.UserInfoUpdated,{updates:t,previousValues:n})}async sendSessionInit(e,t){await this.sendSystemMessage(f.SessionInit,t)}async sendVisitorOffline(e,t){await this.sendSystemMessage(f.VisitorOffline,t)}async sendVisitorOnline(e,t){await this.sendSystemMessage(f.VisitorOnline,t)}async acknowledgeDelivery(e,t){const n=this.client.getHub();try{await n.invoke("AcknowledgeDelivery",e,t)}catch(i){throw r.error("Message",`上报送达失败(conversationId=${e}, seq=${t}):`,i),i}}async acknowledgeRead(e,t){const n=this.client.getHub();try{await n.invoke("AcknowledgeRead",e,t)}catch(i){throw r.error("Message",`上报已读失败(conversationId=${e}, lastReadSeq=${t}):`,i),i}}async withdraw(e,t){const n=this.client.getHub();try{const i=await n.invoke("WithdrawMessage",e,t);return y(i)}catch(i){throw r.error("Message",`撤回消息失败(conversationId=${e}, seq=${t}):`,i),i}}async edit(e,t,n){const i=this.client.getHub();try{const a=await i.invoke("EditMessage",e,t,n);return y(a)}catch(a){throw r.error("Message",`编辑消息失败(conversationId=${e}, seq=${t}):`,a),a}}async deleteMessage(e,t){const n=this.client.getHub();try{await n.invoke("DeleteMessage",e,t)}catch(i){throw r.error("Message",`删除消息失败(conversationId=${e}, seq=${t}):`,i),i}}}class G{constructor(e){this.client=e}async getList(e=100){const t=this.client.getHub();try{const n=await t.invoke("GetConversationList",e);return k(n)}catch(n){return r.error("Conversation","获取会话列表失败:",n),[]}}async getOne(e,t=1){const n=this.client.getHub();try{console.log("[ConversationService] getOne 调用, userID:",e,"sessionType:",t);const i=await n.invoke("GetOneConversation",e,t),a=D(i);return console.log("[ConversationService] getOne 结果:",a?{conversationID:a.conversationID,showName:a.showName}:"null"),a}catch(i){return console.error("[ConversationService] getOne 失败:",i),r.error("Conversation","获取会话失败:",i),null}}async joinConversation(e){const t=this.client.getHub();try{console.log("[ConversationService] joinConversation 调用, conversationId:",e),await t.invoke("JoinConversation",e),console.log("[ConversationService] joinConversation 成功, 已加入 conv:"+e+" 组")}catch(n){console.error("[ConversationService] joinConversation 失败:",n),r.error("Conversation","加入会话组失败:",n)}}async leaveConversation(e){const t=this.client.getHub();try{await t.invoke("LeaveConversation",e)}catch(n){r.error("Conversation","离开会话组失败:",n)}}async getHistory(e,t=20,n=""){const i=this.client.getHub();try{const a=n&&parseInt(n,10)||0;console.log("[ConversationService] getHistory 调用, conversationID:",e,"beforeId:",a,"count:",t);const c=await i.invoke("GetHistory",e,a,t),d=(Array.isArray(c)?c:[]).map(m=>y(m)).filter(m=>m!==null);return console.log("[ConversationService] getHistory 返回, 消息数量:",d.length,"(服务端会自动加入 conv:"+e+" 组)"),d.length>0?d:R.parse(c)||[]}catch(a){return console.error("[ConversationService] getHistory 失败:",a),r.error("Conversation","获取历史消息失败:",a),[]}}async markAsRead(e){const t=this.client.getHub();try{await t.invoke("MarkRead",e,0),r.debug("Conversation","标记已读成功:",e)}catch(n){r.error("Conversation","标记已读失败:",n)}}}class K{constructor(e){this.baseURL=e}async uploadFile(e){const t=new FormData;t.append("file",e);const n=await fetch(`${this.baseURL}/api/File/Upload`,{method:"POST",body:t});if(!n.ok)throw new Error(`上传失败: HTTP ${n.status}`);const i=await n.json();if(i.code!==0)throw new Error(i.message||"上传失败");return i.data}getImageDimensions(e){return new Promise((t,n)=>{const i=new Image;i.onload=()=>{t({width:i.width,height:i.height}),URL.revokeObjectURL(i.src)},i.onerror=()=>{n(new Error("无法读取图片尺寸")),URL.revokeObjectURL(i.src)},i.src=URL.createObjectURL(e)})}}class W{constructor(e){var t,n,i,a,c,u,d;this.hub=null,this.currentUserId="",this.currentToken="",this.currentConversationId="",this.hubListenersRegistered=!1,this.heartbeatTimer=null,this._connectionStatus=g.DISCONNECTED,this._isManualClose=!1,this.config=e,_(e.logLevel??2),this.events=new N,this.auth=new F(e.baseURL),this.session=new B,this.tokenRefresher=new x(this.auth,this),this.message=new V(this),this.conversation=new G(this),this.upload=new K(e.baseURL),this.reconnectManager=new U({maxRetries:(t=e.reconnect)==null?void 0:t.maxRetries,baseDelay:(n=e.reconnect)==null?void 0:n.baseDelay,maxDelay:(i=e.reconnect)==null?void 0:i.maxDelay,jitterFactor:(a=e.reconnect)==null?void 0:a.jitterFactor},m=>this.handleReconnectAttempt(m),()=>this.handleReconnectFinalFail()),this.reconnectManager.on("reconnecting",m=>{this.setConnectionStatus(g.RECONNECTING),this.events.emit("connection:reconnecting",{attempt:m})}),this.reconnectManager.on("reconnected",()=>{this.setConnectionStatus(g.CONNECTED),this.circuitBreaker.recordSuccess()}),this.circuitBreaker=new H({failureThreshold:(c=e.circuitBreaker)==null?void 0:c.failureThreshold,resetTimeout:(u=e.circuitBreaker)==null?void 0:u.resetTimeout,successThreshold:(d=e.circuitBreaker)==null?void 0:d.successThreshold}),this.circuitBreaker.onStateChanged(({from:m,to:T})=>{r.info("Realtime",`熔断器状态变化: ${m} -> ${T}`),T===M.Open?(this.events.emit("connection:circuit-open",void 0),this.reconnectManager.isRetrying&&this.reconnectManager.stop(),this.setConnectionStatus(g.FAILED)):T===M.HalfOpen&&(this.events.emit("connection:circuit-half-open",void 0),!this._isManualClose&&this._connectionStatus!==g.CONNECTED&&this.startReconnect())})}get connectionStatus(){return this._connectionStatus}setConnectionStatus(e){this._connectionStatus!==e&&(this._connectionStatus=e,r.debug("Realtime",`连接状态变化: ${this._connectionStatus} -> ${e}`))}getHub(){if(!this.hub)throw new Error("Hub 未初始化,请先调用 login");return this.hub}getConversationId(){return this.currentConversationId}setConversationId(e){this.currentConversationId=e}getConfig(){return this.config}updateRealtimeAddrs(e){e&&(this.config.wsAddr=e),r.debug("Realtime",`地址已更新: wsAddr=${e}`)}async isConnected(){return this.hub?this.hub.state===v.HubConnectionState.Connected:!1}async login(e,t){if(!this.config.wsAddr)throw new Error("Realtime 服务器地址未配置,请先调用 updateRealtimeAddrs 或在初始化时传入 wsAddr");if(!this.circuitBreaker.canAttempt()){const i=new Error("熔断器已打开,暂时无法连接,请稍后再试");throw r.error("Realtime",i.message),this.setConnectionStatus(g.FAILED),this.events.emit("connection:failed",i),i}if(this.setConnectionStatus(g.CONNECTING),this.events.emit("connection:connecting",void 0),this._isManualClose=!1,this.hub&&this.hub.state===v.HubConnectionState.Connected){if(this.currentUserId===e&&this.currentToken===t)return r.debug("Realtime","已连接且凭证未变,跳过重复登录"),this.setConnectionStatus(g.CONNECTED),this.circuitBreaker.recordSuccess(),!0;r.debug("Realtime","检测到新凭证,重新连接");try{await this.hub.stop()}catch(i){r.warn("Realtime","重新连接前停止失败(忽略):",i)}}this.reconnectManager.isRetrying&&this.reconnectManager.stop();const n=this.config.wsAddr.replace(/^ws:\/\//,"http://").replace(/^wss:\/\//,"https://");this.hub=new v.HubConnectionBuilder().withUrl(n,{accessTokenFactory:()=>t}).configureLogging(v.LogLevel.Information).build(),this.currentUserId=e,this.currentToken=t,this.hubListenersRegistered=!1,this.setupHubListeners(this.hub);try{return await this.hub.start(),console.log("[RealtimeClient] SignalR 连接成功, connectionId:",this.hub.connectionId),r.info("Realtime","SignalR 连接成功"),this.setConnectionStatus(g.CONNECTED),this.circuitBreaker.recordSuccess(),this.reconnectManager.reset(),this.events.emit("connection:success",void 0),this.events.emit("sync:finished",void 0),this.startHeartbeat(),!0}catch(i){throw r.error("Realtime","SignalR 连接失败:",i),this.circuitBreaker.recordFailure(),this.events.emit("connection:failed",i),this.events.emit("sync:failed",i),this.circuitBreaker.canAttempt()?this.startReconnect():this.setConnectionStatus(g.FAILED),i}}async logout(){if(this.currentUserId){this._isManualClose=!0,this.stopHeartbeat(),this.reconnectManager.stop();try{this.hub&&await this.hub.stop(),r.info("Realtime","登出成功")}catch(e){r.error("Realtime","登出失败:",e)}finally{this.setConnectionStatus(g.DISCONNECTED),this.currentUserId="",this.currentToken="",this.currentConversationId=""}}}getCurrentUserId(){return this.currentUserId}startHeartbeat(){this.stopHeartbeat();const e=this.config.heartbeatInterval??15e3;this.heartbeatTimer=setInterval(()=>{this.hub&&this.hub.state===v.HubConnectionState.Connected&&this.hub.invoke("Ping").catch(t=>{r.warn("Realtime","心跳发送失败:",t)})},e)}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}startReconnect(){if(this._isManualClose){r.debug("Realtime","主动关闭,不触发重连");return}if(this.reconnectManager.isRetrying){r.debug("Realtime","重连已在进行中");return}if(!this.circuitBreaker.canAttempt()){r.warn("Realtime","熔断器已打开,不启动重连"),this.setConnectionStatus(g.FAILED);return}r.info("Realtime","启动重连流程"),this.reconnectManager.start()}async handleReconnectAttempt(e){if(!this.hub||this._isManualClose)throw new Error("连接已被主动关闭,取消重连");if(!this.circuitBreaker.canAttempt())throw new Error("熔断器已打开,取消重连");r.info("Realtime",`第 ${e} 次重连尝试...`);try{await this.hub.start(),r.info("Realtime",`第 ${e} 次重连成功`),this._isManualClose=!1,this.setConnectionStatus(g.CONNECTED),this.events.emit("connection:success",void 0),this.events.emit("sync:finished",void 0),this.startHeartbeat()}catch(t){throw this.circuitBreaker.recordFailure(),t}}handleReconnectFinalFail(){r.error("Realtime","重连次数耗尽,连接失败"),this.setConnectionStatus(g.FAILED);const e=new Error("重连次数耗尽,无法建立连接");this.events.emit("connection:failed",e),this.events.emit("sync:failed",e)}async getUserInfo(){if(!this.hub||this.hub.state!==v.HubConnectionState.Connected)return null;try{const e=await this.hub.invoke("GetUserInfo",this.currentUserId);return O(typeof e=="string"?JSON.parse(e):e)}catch(e){return r.error("Realtime","获取用户信息失败:",e),null}}on(e,t){this.events.on(e,t)}off(e,t){this.events.off(e,t)}destroy(){if(this._isManualClose=!0,this.hub)try{this.hub.stop()}catch{}this.stopHeartbeat(),this.reconnectManager.stop(),this.events.clear(),this.tokenRefresher.stop(),this.currentUserId="",this.currentToken="",this.currentConversationId="",this.hubListenersRegistered=!1,this.hub=null,this.setConnectionStatus(g.DISCONNECTED)}setupHubListeners(e){if(this.hubListenersRegistered){console.warn("[RealtimeClient] setupHubListeners 跳过: hubListenersRegistered 已为 true,新 hub 实例未注册监听器!");return}this.hubListenersRegistered=!0,console.log("[RealtimeClient] setupHubListeners 开始注册 hub 事件监听器 (OnMessage, OnReadReceipt, etc.)"),e.onclose(t=>{if(console.log("[RealtimeClient] onclose 触发, error:",(t==null?void 0:t.message)||"无","isManualClose:",this._isManualClose),this.stopHeartbeat(),this._isManualClose){r.info("Realtime","连接已主动关闭"),this.setConnectionStatus(g.DISCONNECTED);return}t?(r.error("Realtime","连接异常断开:",t),this.circuitBreaker.recordFailure(),this.events.emit("connection:failed",t),this.circuitBreaker.canAttempt()?this.startReconnect():this.setConnectionStatus(g.FAILED)):(r.info("Realtime","连接正常关闭"),this.setConnectionStatus(g.DISCONNECTED))}),e.on("OnMessage",t=>{console.log("[RealtimeClient] OnMessage 收到原始数据:",t);const n=this.parseMessage(t);if(!n){console.warn("[RealtimeClient] OnMessage 解析消息失败,data:",t);return}console.log("[RealtimeClient] OnMessage 解析成功:",{clientMsgID:n.clientMsgID,sendID:n.sendID,contentType:n.contentType,content:n.content,conversationID:n.conversationID}),this.events.emit("message:received:raw",n);const i=A(n),a=j(n);!i||a?(console.log("[RealtimeClient] 发出 message:received 事件, isSys:",i,"isDisplayable:",a),this.events.emit("message:received",n)):console.log("[RealtimeClient] 消息被过滤(系统消息且不可显示), contentType:",n.contentType)}),e.on("OnReadReceipt",t=>{const n=this.parseData(t);this.events.emit("message:read-receipt",Array.isArray(n)?n:n?[n]:[])}),e.on("OnDeliveryReceipt",t=>{const n=this.parseData(t);n&&n.conversationId!==void 0&&this.events.emit("message:delivered",n)}),e.on("OnMessageWithdrawn",t=>{const n=this.parseData(t);if(!n)return;const i=n.conversationId??n.ConversationId??"",a=n.seq??n.Seq??0,c=n.message?y(n.message)??void 0:void 0,u={conversationId:i,seq:a,message:c};this.events.emit("message:withdrawn",u)}),e.on("OnMessageEdited",t=>{const n=this.parseMessage(t);n&&this.events.emit("message:edited",n)}),e.on("OnMessageDeleted",t=>{const n=this.parseData(t);if(!n)return;const i=n.conversationId??n.ConversationId??"",a=n.seq??n.Seq??0,c={conversationId:i,seq:a};this.events.emit("message:deleted",c)}),e.on("OnConversationChanged",t=>{const n=Array.isArray(t)?t:t?[t]:[],i=k(n);i.length>0&&this.events.emit("conversation:changed",i)}),e.on("OnKicked",()=>{r.warn("Realtime","被踢下线"),this._isManualClose=!0,this.reconnectManager.stop(),this.stopHeartbeat(),this.setConnectionStatus(g.DISCONNECTED),this.events.emit("kicked:offline",void 0)}),e.on("OnTokenExpired",()=>{r.warn("Realtime","Token 已过期"),this.events.emit("token:expired",void 0)}),e.on("OnNewConversation",t=>{const n=this.parseData(t);if(!n)return;const i=n.conversationId??n.ConversationId??"",a=n.visitorName??n.VisitorName??"";r.info("Realtime","收到新会话通知:",i,a),this.events.emit("conversation:new",{conversationId:i,visitorName:a})})}parseMessage(e){if(!e)return null;if(typeof e=="string")try{return y(JSON.parse(e))}catch{return null}return y(e)}parseData(e){if(typeof e=="string")try{return JSON.parse(e)}catch{return e}return e}}const oe={Web:0,iOS:1,Android:2,Windows:3,MacOS:4,Linux:5},ce={Anonymous:0,Authenticated:1,VIP:2},he={Offline:0,Online:1,Busy:2,Away:3};function le(s){return new W(s)}o.AuthService=F,o.ChatUserType=ce,o.CircuitBreaker=H,o.CircuitState=M,o.ConnectionStatus=g,o.ConversationService=G,o.EventManager=N,o.KefuOnlineStatus=he,o.MessageParser=R,o.MessageService=V,o.MessageStatus=Q,o.MessageType=h,o.MessageTypeRegistry=J,o.PlatformType=oe,o.RealtimeClient=W,o.ReconnectManager=U,o.SYSTEM_MESSAGE_TYPES=E,o.SessionManager=B,o.SystemMsg=f,o.TokenRefresher=x,o.UploadService=K,o.classifyMessage=re,o.collectDeviceInfo=$,o.createRealtimeCore=le,o.getClientId=P,o.getLogLevel=z,o.getMessagePreview=se,o.getSystemMessageLabel=te,o.getSystemMessageType=S,o.hasClientId=q,o.isDisplayableSystemMessage=j,o.isSystemMessage=A,o.logger=r,o.mapToConversation=D,o.mapToConversationList=k,o.mapToMessage=y,o.mapToUserInfo=O,o.resetClientId=Y,o.setLogLevel=_,Object.defineProperty(o,Symbol.toStringTag,{value:"Module"})}));
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@saiboniuma/realtime-core",
3
+ "version": "0.1.0",
4
+ "description": "Universal realtime core library for chat messaging, auth, and session management (SignalR)",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "saibotianlong",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/make33136671/sb-framework-chat-js.git",
11
+ "directory": "packages/realtime-core"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public",
15
+ "registry": "https://registry.npmjs.org/"
16
+ },
17
+ "keywords": [
18
+ "realtime",
19
+ "chat",
20
+ "messaging",
21
+ "signalr",
22
+ "customer-service"
23
+ ],
24
+ "sideEffects": false,
25
+ "main": "./dist/realtime-core.umd.cjs",
26
+ "module": "./dist/realtime-core.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/realtime-core.js",
32
+ "require": "./dist/realtime-core.umd.cjs"
33
+ }
34
+ },
35
+ "files": [
36
+ "dist"
37
+ ],
38
+ "scripts": {
39
+ "build": "vite build",
40
+ "type-check": "tsc --noEmit"
41
+ },
42
+ "dependencies": {
43
+ "@microsoft/signalr": "^8.0.7"
44
+ },
45
+ "devDependencies": {
46
+ "typescript": "~5.6.3",
47
+ "vite": "^6.2.0",
48
+ "vite-plugin-dts": "^4.5.0"
49
+ }
50
+ }