@typexim/agent-js-sdk 1.0.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.
- package/README.md +22 -0
- package/dist/index.es.js +1 -0
- package/dist/index.js +1 -0
- package/dist/types/index.d.ts +464 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Typex Agent Web SDK
|
|
2
|
+
|
|
3
|
+
## 安装
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
yarn add @typexim/agent-js-sdk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
# 使用
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import ChatSdk from '@typexim/agent-js-sdk';
|
|
13
|
+
|
|
14
|
+
const chatSdk = ChatSdk.create('[appid]', {
|
|
15
|
+
httpHost: '/api',
|
|
16
|
+
websocketHost: 'wss://w.typexim.com',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
sdk.setLogLevel(0);
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
详细接口文档,[点击查看](https://ljwksd8dbi1.sg.larksuite.com/wiki/NUxvwWVsliZKoUkgtRKlOyxTgjg)
|
package/dist/index.es.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"crypto-js";import t from"axios";import s from"@fingerprintjs/fingerprintjs";import r from"js-cookie";var i,n,o,a,h,c,d,E,g,l;function u(e,t,s){return{ok:!1,code:e||E.SDK_ERROR,msg:t||"sdk error",msg_i18n:s}}function _(e){return{ok:!0,data:e}}!function(e){e[e.KICK_OFF=4001]="KICK_OFF"}(i||(i={})),function(e){e[e.CREATE_FEED=0]="CREATE_FEED",e[e.UPDATE_FEED=1]="UPDATE_FEED",e[e.ADD_MESSAGE=2]="ADD_MESSAGE",e[e.UPDATE_MESSAGE=3]="UPDATE_MESSAGE",e[e.UPDATE_USER_DATA=4]="UPDATE_USER_DATA",e[e.USER_LOGIN=5]="USER_LOGIN",e[e.READ_MESSAGE=6]="READ_MESSAGE",e[e.PONG_MESSAGE=8]="PONG_MESSAGE",e[e.DELETE_MESSAGE=9]="DELETE_MESSAGE",e[e.UPDATE_GROUP=12]="UPDATE_GROUP",e[e.DELETE_GROUP=13]="DELETE_GROUP",e[e.UPDATE_LOGIN_USER=17]="UPDATE_LOGIN_USER",e[e.UPDATE_MESSAGE_REACTION=19]="UPDATE_MESSAGE_REACTION",e[e.ADD_GROUP_MEMBER=21]="ADD_GROUP_MEMBER",e[e.DEL_GROUP_MEMBER=22]="DEL_GROUP_MEMBER",e[e.ADD_CONTACTS=23]="ADD_CONTACTS",e[e.UPDATE_CONTACTS=24]="UPDATE_CONTACTS",e[e.DEL_CONTACTS=25]="DEL_CONTACTS",e[e.BLOCK_USER=26]="BLOCK_USER",e[e.DEVICE_CODE=27]="DEVICE_CODE",e[e.MSG_OPEN_CLOCK=28]="MSG_OPEN_CLOCK",e[e.DELETE_SECRET_CHAT=29]="DELETE_SECRET_CHAT",e[e.DELETE_FEED=30]="DELETE_FEED",e[e.FAVORITE_UPDATE=31]="FAVORITE_UPDATE",e[e.STICKERS_UPDATE=32]="STICKERS_UPDATE",e[e.CUSTOM_STICKERS_UPDATE=33]="CUSTOM_STICKERS_UPDATE",e[e.MANAGE_DEVICE_UPDATE=34]="MANAGE_DEVICE_UPDATE",e[e.MASS_MESSAGE_UPDATE=35]="MASS_MESSAGE_UPDATE",e[e.CHAT_WALLPAPER_DELETE=36]="CHAT_WALLPAPER_DELETE",e[e.CHAT_WALLPAPER_ADD=37]="CHAT_WALLPAPER_ADD",e[e.CONTACT_REQUEST=40]="CONTACT_REQUEST",e[e.USER_PREFERENCE_CHANGE=38]="USER_PREFERENCE_CHANGE",e[e.UPDATE_GROUP_PIN=41]="UPDATE_GROUP_PIN",e[e.RTC_CANCEL=46]="RTC_CANCEL",e[e.ADD_FEED_FOLDER=43]="ADD_FEED_FOLDER",e[e.UPDATE_FEED_FOLDER=44]="UPDATE_FEED_FOLDER",e[e.UPDATE_FOLDER_FEED_LIST=47]="UPDATE_FOLDER_FEED_LIST",e[e.DELETE_FEED_FOLDER=45]="DELETE_FEED_FOLDER",e[e.UPDATE_CHATTER=42]="UPDATE_CHATTER",e[e.PAPER_KEY=48]="PAPER_KEY",e[e.PAPER_KEY_LOG=49]="PAPER_KEY_LOG",e[e.USER_LOGIN_SCAN=50]="USER_LOGIN_SCAN",e[e.RTC_RINGING=51]="RTC_RINGING",e[e.ON_UPDATE_PRIVATE_SETTING=52]="ON_UPDATE_PRIVATE_SETTING",e[e.ON_DELETE_PRIVATE_SETTING=53]="ON_DELETE_PRIVATE_SETTING",e[e.ON_UPDATE_LOGIN_PRIVATE_SETTING=54]="ON_UPDATE_LOGIN_PRIVATE_SETTING",e[e.PUSH_LIVE_MESSAGE=56]="PUSH_LIVE_MESSAGE",e[e.BIG_PACKAGE_PUSH=126]="BIG_PACKAGE_PUSH",e[e.KICK_OUT_DEVICE=127]="KICK_OUT_DEVICE",e[e.CLEAR_HISTORY=128]="CLEAR_HISTORY",e[e.CLEAR_DATA=255]="CLEAR_DATA"}(n||(n={})),function(e){e[e.KICK_OUT=0]="KICK_OUT"}(o||(o={})),function(e){e[e.singleChat=1]="singleChat",e[e.groupChat=2]="groupChat"}(a||(a={})),function(e){e[e.normal=0]="normal",e[e.system=1]="system"}(h||(h={})),function(e){e[e.text=0]="text",e[e.image=1]="image",e[e.audio=3]="audio",e[e.video=4]="video",e[e.file=5]="file",e[e.mergeForward=6]="mergeForward",e[e.sticker=7]="sticker",e[e.richText=8]="richText",e[e.contact=9]="contact",e[e.communication=10]="communication",e[e.mixedImageText=11]="mixedImageText",e[e.luckyGift=14]="luckyGift",e[e.fileGroup=15]="fileGroup",e[e.cardMsg=16]="cardMsg"}(c||(c={})),function(e){e[e.normal=0]="normal",e[e.recall=1]="recall"}(d||(d={})),function(e){e[e.NETWORK_ERROR=-1e4]="NETWORK_ERROR",e[e.SDK_ERROR=-10001]="SDK_ERROR"}(E||(E={})),function(e){e.SDK_READY="SDK_READY",e.SDK_NOT_READY="SDK_NOT_READY",e.SDK_KICKOUT="SDK_KICKOUT",e.FEED_UPDATED="FEED_UPDATED",e.MESSAGE_RECEIVED="MESSAGE_RECEIVED",e.MESSAGE_UPDATED="MESSAGE_UPDATED",e.MESSAGE_RECEIPT_RECEIVED="MESSAGE_RECEIPT_RECEIVED"}(g||(g={})),function(e){e[e.up=0]="up",e[e.down=1]="down"}(l||(l={}));const S="[TYPEX_AGENT_JS_SDK]";class p{level=0;setLogLevel(e){this.level=e}getLogLevel(){return this.level}shouldPrint(e){return e>=this.level}debug(...e){this.shouldPrint(0)&&console.debug(`${S}[DEBUG]`,...e)}info(...e){this.shouldPrint(0)&&console.info(`${S}[INFO]`,...e)}warn(...e){this.shouldPrint(1)&&console.warn(`${S}[WARN]`,...e)}error(...e){this.shouldPrint(2)&&console.error(`${S}[ERROR]`,...e)}}function m(e){return(new TextEncoder).encode(e).buffer}async function R(t,s){return window.crypto&&window.crypto.subtle?await async function(e,t){const s=m(t),r=m(e),i=await crypto.subtle.importKey("raw",s,{name:"HMAC",hash:{name:"SHA-256"}},!1,["sign"]),n=await crypto.subtle.sign("HMAC",i,r);return Array.from(new Uint8Array(n)).map(e=>e.toString(16).padStart(2,"0")).join("")}(t,s):function(t,s){const r=e.HmacSHA256(t,s);return e.enc.Hex.stringify(r)}(t,s)}const T=e=>{if(null==e)return"";if("object"!=typeof e||Array.isArray(e)){if(Array.isArray(e)){let t="";for(const s of e)t+=T(s);return t}return"string"==typeof e?e:String(e)}{const t=Object.keys(e).sort();let s="";for(const r of t)s+=T(e[r]);return s}};class C{instance;developer;logger;store;constructor(e,s){this.logger=s.logger,this.store=s.store,this.developer=e.developer;const r=e.baseURL.replace(/\/$/,"");this.instance=t.create({baseURL:r,headers:{...e.defaultHeaders??{}},timeout:e.timeout??3e4}),this.setupInterceptors()}setupInterceptors(){this.instance.interceptors.request.use(e=>(e.headers=e.headers||{},e.headers["Content-Type"]||(e.headers["Content-Type"]="application/json"),e.headers.Platform="web",e.headers.AppID=this.store.appId,e.url?.startsWith("/open")&&(e.headers.Authorization=`Bearer ${this.store.token}`),this.developer&&!e.ignoreDeveloper&&(e.headers["x-developer"]=this.developer),e))}async get(e,t={},s={}){return this.request({url:e,method:"GET",params:t,...s})}async post(e,t,s={}){return this.request({url:e,method:"POST",data:t,...s})}async request(e){try{const{needSign:t,...s}=e;if(t&&"POST"===s.method?.toUpperCase()&&s.data){const t=this.store.currentUser;if(!t){const e="[HttpClient] HttpClient needs make sign but not found login user";return this.logger.error(e),u(E.SDK_ERROR,e)}try{const r=await(async(e,t)=>{const s=T(e);return await R(s,t)})(s.data,t.secretKey);e.headers||(e.headers={}),e.headers.sign=r}catch(e){const t=`[HttpClient] HttpClient make sign error: ${e.message}`;return this.logger.error(t),u(E.SDK_ERROR,t)}}const r=await this.instance.request({...s,headers:{...e.headers||{}}}),i=r.data;return 0!==i.code?(this.logger.error("[HttpClient]","Business server response error:",r),{ok:!1,code:i.code,msg:i.msg,msg_i18n:i.msg_i18n}):{ok:!0,data:i.data}}catch(e){return this.handleError(e)}}handleError(e){return"ECONNABORTED"===e.code?{ok:!1,code:E.NETWORK_ERROR,msg:e.message||"request timeout"}:{ok:!1,code:E.NETWORK_ERROR,msg:e.message||"network error"}}}class I{listeners=new Map;logger;constructor(e){this.logger=e}on(e,t){this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t)}once(e,t){const s=r=>{t(r),this.off(e,s)};this.on(e,s)}off(e,t){this.listeners.has(e)&&(t?this.listeners.get(e).delete(t):this.listeners.delete(e))}emit(e,t){if(this.listeners.has(e))for(const s of this.listeners.get(e))try{s(t)}catch(t){this.logger.error(`[EventCore] handler error on "${String(e)}":`,t)}}clear(){this.listeners.clear()}}var A,D,O,N;!function(e){e.INIT="INIT",e.CONNECTING="CONNECTING",e.CONNECTED="CONNECTED",e.DISCONNECTED="DISCONNECTED",e.CLOSED="CLOSED"}(A||(A={})),function(e){e.OPEN="OPEN",e.ERROR="ERROR",e.CLOSE="CLOSE",e.MESSAGE="MESSAGE",e.STATUS_CHANGE="STATUS_CHANGE"}(D||(D={}));class b extends I{ws;options;status=A.INIT;constructor(e,t){super(t),this.options=e}connect(){if(this.status===A.CONNECTING||this.status===A.CONNECTED)return void this.logger.debug("[CONNECTION]","WebSocket is connecting or connected,current connect status is",`[${this.status}]`);this.logger.debug("[CONNECTION][connect]","WebSocket is connecting"),this.setStatus(A.CONNECTING);const e=new WebSocket(`${this.options.url}?token=${this.options.token}&platform=web`);this.ws=e,e.onopen=()=>{this.logger.debug("[CONNECTION][ws.onopen]","WebSocket is connected"),this.setStatus(A.CONNECTED),this.emit(D.OPEN)},e.onmessage=e=>{this.logger.debug("[CONNECTION][ws.onmessage]","WebSocket received a message"),this.emit(D.MESSAGE,e.data)},e.onerror=e=>{this.logger.error("[CONNECTION][ws.onerror]","WebSocket connect error"),this.emit(D.ERROR,e)},e.onclose=e=>{this.logger.debug("[CONNECTION][ws.onclose]","WebSocket connect be closed"),this.setStatus(A.DISCONNECTED),this.emit(D.CLOSE,e)}}close(){this.ws?.close(),this.ws=void 0,this.setStatus(A.CLOSED)}send(e){if(this.status!==A.CONNECTED)throw new Error("WebSocket is not connected");this.ws.send(e)}getStatus(){return this.status}setStatus(e){this.status!==e&&(this.status=e,this.emit(D.STATUS_CHANGE,e))}}class f{connection;pingTimeout;pongTimeout;pingTimer;pongTimer;logger;constructor(e,t,s={}){this.connection=e,this.pingTimeout=s.pingTimeout??3e4,this.pongTimeout=s.pongTimeout??2e4,this.logger=t}ping(e){this.logger.debug("[Heartbeat]","Heartbeat ping"),this.stop(),this.pingTimer=window.setTimeout(()=>{try{this.connection.send(JSON.stringify({type:0})),this.logger.debug("[Heartbeat]","Heartbeat send PING"),this.pong(e)}catch{}},this.pingTimeout)}pong(e){clearTimeout(this.pongTimer),this.pongTimer=window.setTimeout(()=>{e?.(),this.logger.debug("[Heartbeat]","Heartbeat called PONG callback")},this.pongTimeout)}stop(){this.logger.debug("[Heartbeat]","Heartbeat stop"),clearTimeout(this.pingTimer),clearTimeout(this.pongTimer),this.pingTimer=void 0,this.pongTimer=void 0}}class P{connection;baseInterval=1e3;maxAttempts=1/0;attempts=0;timer;enabled=!1;logger;constructor(e,t,s={}){this.logger=t,this.connection=e,s.baseInterval&&(this.baseInterval=s.baseInterval),s.maxAttempts&&(this.maxAttempts=s.maxAttempts),this.bindEvents()}bindEvents(){this.connection.on(D.CLOSE,()=>{this.enabled&&this.scheduleReconnect()}),this.connection.on(D.OPEN,()=>{this.reset()})}start(){this.logger.debug("[Reconnector]","Reconnector start"),this.enabled=!0}stop(){this.logger.debug("[Reconnector]","Reconnector stop"),this.enabled=!1,this.clearTimer()}scheduleReconnect(){if(this.logger.debug("[Reconnector]",`Reconnector scheduleReconnect, attempts: ${this.attempts}, maxAttempts: ${this.maxAttempts}`),this.attempts>=this.maxAttempts)return;this.attempts++;const e=this.baseInterval*Math.min(this.attempts,10);this.clearTimer(),this.timer=window.setTimeout(()=>{this.connection.connect()},e)}reset(){this.logger.debug("[Reconnector]","Reconnector reset"),this.attempts=0,this.clearTimer()}clearTimer(){this.timer&&(clearTimeout(this.timer),this.timer=void 0)}}!function(e){e.OPEN="OPEN",e.ERROR="ERROR",e.CLOSE="CLOSE",e.MESSAGE="MESSAGE",e.STATUS_CHANGE="STATUS_CHANGE",e.KICK_OUT="KICK_OUT"}(O||(O={}));class U extends I{connection;reconnector;heartbeat;constructor(e,t){super(t),this.connection=new b({url:e.wsUrl,token:e.token},t),this.reconnector=new P(this.connection,t),this.heartbeat=new f(this.connection,t),this.bindEvents()}connect(){this.reconnector.start(),this.connection.connect()}disconnect(){this.reconnector.stop(),this.heartbeat.stop(),this.connection.close()}startPing(){this.heartbeat.ping(()=>{this.disconnect();const e=setTimeout(()=>{this.connect(),clearTimeout(e)},1e4)})}bindEvents(){this.connection.on(D.OPEN,()=>{this.emit(O.OPEN),this.startPing()}),this.connection.on(D.CLOSE,e=>{if(e.code===i.KICK_OFF)return this.logger.warn("[IMClient][KICK_OUT]","You has been kicked off"),this.emit(O.KICK_OUT),void this.disconnect();this.emit(O.CLOSE),this.connect()}),this.connection.on(D.MESSAGE,e=>{this.handleRawMessage(e)}),this.connection.on(D.STATUS_CHANGE,e=>{this.emit(O.STATUS_CHANGE,e)}),this.connection.on(D.ERROR,e=>{this.emit(O.ERROR,e)})}handleRawMessage(e){try{const t=JSON.parse(e);if(t.type===n.PONG_MESSAGE?this.startPing():this.sendAck(t.id),t.type===n.BIG_PACKAGE_PUSH)for(const e of t.content.big_push)this.emit(O.MESSAGE,e);else this.emit(O.MESSAGE,t)}catch(e){this.logger.error("[IMClient][handleRawMessage]","Message raw data json.parse error",e),this.emit(O.ERROR,e)}}sendAck(e){this.connection.send(JSON.stringify({type:1,ids:[e]}))}}!function(e){e.APP_ID="appId",e.TOKEN="token",e.CURRENT_USER="currentUser",e.CURRENT_FEED="currentFeed",e.SDK_READY="sdkReady",e.IS_CONNECTED="isConnected",e.DEVICE_ID="typeximDeviceId",e.BROWSER_FINGERPRINT="typeximBrowserFingerprint"}(N||(N={}));class y extends I{state={};constructor(e){super(e)}set(e,t){this.logger.debug("[Store] set:",e,t),this.state[e]=t,this.emit("change",{key:e,value:t})}get(e){return this.logger.debug("[Store] get:",e,this.state[e]),this.state[e]}remove(e){this.logger.debug("[Store] remove:",e,this.state[e]),delete this.state[e],this.emit("change",{key:e,value:void 0})}clear(){this.logger.debug("[Store] clear",this.state),this.state={appId:this.state.appId,token:this.state.token,typeximBrowserFingerprint:this.state.typeximBrowserFingerprint},this.emit("clear",void 0)}setWithLocal(e,t){this.logger.debug("[Store] setWithLocal:",e,t),this.set(e,t),localStorage.setItem(e,JSON.stringify(t))}getWithLocal(e){if(this.logger.debug("[Store] getWithLocal:",e,this.state[e]),this.state[e])return this.state[e];const t=localStorage.getItem(e);return t?JSON.parse(t):void 0}removeWithLocal(e){this.logger.debug("[Store] removeWithLocal:",e,this.state[e]),localStorage.removeItem(e),this.remove(e)}onChange(e){this.on("change",e)}offChange(e){this.off("change",e)}onClear(e){this.on("clear",e)}get appId(){return this.logger.debug("[Store] get appId",this.state.appId),this.state.appId}get ready(){return this.logger.debug("[Store] get ready",this.state.sdkReady),!!this.state.sdkReady}get deviceId(){const e=this.getWithLocal(N.DEVICE_ID);return this.logger.debug("[Store] get deviceId",e),e}get currentUser(){return this.logger.debug("[Store] get currentUser",this.state.currentUser),this.state.currentUser}get currentChatId(){return this.logger.debug("[Store] get currentChatId",this.state.currentFeed?.chat_id),this.state.currentFeed?.chat_id}get currentFeed(){return this.logger.debug("[Store] get currentFeed",this.state.currentFeed),this.state.currentFeed}get sessionId(){return this.logger.debug("[Store] get sessionId",this.state.currentUser?.sessionId),this.state.currentUser?.sessionId}get token(){return this.logger.debug("[Store] get token",this.state.token),this.state.token}get browserFingerprint(){const e=this.getWithLocal(N.BROWSER_FINGERPRINT);return this.logger.debug("[Store] get browserFingerprint",e),e}}class w{version;constructor(){this.version=0}increment(){this.version++}get currentVersion(){return this.version}check(e){return e===this.version}}const M="@typexim/agent-js-sdk_broadcast_channel";class L{config;logger;http;imClient=null;store;events;version;loadFeedAbortController=null;tabId;constructor(e,t,s){this.logger=new p,this.store=new y(this.logger),this.events=new I(this.logger),this.store.set(N.APP_ID,e),this.store.set(N.TOKEN,t),this.config=s,this.version=new w,this.http=new C({baseURL:s.httpHost,developer:s.developer},{logger:this.logger,store:this.store}),this.tabId=`${e}_${Date.now()}_${Math.random().toString(36).substring(2,15)}`,this.generateBrowserFingerprint(),this.listenBroadcastChannel()}setLogLevel(e){this.logger.setLogLevel(e)}async login(e){try{const t={...e||{}};if(t.userId&&!t.busiToken)return u(E.SDK_ERROR,"Invalid param: userId and busiToken must be provided together");if(!t.userId){const e=this.store.browserFingerprint||await this.generateBrowserFingerprint();t.userId=e}const s=await this.innerRegister(t);if(!s.ok||!s.data)return this.logger.error("[InternalChatSdk] innerRegister failed",s),u(s.code,s.msg,s.msg_i18n);const i=s.data,n=await this.requestDeviceId(),o=await this.innerLogin(i,n,t);if(!o.ok||!o.data)return this.logger.error("[InternalChatSdk] innerLogin failed",o),u(o.code,o.msg,o.msg_i18n);const a=r.get("sessionid");if(!a)throw new Error("Login failed, not found sessionid");const h=o.data||{};return h.sessionId=a,h.secretKey=o.data.key,await Promise.all([this.syncLoginState(h),this.connectWebSocket(a)]),this.notifyOtherTabsLogin(),_({id:h.id,userId:t.userId,name:h.name,avatar:h.avatar,email:h.email})}catch(e){return this.handleException(e)}}innerRegister(e){return this.http.post("/open/user/register",{userID:e.userId,name:e.name,avatar:e.avatar,email:e.email,passWord:e.password,userBuf:e.userBuf,busiToken:e.busiToken})}innerLogin(e,t,s){return this.http.post("/open/user/login",{uid:e,busi_token:s.busiToken,device_id:t})}async generateBrowserFingerprint(){try{const e=this.store.getWithLocal(N.BROWSER_FINGERPRINT);if(e)return this.logger.debug("[ChatSdk] generateBrowserFingerprint success from localstorage",e),e;const t=await s.load(),r=await t.get();return this.logger.debug("[ChatSdk] generateBrowserFingerprint success",r.visitorId),this.store.setWithLocal(N.BROWSER_FINGERPRINT,r.visitorId),r.visitorId}catch(e){this.logger.info("[ChatSdk] FingerprintJS generateBrowserFingerprint Error",e);const t=`${Date.now()}_${Math.random().toString(36).substring(2,15)}`;return this.logger.debug("[ChatSdk] random generateBrowserFingerprint success",t),this.store.setWithLocal(N.BROWSER_FINGERPRINT,t),t}}async loadFeed(){const e=this.version.currentVersion;this.loadFeedAbortController&&this.loadFeedAbortController.abort(),this.loadFeedAbortController=new AbortController;const t=await this.http.get("/feed",{limit:10},{signal:this.loadFeedAbortController.signal}).catch(e=>{this.logger.error("[ChatSdk] loadFeed Error",e)});this.loadFeedAbortController=null;const s=this.version.check(e);if(!t||!s)return;const r=t.data?.feed_list?.find(e=>e.chat_type===a.groupChat);t.ok&&(this.store.set(N.CURRENT_FEED,r),this.events.emit(g.FEED_UPDATED,r))}async syncLoginState(e){this.store.set(N.CURRENT_USER,e),this.store.set(N.SDK_READY,!0),await this.loadFeed(),this.events.emit(g.SDK_READY)}syncLogoutState(){this.unlistenBroadcastChannel(),this.store.set(N.CURRENT_USER,void 0),this.store.set(N.SDK_READY,!1),this.store.set(N.CURRENT_FEED,void 0),this.events.emit(g.SDK_NOT_READY)}async requestDeviceId(){const e=this.store.getWithLocal(N.DEVICE_ID);if(e)return this.logger.debug("[ChatSdk] requestDeviceId success from localstorage",e),e;const t=await this.http.post("/device_api/device_id",{platform:"web",hash_str:this.store.browserFingerprint||""}).catch(e=>{this.logger.error("[ChatSdk] requestDeviceId Error",e)});if(t?.ok&&t?.data)return this.logger.debug("[ChatSdk] requestDeviceId success",t.data),this.store.setWithLocal(N.DEVICE_ID,t.data),t.data;if(this.store.browserFingerprint)return this.logger.debug("[ChatSdk] use browserFingerprint as deviceId",this.store.browserFingerprint),this.store.setWithLocal(N.DEVICE_ID,this.store.browserFingerprint),this.store.browserFingerprint;const s=`${Date.now()}_${Math.random().toString(36).substring(2,15)}`;return this.logger.debug("[ChatSdk] random generateDeviceId success",s),this.store.setWithLocal(N.DEVICE_ID,s),s}async logout(){try{if(!this.store.currentUser)return _(null);const e=await this.http.get("/user/logout");return e.ok?(this.syncLogoutState(),this.disconnectWebSocket(),_(null)):u(e.code,e.msg,e.msg_i18n)}catch(e){return this.handleException(e)}}async addGroupMember(e,t){try{if(this.assertReady(),!e)return u(E.SDK_ERROR,"not found chat id");if(!t?.length)return _(null);const s=this.store.currentFeed;if(!s)return u(E.SDK_ERROR,"not found current feed");const r=await this.getChatMembers(s.chat_id,s.chat_member);if(!r.ok)return u(r.code,r.msg,r.msg_i18n);const i=(r.data||[]).map(e=>e.user_id),n=t.filter(e=>!i.includes(e)),o=await this.addMember(n,s.chat_id);return o.ok?_(o.data):u(o.code,o.msg,o.msg_i18n)}catch(e){return this.handleException(e)}}async initialChat(e){try{this.assertReady();const t=this.store.currentFeed;if(!t){const t=await this.createFeed(e.receiverIds);return t.ok&&t.data?(this.store.set(N.CURRENT_FEED,t.data),_(t.data.chat_id)):u(t.code,t.msg,t.msg_i18n)}const s=await this.addGroupMember(t.chat_id,e.receiverIds);return s.ok?_(t.chat_id):s}catch(e){return this.handleException(e)}}async getCurrentFeed(){try{return this.assertReady(),{ok:!0,data:this.store.currentFeed}}catch(e){return this.handleException(e)}}async getMessageList(e){try{this.assertReady();const t=this.store.currentFeed;if(!t)return u(E.SDK_ERROR,"No feed found");const{chat_id:s,first_position:r,last_message_position:i}=t,{positions:n,nextPosition:o,hasMore:a}=(e=>{const t=[];if(e.direction===l.up){let s=e.startPosition??e.maxPosition;for(;t.length<e.limit&&s>=e.minPosition;)t.push(s),s--;const r=s>=e.minPosition;let i;return r&&(i=s),{positions:t,nextPosition:i,hasMore:r}}{let s=e.startPosition??e.minPosition;for(;t.length<e.limit&&s<=e.maxPosition;)t.push(s),s++;const r=s<=e.maxPosition;let i;return r&&(i=s),{positions:t,nextPosition:i,hasMore:r}}})({maxPosition:i,minPosition:r,startPosition:e.messagePosition,limit:e.limit,direction:e.direction??l.up});if(n.length>0){const e=await this.http.post("/message/get_by_position",{chat_position:{[s]:n}});if(e.ok&&e.data?.[s]){return _({hasMore:a,list:e.data[s].reverse().filter(e=>e.message_kind_id!==h.system&&e.operation!==d.recall),nextMessagePosition:o})}return u(e.code,e.msg,e.msg_i18n)}return _({hasMore:!1,list:[]})}catch(e){return this.handleException(e)}}async sendMessage(e){try{this.assertReady();const t=e.messageType;switch(t){case c.text:return this.sendTextMessage(e);case c.image:return this.sendImageMessage(e);case c.video:return this.sendVideoMessage(e);case c.file:return this.sendFileMessage(e);default:return this.logger.warn("Unsupported message type: ",t),u(E.SDK_ERROR,`Unsupported message type: ${t}`)}}catch(e){return this.handleException(e)}}async sendTextMessage(e){return this.http.post("/message/send_text",{chat_id:e.chatId,client_msg_id:e.clientMsgId,text:e.text.trim()},{needSign:!0})}async sendImageMessage(e){return this.http.post("/message/send_image",{chat_id:e.chatId,client_msg_id:e.clientMsgId,object_url:e.objectUrl,width:e.width,height:e.height},{needSign:!0})}async sendVideoMessage(e){return this.http.post("/message/send_video",{chat_id:e.chatId,client_msg_id:e.clientMsgId,video_url:e.objectUrl,duration_second:e.duration,image_url:e.coverUrl,width:e.width,height:e.height},{needSign:!0})}async sendFileMessage(e){return this.http.post("/message/send_file",{chat_id:e.chatId,client_msg_id:e.clientMsgId,object_url:e.objectUrl,file_name:e.fileName,file_size:e.fileSize},{needSign:!0})}async uploadFile(e){const{chatId:t,fileContent:s,fileFormat:r,fileName:i,fileType:n,onUploadProgress:o}=e,a=new FormData;return a.append("file_content",s),a.append("file_type",n),a.append("file_name",i),a.append("file_format",r),a.append("chat_id",t),this.http.post("/chat/upload",a,{needSign:!1,ignoreDeveloper:!0,onUploadProgress:o,headers:{"Content-Type":"multipart/form-data"}})}async sendMessageReadReceipt(e){try{return this.assertReady(),e.messageIds?.length?this.http.post("/message/read",{chat_id:e.chatId,message_ids:e.messageIds},{needSign:!0}):_(null)}catch(e){return this.handleException(e)}}async markAllMessagesAsRead(e){const t=this.store.currentFeed?.unread_message_ids||[];return this.sendMessageReadReceipt({chatId:e,messageIds:t})}async recallMessage(e){try{return this.assertReady(),e.messageIds.length?this.http.post("/message/remove",{chat_id:e.chatId,message_ids:e.messageIds,two_way:1},{needSign:!0}):_(null)}catch(e){return this.handleException(e)}}async createFeed(e){return await this.http.post("/chat/group",{receiver_ids:e,group_type:0,name:`User ${this.store.currentUser?.name||this.store.currentUser?.id}`},{needSign:!0})}async getChatMembers(e,t){return this.http.post("/chat/chatter_info",JSON.stringify({chatters:{[e]:t}}))}async addMember(e,t){return t?e.length?this.http.post("/chat/group/add_member",{user_ids:e,chat_id:t},{needSign:!0}):_(null):u(E.SDK_ERROR,"not found chat id")}handlePushMessage(e){switch(this.logger.debug("handlePushMessage",e),e.type){case n.CREATE_FEED:case n.UPDATE_FEED:this.handleFeedUpdate(e);break;case n.ADD_MESSAGE:this.handleAddMessage(e);break;case n.UPDATE_MESSAGE:this.handleUpdateMessage(e);break;case n.READ_MESSAGE:this.handleReadMessage(e);break;case n.KICK_OUT_DEVICE:this.handleKickOut()}}handleFeedUpdate(e){this.store.set(N.CURRENT_FEED,e.content),this.events.emit(g.FEED_UPDATED,e.content)}async handleAddMessage(e){e.content.chat_id===this.store.currentChatId&&(await this.loadFeed(),this.events.emit(g.MESSAGE_RECEIVED,e.content))}async handleUpdateMessage(e){e.content.chat_id===this.store.currentChatId&&(await this.loadFeed(),this.events.emit(g.MESSAGE_UPDATED,e.content))}async handleReadMessage(e){e.content.chat_id===this.store.currentChatId&&(await this.loadFeed(),this.events.emit(g.MESSAGE_RECEIPT_RECEIVED,{chatId:e.content.chat_id,messageIds:e.content.message_ids,readerUserIds:e.content.reader_ids}))}handleKickOut(){this.syncLogoutState(),this.disconnectWebSocket()}on(e,t){return this.events.on(e,t)}once(e,t){return this.events.once(e,t)}off(e,t){return this.events.off(e,t)}get currentUserId(){return this.logger.debug("[InternalChatSdk] get currentUserId==>",this.store.currentUser),this.store.currentUser?.id}get currentChatId(){return this.logger.debug("[InternalChatSdk] get currentChatId==>",this.store.currentChatId),this.store.currentChatId}get isReady(){return this.logger.debug("[InternalChatSdk] get isReady==>",this.store.ready),this.store.ready}listenBroadcastChannel(){new BroadcastChannel(M).onmessage=e=>{const t=this.store.appId;e.data.type===o.KICK_OUT&&e.data.appId===t&&e.data.tabId!==this.tabId&&(this.logger.debug("[BroadcastChannel] message: kick out",{appId:t,sourceTabId:e.data.tabId,currentTabId:this.tabId}),this.handleKickOut())}}unlistenBroadcastChannel(){new BroadcastChannel(M).close()}notifyOtherTabsLogin(){new BroadcastChannel(M).postMessage({type:o.KICK_OUT,appId:this.store.appId,tabId:this.tabId})}async connectWebSocket(e){if(!e)throw Error("connect websocket error, not found login user token");this.imClient=new U({token:e,wsUrl:`${this.config.websocketHost}/ws`},this.logger),this.imClient.on(O.OPEN,()=>{this.store.set(N.IS_CONNECTED,!0)}),this.imClient.on(O.KICK_OUT,()=>{this.store.clear(),this.events.emit(g.SDK_KICKOUT)}),this.imClient.on(O.CLOSE,()=>{this.store.set(N.IS_CONNECTED,!1)}),this.imClient.on(O.MESSAGE,e=>{this.handlePushMessage(e)}),this.imClient.connect()}disconnectWebSocket(){this.imClient&&(this.imClient.disconnect(),this.imClient=null)}assertReady(){if(!this.store.ready)throw{code:E.SDK_ERROR,msg:"sdk not ready"}}handleException(e){return this.logger.error("[ChatSDK Error]",e),{ok:!1,code:e.code||E.SDK_ERROR,msg:e.msg||e.message||"sdk error"}}}const G=new Map;function v(e){return{setLogLevel(t){e.setLogLevel(t)},login:t=>e.login(t),logout:()=>e.logout(),initialChat:t=>e.initialChat(t),addGroupMember:(t,s)=>e.addGroupMember(t,s),getCurrentFeed:()=>e.getCurrentFeed(),getMessageList:t=>e.getMessageList(t),uploadFile:t=>e.uploadFile(t),sendMessage:t=>e.sendMessage(t),sendMessageReadReceipt:t=>e.sendMessageReadReceipt(t),markAllMessagesAsRead:t=>e.markAllMessagesAsRead(t),recallMessage:t=>e.recallMessage(t),get currentUserId(){return e.currentUserId},get currentChatId(){return e.currentChatId},get isReady(){return e.isReady},on:(t,s)=>e.on(t,s),once:(t,s)=>e.once(t,s),off:(t,s)=>e.off(t,s)}}var F={create:function(e,t,s){if(G.has(e))return v(G.get(e));const r=new L(e,t,s);return G.set(e,r),v(r)}};export{E as ChatSdkErrorCode,g as ChatSdkEvent,a as ChatType,h as MessageKind,d as MessageOperation,c as MessageType,l as RequestDirection,F as default};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,"__esModule",{value:!0});var e,t,s,r,i,n,o,a,h,c,d=require("crypto-js"),E=require("axios"),g=require("@fingerprintjs/fingerprintjs"),l=require("js-cookie");function u(e,t,s){return{ok:!1,code:e||exports.ChatSdkErrorCode.SDK_ERROR,msg:t||"sdk error",msg_i18n:s}}function _(e){return{ok:!0,data:e}}!function(e){e[e.KICK_OFF=4001]="KICK_OFF"}(e||(e={})),function(e){e[e.CREATE_FEED=0]="CREATE_FEED",e[e.UPDATE_FEED=1]="UPDATE_FEED",e[e.ADD_MESSAGE=2]="ADD_MESSAGE",e[e.UPDATE_MESSAGE=3]="UPDATE_MESSAGE",e[e.UPDATE_USER_DATA=4]="UPDATE_USER_DATA",e[e.USER_LOGIN=5]="USER_LOGIN",e[e.READ_MESSAGE=6]="READ_MESSAGE",e[e.PONG_MESSAGE=8]="PONG_MESSAGE",e[e.DELETE_MESSAGE=9]="DELETE_MESSAGE",e[e.UPDATE_GROUP=12]="UPDATE_GROUP",e[e.DELETE_GROUP=13]="DELETE_GROUP",e[e.UPDATE_LOGIN_USER=17]="UPDATE_LOGIN_USER",e[e.UPDATE_MESSAGE_REACTION=19]="UPDATE_MESSAGE_REACTION",e[e.ADD_GROUP_MEMBER=21]="ADD_GROUP_MEMBER",e[e.DEL_GROUP_MEMBER=22]="DEL_GROUP_MEMBER",e[e.ADD_CONTACTS=23]="ADD_CONTACTS",e[e.UPDATE_CONTACTS=24]="UPDATE_CONTACTS",e[e.DEL_CONTACTS=25]="DEL_CONTACTS",e[e.BLOCK_USER=26]="BLOCK_USER",e[e.DEVICE_CODE=27]="DEVICE_CODE",e[e.MSG_OPEN_CLOCK=28]="MSG_OPEN_CLOCK",e[e.DELETE_SECRET_CHAT=29]="DELETE_SECRET_CHAT",e[e.DELETE_FEED=30]="DELETE_FEED",e[e.FAVORITE_UPDATE=31]="FAVORITE_UPDATE",e[e.STICKERS_UPDATE=32]="STICKERS_UPDATE",e[e.CUSTOM_STICKERS_UPDATE=33]="CUSTOM_STICKERS_UPDATE",e[e.MANAGE_DEVICE_UPDATE=34]="MANAGE_DEVICE_UPDATE",e[e.MASS_MESSAGE_UPDATE=35]="MASS_MESSAGE_UPDATE",e[e.CHAT_WALLPAPER_DELETE=36]="CHAT_WALLPAPER_DELETE",e[e.CHAT_WALLPAPER_ADD=37]="CHAT_WALLPAPER_ADD",e[e.CONTACT_REQUEST=40]="CONTACT_REQUEST",e[e.USER_PREFERENCE_CHANGE=38]="USER_PREFERENCE_CHANGE",e[e.UPDATE_GROUP_PIN=41]="UPDATE_GROUP_PIN",e[e.RTC_CANCEL=46]="RTC_CANCEL",e[e.ADD_FEED_FOLDER=43]="ADD_FEED_FOLDER",e[e.UPDATE_FEED_FOLDER=44]="UPDATE_FEED_FOLDER",e[e.UPDATE_FOLDER_FEED_LIST=47]="UPDATE_FOLDER_FEED_LIST",e[e.DELETE_FEED_FOLDER=45]="DELETE_FEED_FOLDER",e[e.UPDATE_CHATTER=42]="UPDATE_CHATTER",e[e.PAPER_KEY=48]="PAPER_KEY",e[e.PAPER_KEY_LOG=49]="PAPER_KEY_LOG",e[e.USER_LOGIN_SCAN=50]="USER_LOGIN_SCAN",e[e.RTC_RINGING=51]="RTC_RINGING",e[e.ON_UPDATE_PRIVATE_SETTING=52]="ON_UPDATE_PRIVATE_SETTING",e[e.ON_DELETE_PRIVATE_SETTING=53]="ON_DELETE_PRIVATE_SETTING",e[e.ON_UPDATE_LOGIN_PRIVATE_SETTING=54]="ON_UPDATE_LOGIN_PRIVATE_SETTING",e[e.PUSH_LIVE_MESSAGE=56]="PUSH_LIVE_MESSAGE",e[e.BIG_PACKAGE_PUSH=126]="BIG_PACKAGE_PUSH",e[e.KICK_OUT_DEVICE=127]="KICK_OUT_DEVICE",e[e.CLEAR_HISTORY=128]="CLEAR_HISTORY",e[e.CLEAR_DATA=255]="CLEAR_DATA"}(t||(t={})),function(e){e[e.KICK_OUT=0]="KICK_OUT"}(s||(s={})),exports.ChatType=void 0,(r=exports.ChatType||(exports.ChatType={}))[r.singleChat=1]="singleChat",r[r.groupChat=2]="groupChat",exports.MessageKind=void 0,(i=exports.MessageKind||(exports.MessageKind={}))[i.normal=0]="normal",i[i.system=1]="system",exports.MessageType=void 0,(n=exports.MessageType||(exports.MessageType={}))[n.text=0]="text",n[n.image=1]="image",n[n.audio=3]="audio",n[n.video=4]="video",n[n.file=5]="file",n[n.mergeForward=6]="mergeForward",n[n.sticker=7]="sticker",n[n.richText=8]="richText",n[n.contact=9]="contact",n[n.communication=10]="communication",n[n.mixedImageText=11]="mixedImageText",n[n.luckyGift=14]="luckyGift",n[n.fileGroup=15]="fileGroup",n[n.cardMsg=16]="cardMsg",exports.MessageOperation=void 0,(o=exports.MessageOperation||(exports.MessageOperation={}))[o.normal=0]="normal",o[o.recall=1]="recall",exports.ChatSdkErrorCode=void 0,(a=exports.ChatSdkErrorCode||(exports.ChatSdkErrorCode={}))[a.NETWORK_ERROR=-1e4]="NETWORK_ERROR",a[a.SDK_ERROR=-10001]="SDK_ERROR",exports.ChatSdkEvent=void 0,(h=exports.ChatSdkEvent||(exports.ChatSdkEvent={})).SDK_READY="SDK_READY",h.SDK_NOT_READY="SDK_NOT_READY",h.SDK_KICKOUT="SDK_KICKOUT",h.FEED_UPDATED="FEED_UPDATED",h.MESSAGE_RECEIVED="MESSAGE_RECEIVED",h.MESSAGE_UPDATED="MESSAGE_UPDATED",h.MESSAGE_RECEIPT_RECEIVED="MESSAGE_RECEIPT_RECEIVED",exports.RequestDirection=void 0,(c=exports.RequestDirection||(exports.RequestDirection={}))[c.up=0]="up",c[c.down=1]="down";const p="[TYPEX_AGENT_JS_SDK]";class S{level=0;setLogLevel(e){this.level=e}getLogLevel(){return this.level}shouldPrint(e){return e>=this.level}debug(...e){this.shouldPrint(0)&&console.debug(`${p}[DEBUG]`,...e)}info(...e){this.shouldPrint(0)&&console.info(`${p}[INFO]`,...e)}warn(...e){this.shouldPrint(1)&&console.warn(`${p}[WARN]`,...e)}error(...e){this.shouldPrint(2)&&console.error(`${p}[ERROR]`,...e)}}function C(e){return(new TextEncoder).encode(e).buffer}async function R(e,t){return window.crypto&&window.crypto.subtle?await async function(e,t){const s=C(t),r=C(e),i=await crypto.subtle.importKey("raw",s,{name:"HMAC",hash:{name:"SHA-256"}},!1,["sign"]),n=await crypto.subtle.sign("HMAC",i,r);return Array.from(new Uint8Array(n)).map(e=>e.toString(16).padStart(2,"0")).join("")}(e,t):function(e,t){const s=d.HmacSHA256(e,t);return d.enc.Hex.stringify(s)}(e,t)}const T=e=>{if(null==e)return"";if("object"!=typeof e||Array.isArray(e)){if(Array.isArray(e)){let t="";for(const s of e)t+=T(s);return t}return"string"==typeof e?e:String(e)}{const t=Object.keys(e).sort();let s="";for(const r of t)s+=T(e[r]);return s}};class m{instance;developer;logger;store;constructor(e,t){this.logger=t.logger,this.store=t.store,this.developer=e.developer;const s=e.baseURL.replace(/\/$/,"");this.instance=E.create({baseURL:s,headers:{...e.defaultHeaders??{}},timeout:e.timeout??3e4}),this.setupInterceptors()}setupInterceptors(){this.instance.interceptors.request.use(e=>(e.headers=e.headers||{},e.headers["Content-Type"]||(e.headers["Content-Type"]="application/json"),e.headers.Platform="web",e.headers.AppID=this.store.appId,e.url?.startsWith("/open")&&(e.headers.Authorization=`Bearer ${this.store.token}`),this.developer&&!e.ignoreDeveloper&&(e.headers["x-developer"]=this.developer),e))}async get(e,t={},s={}){return this.request({url:e,method:"GET",params:t,...s})}async post(e,t,s={}){return this.request({url:e,method:"POST",data:t,...s})}async request(e){try{const{needSign:t,...s}=e;if(t&&"POST"===s.method?.toUpperCase()&&s.data){const t=this.store.currentUser;if(!t){const e="[HttpClient] HttpClient needs make sign but not found login user";return this.logger.error(e),u(exports.ChatSdkErrorCode.SDK_ERROR,e)}try{const r=await(async(e,t)=>{const s=T(e);return await R(s,t)})(s.data,t.secretKey);e.headers||(e.headers={}),e.headers.sign=r}catch(e){const t=`[HttpClient] HttpClient make sign error: ${e.message}`;return this.logger.error(t),u(exports.ChatSdkErrorCode.SDK_ERROR,t)}}const r=await this.instance.request({...s,headers:{...e.headers||{}}}),i=r.data;return 0!==i.code?(this.logger.error("[HttpClient]","Business server response error:",r),{ok:!1,code:i.code,msg:i.msg,msg_i18n:i.msg_i18n}):{ok:!0,data:i.data}}catch(e){return this.handleError(e)}}handleError(e){return"ECONNABORTED"===e.code?{ok:!1,code:exports.ChatSdkErrorCode.NETWORK_ERROR,msg:e.message||"request timeout"}:{ok:!1,code:exports.ChatSdkErrorCode.NETWORK_ERROR,msg:e.message||"network error"}}}class D{listeners=new Map;logger;constructor(e){this.logger=e}on(e,t){this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t)}once(e,t){const s=r=>{t(r),this.off(e,s)};this.on(e,s)}off(e,t){this.listeners.has(e)&&(t?this.listeners.get(e).delete(t):this.listeners.delete(e))}emit(e,t){if(this.listeners.has(e))for(const s of this.listeners.get(e))try{s(t)}catch(t){this.logger.error(`[EventCore] handler error on "${String(e)}":`,t)}}clear(){this.listeners.clear()}}var I,A,O,N;!function(e){e.INIT="INIT",e.CONNECTING="CONNECTING",e.CONNECTED="CONNECTED",e.DISCONNECTED="DISCONNECTED",e.CLOSED="CLOSED"}(I||(I={})),function(e){e.OPEN="OPEN",e.ERROR="ERROR",e.CLOSE="CLOSE",e.MESSAGE="MESSAGE",e.STATUS_CHANGE="STATUS_CHANGE"}(A||(A={}));class b extends D{ws;options;status=I.INIT;constructor(e,t){super(t),this.options=e}connect(){if(this.status===I.CONNECTING||this.status===I.CONNECTED)return void this.logger.debug("[CONNECTION]","WebSocket is connecting or connected,current connect status is",`[${this.status}]`);this.logger.debug("[CONNECTION][connect]","WebSocket is connecting"),this.setStatus(I.CONNECTING);const e=new WebSocket(`${this.options.url}?token=${this.options.token}&platform=web`);this.ws=e,e.onopen=()=>{this.logger.debug("[CONNECTION][ws.onopen]","WebSocket is connected"),this.setStatus(I.CONNECTED),this.emit(A.OPEN)},e.onmessage=e=>{this.logger.debug("[CONNECTION][ws.onmessage]","WebSocket received a message"),this.emit(A.MESSAGE,e.data)},e.onerror=e=>{this.logger.error("[CONNECTION][ws.onerror]","WebSocket connect error"),this.emit(A.ERROR,e)},e.onclose=e=>{this.logger.debug("[CONNECTION][ws.onclose]","WebSocket connect be closed"),this.setStatus(I.DISCONNECTED),this.emit(A.CLOSE,e)}}close(){this.ws?.close(),this.ws=void 0,this.setStatus(I.CLOSED)}send(e){if(this.status!==I.CONNECTED)throw new Error("WebSocket is not connected");this.ws.send(e)}getStatus(){return this.status}setStatus(e){this.status!==e&&(this.status=e,this.emit(A.STATUS_CHANGE,e))}}class P{connection;pingTimeout;pongTimeout;pingTimer;pongTimer;logger;constructor(e,t,s={}){this.connection=e,this.pingTimeout=s.pingTimeout??3e4,this.pongTimeout=s.pongTimeout??2e4,this.logger=t}ping(e){this.logger.debug("[Heartbeat]","Heartbeat ping"),this.stop(),this.pingTimer=window.setTimeout(()=>{try{this.connection.send(JSON.stringify({type:0})),this.logger.debug("[Heartbeat]","Heartbeat send PING"),this.pong(e)}catch{}},this.pingTimeout)}pong(e){clearTimeout(this.pongTimer),this.pongTimer=window.setTimeout(()=>{e?.(),this.logger.debug("[Heartbeat]","Heartbeat called PONG callback")},this.pongTimeout)}stop(){this.logger.debug("[Heartbeat]","Heartbeat stop"),clearTimeout(this.pingTimer),clearTimeout(this.pongTimer),this.pingTimer=void 0,this.pongTimer=void 0}}class f{connection;baseInterval=1e3;maxAttempts=1/0;attempts=0;timer;enabled=!1;logger;constructor(e,t,s={}){this.logger=t,this.connection=e,s.baseInterval&&(this.baseInterval=s.baseInterval),s.maxAttempts&&(this.maxAttempts=s.maxAttempts),this.bindEvents()}bindEvents(){this.connection.on(A.CLOSE,()=>{this.enabled&&this.scheduleReconnect()}),this.connection.on(A.OPEN,()=>{this.reset()})}start(){this.logger.debug("[Reconnector]","Reconnector start"),this.enabled=!0}stop(){this.logger.debug("[Reconnector]","Reconnector stop"),this.enabled=!1,this.clearTimer()}scheduleReconnect(){if(this.logger.debug("[Reconnector]",`Reconnector scheduleReconnect, attempts: ${this.attempts}, maxAttempts: ${this.maxAttempts}`),this.attempts>=this.maxAttempts)return;this.attempts++;const e=this.baseInterval*Math.min(this.attempts,10);this.clearTimer(),this.timer=window.setTimeout(()=>{this.connection.connect()},e)}reset(){this.logger.debug("[Reconnector]","Reconnector reset"),this.attempts=0,this.clearTimer()}clearTimer(){this.timer&&(clearTimeout(this.timer),this.timer=void 0)}}!function(e){e.OPEN="OPEN",e.ERROR="ERROR",e.CLOSE="CLOSE",e.MESSAGE="MESSAGE",e.STATUS_CHANGE="STATUS_CHANGE",e.KICK_OUT="KICK_OUT"}(O||(O={}));class U extends D{connection;reconnector;heartbeat;constructor(e,t){super(t),this.connection=new b({url:e.wsUrl,token:e.token},t),this.reconnector=new f(this.connection,t),this.heartbeat=new P(this.connection,t),this.bindEvents()}connect(){this.reconnector.start(),this.connection.connect()}disconnect(){this.reconnector.stop(),this.heartbeat.stop(),this.connection.close()}startPing(){this.heartbeat.ping(()=>{this.disconnect();const e=setTimeout(()=>{this.connect(),clearTimeout(e)},1e4)})}bindEvents(){this.connection.on(A.OPEN,()=>{this.emit(O.OPEN),this.startPing()}),this.connection.on(A.CLOSE,t=>{if(t.code===e.KICK_OFF)return this.logger.warn("[IMClient][KICK_OUT]","You has been kicked off"),this.emit(O.KICK_OUT),void this.disconnect();this.emit(O.CLOSE),this.connect()}),this.connection.on(A.MESSAGE,e=>{this.handleRawMessage(e)}),this.connection.on(A.STATUS_CHANGE,e=>{this.emit(O.STATUS_CHANGE,e)}),this.connection.on(A.ERROR,e=>{this.emit(O.ERROR,e)})}handleRawMessage(e){try{const s=JSON.parse(e);if(s.type===t.PONG_MESSAGE?this.startPing():this.sendAck(s.id),s.type===t.BIG_PACKAGE_PUSH)for(const e of s.content.big_push)this.emit(O.MESSAGE,e);else this.emit(O.MESSAGE,s)}catch(e){this.logger.error("[IMClient][handleRawMessage]","Message raw data json.parse error",e),this.emit(O.ERROR,e)}}sendAck(e){this.connection.send(JSON.stringify({type:1,ids:[e]}))}}!function(e){e.APP_ID="appId",e.TOKEN="token",e.CURRENT_USER="currentUser",e.CURRENT_FEED="currentFeed",e.SDK_READY="sdkReady",e.IS_CONNECTED="isConnected",e.DEVICE_ID="typeximDeviceId",e.BROWSER_FINGERPRINT="typeximBrowserFingerprint"}(N||(N={}));class y extends D{state={};constructor(e){super(e)}set(e,t){this.logger.debug("[Store] set:",e,t),this.state[e]=t,this.emit("change",{key:e,value:t})}get(e){return this.logger.debug("[Store] get:",e,this.state[e]),this.state[e]}remove(e){this.logger.debug("[Store] remove:",e,this.state[e]),delete this.state[e],this.emit("change",{key:e,value:void 0})}clear(){this.logger.debug("[Store] clear",this.state),this.state={appId:this.state.appId,token:this.state.token,typeximBrowserFingerprint:this.state.typeximBrowserFingerprint},this.emit("clear",void 0)}setWithLocal(e,t){this.logger.debug("[Store] setWithLocal:",e,t),this.set(e,t),localStorage.setItem(e,JSON.stringify(t))}getWithLocal(e){if(this.logger.debug("[Store] getWithLocal:",e,this.state[e]),this.state[e])return this.state[e];const t=localStorage.getItem(e);return t?JSON.parse(t):void 0}removeWithLocal(e){this.logger.debug("[Store] removeWithLocal:",e,this.state[e]),localStorage.removeItem(e),this.remove(e)}onChange(e){this.on("change",e)}offChange(e){this.off("change",e)}onClear(e){this.on("clear",e)}get appId(){return this.logger.debug("[Store] get appId",this.state.appId),this.state.appId}get ready(){return this.logger.debug("[Store] get ready",this.state.sdkReady),!!this.state.sdkReady}get deviceId(){const e=this.getWithLocal(N.DEVICE_ID);return this.logger.debug("[Store] get deviceId",e),e}get currentUser(){return this.logger.debug("[Store] get currentUser",this.state.currentUser),this.state.currentUser}get currentChatId(){return this.logger.debug("[Store] get currentChatId",this.state.currentFeed?.chat_id),this.state.currentFeed?.chat_id}get currentFeed(){return this.logger.debug("[Store] get currentFeed",this.state.currentFeed),this.state.currentFeed}get sessionId(){return this.logger.debug("[Store] get sessionId",this.state.currentUser?.sessionId),this.state.currentUser?.sessionId}get token(){return this.logger.debug("[Store] get token",this.state.token),this.state.token}get browserFingerprint(){const e=this.getWithLocal(N.BROWSER_FINGERPRINT);return this.logger.debug("[Store] get browserFingerprint",e),e}}class M{version;constructor(){this.version=0}increment(){this.version++}get currentVersion(){return this.version}check(e){return e===this.version}}const k="@typexim/agent-js-sdk_broadcast_channel";class v{config;logger;http;imClient=null;store;events;version;loadFeedAbortController=null;tabId;constructor(e,t,s){this.logger=new S,this.store=new y(this.logger),this.events=new D(this.logger),this.store.set(N.APP_ID,e),this.store.set(N.TOKEN,t),this.config=s,this.version=new M,this.http=new m({baseURL:s.httpHost,developer:s.developer},{logger:this.logger,store:this.store}),this.tabId=`${e}_${Date.now()}_${Math.random().toString(36).substring(2,15)}`,this.generateBrowserFingerprint(),this.listenBroadcastChannel()}setLogLevel(e){this.logger.setLogLevel(e)}async login(e){try{const t={...e||{}};if(t.userId&&!t.busiToken)return u(exports.ChatSdkErrorCode.SDK_ERROR,"Invalid param: userId and busiToken must be provided together");if(!t.userId){const e=this.store.browserFingerprint||await this.generateBrowserFingerprint();t.userId=e}const s=await this.innerRegister(t);if(!s.ok||!s.data)return this.logger.error("[InternalChatSdk] innerRegister failed",s),u(s.code,s.msg,s.msg_i18n);const r=s.data,i=await this.requestDeviceId(),n=await this.innerLogin(r,i,t);if(!n.ok||!n.data)return this.logger.error("[InternalChatSdk] innerLogin failed",n),u(n.code,n.msg,n.msg_i18n);const o=l.get("sessionid");if(!o)throw new Error("Login failed, not found sessionid");const a=n.data||{};return a.sessionId=o,a.secretKey=n.data.key,await Promise.all([this.syncLoginState(a),this.connectWebSocket(o)]),this.notifyOtherTabsLogin(),_({id:a.id,userId:t.userId,name:a.name,avatar:a.avatar,email:a.email})}catch(e){return this.handleException(e)}}innerRegister(e){return this.http.post("/open/user/register",{userID:e.userId,name:e.name,avatar:e.avatar,email:e.email,passWord:e.password,userBuf:e.userBuf,busiToken:e.busiToken})}innerLogin(e,t,s){return this.http.post("/open/user/login",{uid:e,busi_token:s.busiToken,device_id:t})}async generateBrowserFingerprint(){try{const e=this.store.getWithLocal(N.BROWSER_FINGERPRINT);if(e)return this.logger.debug("[ChatSdk] generateBrowserFingerprint success from localstorage",e),e;const t=await g.load(),s=await t.get();return this.logger.debug("[ChatSdk] generateBrowserFingerprint success",s.visitorId),this.store.setWithLocal(N.BROWSER_FINGERPRINT,s.visitorId),s.visitorId}catch(e){this.logger.info("[ChatSdk] FingerprintJS generateBrowserFingerprint Error",e);const t=`${Date.now()}_${Math.random().toString(36).substring(2,15)}`;return this.logger.debug("[ChatSdk] random generateBrowserFingerprint success",t),this.store.setWithLocal(N.BROWSER_FINGERPRINT,t),t}}async loadFeed(){const e=this.version.currentVersion;this.loadFeedAbortController&&this.loadFeedAbortController.abort(),this.loadFeedAbortController=new AbortController;const t=await this.http.get("/feed",{limit:10},{signal:this.loadFeedAbortController.signal}).catch(e=>{this.logger.error("[ChatSdk] loadFeed Error",e)});this.loadFeedAbortController=null;const s=this.version.check(e);if(!t||!s)return;const r=t.data?.feed_list?.find(e=>e.chat_type===exports.ChatType.groupChat);t.ok&&(this.store.set(N.CURRENT_FEED,r),this.events.emit(exports.ChatSdkEvent.FEED_UPDATED,r))}async syncLoginState(e){this.store.set(N.CURRENT_USER,e),this.store.set(N.SDK_READY,!0),await this.loadFeed(),this.events.emit(exports.ChatSdkEvent.SDK_READY)}syncLogoutState(){this.unlistenBroadcastChannel(),this.store.set(N.CURRENT_USER,void 0),this.store.set(N.SDK_READY,!1),this.store.set(N.CURRENT_FEED,void 0),this.events.emit(exports.ChatSdkEvent.SDK_NOT_READY)}async requestDeviceId(){const e=this.store.getWithLocal(N.DEVICE_ID);if(e)return this.logger.debug("[ChatSdk] requestDeviceId success from localstorage",e),e;const t=await this.http.post("/device_api/device_id",{platform:"web",hash_str:this.store.browserFingerprint||""}).catch(e=>{this.logger.error("[ChatSdk] requestDeviceId Error",e)});if(t?.ok&&t?.data)return this.logger.debug("[ChatSdk] requestDeviceId success",t.data),this.store.setWithLocal(N.DEVICE_ID,t.data),t.data;if(this.store.browserFingerprint)return this.logger.debug("[ChatSdk] use browserFingerprint as deviceId",this.store.browserFingerprint),this.store.setWithLocal(N.DEVICE_ID,this.store.browserFingerprint),this.store.browserFingerprint;const s=`${Date.now()}_${Math.random().toString(36).substring(2,15)}`;return this.logger.debug("[ChatSdk] random generateDeviceId success",s),this.store.setWithLocal(N.DEVICE_ID,s),s}async logout(){try{if(!this.store.currentUser)return _(null);const e=await this.http.get("/user/logout");return e.ok?(this.syncLogoutState(),this.disconnectWebSocket(),_(null)):u(e.code,e.msg,e.msg_i18n)}catch(e){return this.handleException(e)}}async addGroupMember(e,t){try{if(this.assertReady(),!e)return u(exports.ChatSdkErrorCode.SDK_ERROR,"not found chat id");if(!t?.length)return _(null);const s=this.store.currentFeed;if(!s)return u(exports.ChatSdkErrorCode.SDK_ERROR,"not found current feed");const r=await this.getChatMembers(s.chat_id,s.chat_member);if(!r.ok)return u(r.code,r.msg,r.msg_i18n);const i=(r.data||[]).map(e=>e.user_id),n=t.filter(e=>!i.includes(e)),o=await this.addMember(n,s.chat_id);return o.ok?_(o.data):u(o.code,o.msg,o.msg_i18n)}catch(e){return this.handleException(e)}}async initialChat(e){try{this.assertReady();const t=this.store.currentFeed;if(!t){const t=await this.createFeed(e.receiverIds);return t.ok&&t.data?(this.store.set(N.CURRENT_FEED,t.data),_(t.data.chat_id)):u(t.code,t.msg,t.msg_i18n)}const s=await this.addGroupMember(t.chat_id,e.receiverIds);return s.ok?_(t.chat_id):s}catch(e){return this.handleException(e)}}async getCurrentFeed(){try{return this.assertReady(),{ok:!0,data:this.store.currentFeed}}catch(e){return this.handleException(e)}}async getMessageList(e){try{this.assertReady();const t=this.store.currentFeed;if(!t)return u(exports.ChatSdkErrorCode.SDK_ERROR,"No feed found");const{chat_id:s,first_position:r,last_message_position:i}=t,{positions:n,nextPosition:o,hasMore:a}=(e=>{const t=[];if(e.direction===exports.RequestDirection.up){let s=e.startPosition??e.maxPosition;for(;t.length<e.limit&&s>=e.minPosition;)t.push(s),s--;const r=s>=e.minPosition;let i;return r&&(i=s),{positions:t,nextPosition:i,hasMore:r}}{let s=e.startPosition??e.minPosition;for(;t.length<e.limit&&s<=e.maxPosition;)t.push(s),s++;const r=s<=e.maxPosition;let i;return r&&(i=s),{positions:t,nextPosition:i,hasMore:r}}})({maxPosition:i,minPosition:r,startPosition:e.messagePosition,limit:e.limit,direction:e.direction??exports.RequestDirection.up});if(n.length>0){const e=await this.http.post("/message/get_by_position",{chat_position:{[s]:n}});if(e.ok&&e.data?.[s]){return _({hasMore:a,list:e.data[s].reverse().filter(e=>e.message_kind_id!==exports.MessageKind.system&&e.operation!==exports.MessageOperation.recall),nextMessagePosition:o})}return u(e.code,e.msg,e.msg_i18n)}return _({hasMore:!1,list:[]})}catch(e){return this.handleException(e)}}async sendMessage(e){try{this.assertReady();const t=e.messageType;switch(t){case exports.MessageType.text:return this.sendTextMessage(e);case exports.MessageType.image:return this.sendImageMessage(e);case exports.MessageType.video:return this.sendVideoMessage(e);case exports.MessageType.file:return this.sendFileMessage(e);default:return this.logger.warn("Unsupported message type: ",t),u(exports.ChatSdkErrorCode.SDK_ERROR,`Unsupported message type: ${t}`)}}catch(e){return this.handleException(e)}}async sendTextMessage(e){return this.http.post("/message/send_text",{chat_id:e.chatId,client_msg_id:e.clientMsgId,text:e.text.trim()},{needSign:!0})}async sendImageMessage(e){return this.http.post("/message/send_image",{chat_id:e.chatId,client_msg_id:e.clientMsgId,object_url:e.objectUrl,width:e.width,height:e.height},{needSign:!0})}async sendVideoMessage(e){return this.http.post("/message/send_video",{chat_id:e.chatId,client_msg_id:e.clientMsgId,video_url:e.objectUrl,duration_second:e.duration,image_url:e.coverUrl,width:e.width,height:e.height},{needSign:!0})}async sendFileMessage(e){return this.http.post("/message/send_file",{chat_id:e.chatId,client_msg_id:e.clientMsgId,object_url:e.objectUrl,file_name:e.fileName,file_size:e.fileSize},{needSign:!0})}async uploadFile(e){const{chatId:t,fileContent:s,fileFormat:r,fileName:i,fileType:n,onUploadProgress:o}=e,a=new FormData;return a.append("file_content",s),a.append("file_type",n),a.append("file_name",i),a.append("file_format",r),a.append("chat_id",t),this.http.post("/chat/upload",a,{needSign:!1,ignoreDeveloper:!0,onUploadProgress:o,headers:{"Content-Type":"multipart/form-data"}})}async sendMessageReadReceipt(e){try{return this.assertReady(),e.messageIds?.length?this.http.post("/message/read",{chat_id:e.chatId,message_ids:e.messageIds},{needSign:!0}):_(null)}catch(e){return this.handleException(e)}}async markAllMessagesAsRead(e){const t=this.store.currentFeed?.unread_message_ids||[];return this.sendMessageReadReceipt({chatId:e,messageIds:t})}async recallMessage(e){try{return this.assertReady(),e.messageIds.length?this.http.post("/message/remove",{chat_id:e.chatId,message_ids:e.messageIds,two_way:1},{needSign:!0}):_(null)}catch(e){return this.handleException(e)}}async createFeed(e){return await this.http.post("/chat/group",{receiver_ids:e,group_type:0,name:`User ${this.store.currentUser?.name||this.store.currentUser?.id}`},{needSign:!0})}async getChatMembers(e,t){return this.http.post("/chat/chatter_info",JSON.stringify({chatters:{[e]:t}}))}async addMember(e,t){return t?e.length?this.http.post("/chat/group/add_member",{user_ids:e,chat_id:t},{needSign:!0}):_(null):u(exports.ChatSdkErrorCode.SDK_ERROR,"not found chat id")}handlePushMessage(e){switch(this.logger.debug("handlePushMessage",e),e.type){case t.CREATE_FEED:case t.UPDATE_FEED:this.handleFeedUpdate(e);break;case t.ADD_MESSAGE:this.handleAddMessage(e);break;case t.UPDATE_MESSAGE:this.handleUpdateMessage(e);break;case t.READ_MESSAGE:this.handleReadMessage(e);break;case t.KICK_OUT_DEVICE:this.handleKickOut()}}handleFeedUpdate(e){this.store.set(N.CURRENT_FEED,e.content),this.events.emit(exports.ChatSdkEvent.FEED_UPDATED,e.content)}async handleAddMessage(e){e.content.chat_id===this.store.currentChatId&&(await this.loadFeed(),this.events.emit(exports.ChatSdkEvent.MESSAGE_RECEIVED,e.content))}async handleUpdateMessage(e){e.content.chat_id===this.store.currentChatId&&(await this.loadFeed(),this.events.emit(exports.ChatSdkEvent.MESSAGE_UPDATED,e.content))}async handleReadMessage(e){e.content.chat_id===this.store.currentChatId&&(await this.loadFeed(),this.events.emit(exports.ChatSdkEvent.MESSAGE_RECEIPT_RECEIVED,{chatId:e.content.chat_id,messageIds:e.content.message_ids,readerUserIds:e.content.reader_ids}))}handleKickOut(){this.syncLogoutState(),this.disconnectWebSocket()}on(e,t){return this.events.on(e,t)}once(e,t){return this.events.once(e,t)}off(e,t){return this.events.off(e,t)}get currentUserId(){return this.logger.debug("[InternalChatSdk] get currentUserId==>",this.store.currentUser),this.store.currentUser?.id}get currentChatId(){return this.logger.debug("[InternalChatSdk] get currentChatId==>",this.store.currentChatId),this.store.currentChatId}get isReady(){return this.logger.debug("[InternalChatSdk] get isReady==>",this.store.ready),this.store.ready}listenBroadcastChannel(){new BroadcastChannel(k).onmessage=e=>{const t=this.store.appId;e.data.type===s.KICK_OUT&&e.data.appId===t&&e.data.tabId!==this.tabId&&(this.logger.debug("[BroadcastChannel] message: kick out",{appId:t,sourceTabId:e.data.tabId,currentTabId:this.tabId}),this.handleKickOut())}}unlistenBroadcastChannel(){new BroadcastChannel(k).close()}notifyOtherTabsLogin(){new BroadcastChannel(k).postMessage({type:s.KICK_OUT,appId:this.store.appId,tabId:this.tabId})}async connectWebSocket(e){if(!e)throw Error("connect websocket error, not found login user token");this.imClient=new U({token:e,wsUrl:`${this.config.websocketHost}/ws`},this.logger),this.imClient.on(O.OPEN,()=>{this.store.set(N.IS_CONNECTED,!0)}),this.imClient.on(O.KICK_OUT,()=>{this.store.clear(),this.events.emit(exports.ChatSdkEvent.SDK_KICKOUT)}),this.imClient.on(O.CLOSE,()=>{this.store.set(N.IS_CONNECTED,!1)}),this.imClient.on(O.MESSAGE,e=>{this.handlePushMessage(e)}),this.imClient.connect()}disconnectWebSocket(){this.imClient&&(this.imClient.disconnect(),this.imClient=null)}assertReady(){if(!this.store.ready)throw{code:exports.ChatSdkErrorCode.SDK_ERROR,msg:"sdk not ready"}}handleException(e){return this.logger.error("[ChatSDK Error]",e),{ok:!1,code:e.code||exports.ChatSdkErrorCode.SDK_ERROR,msg:e.msg||e.message||"sdk error"}}}const w=new Map;function L(e){return{setLogLevel(t){e.setLogLevel(t)},login:t=>e.login(t),logout:()=>e.logout(),initialChat:t=>e.initialChat(t),addGroupMember:(t,s)=>e.addGroupMember(t,s),getCurrentFeed:()=>e.getCurrentFeed(),getMessageList:t=>e.getMessageList(t),uploadFile:t=>e.uploadFile(t),sendMessage:t=>e.sendMessage(t),sendMessageReadReceipt:t=>e.sendMessageReadReceipt(t),markAllMessagesAsRead:t=>e.markAllMessagesAsRead(t),recallMessage:t=>e.recallMessage(t),get currentUserId(){return e.currentUserId},get currentChatId(){return e.currentChatId},get isReady(){return e.isReady},on:(t,s)=>e.on(t,s),once:(t,s)=>e.once(t,s),off:(t,s)=>e.off(t,s)}}var G={create:function(e,t,s){if(w.has(e))return L(w.get(e));const r=new v(e,t,s);return w.set(e,r),L(r)}};exports.default=G;
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { AxiosRequestConfig } from 'axios';
|
|
2
|
+
|
|
3
|
+
interface HttpResponse<T = any> {
|
|
4
|
+
/** 接口成功与否 */
|
|
5
|
+
ok: boolean;
|
|
6
|
+
/** 返回的数据 */
|
|
7
|
+
data?: T;
|
|
8
|
+
/** 错误码,ok为true时没有该字段 */
|
|
9
|
+
code?: number;
|
|
10
|
+
/** 错误信息 */
|
|
11
|
+
msg?: string;
|
|
12
|
+
/** 错误信息的i18n */
|
|
13
|
+
msg_i18n?: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare enum ChatType {
|
|
17
|
+
/** 单聊 */
|
|
18
|
+
singleChat = 1,
|
|
19
|
+
/** 群聊 */
|
|
20
|
+
groupChat = 2
|
|
21
|
+
}
|
|
22
|
+
declare enum MessageKind {
|
|
23
|
+
/** 普通消息 */
|
|
24
|
+
normal = 0,
|
|
25
|
+
/** 系统消息 */
|
|
26
|
+
system = 1
|
|
27
|
+
}
|
|
28
|
+
declare enum MessageType {
|
|
29
|
+
/** 文本消息 */
|
|
30
|
+
text = 0,
|
|
31
|
+
/** 图片消息 */
|
|
32
|
+
image = 1,
|
|
33
|
+
/** 卡片消息(已弃用,暂时没有这个消息类型) */
|
|
34
|
+
/** 音频消息 */
|
|
35
|
+
audio = 3,
|
|
36
|
+
/** 视频消息 */
|
|
37
|
+
video = 4,
|
|
38
|
+
/** 文件消息 */
|
|
39
|
+
file = 5,
|
|
40
|
+
/** 合并转发消息 */
|
|
41
|
+
mergeForward = 6,
|
|
42
|
+
/** 表情消息 */
|
|
43
|
+
sticker = 7,
|
|
44
|
+
/** 富文本消息 */
|
|
45
|
+
richText = 8,
|
|
46
|
+
/** 联系人卡片消息 */
|
|
47
|
+
contact = 9,
|
|
48
|
+
/** RTC 消息 */
|
|
49
|
+
communication = 10,
|
|
50
|
+
/** 图文混合消息 */
|
|
51
|
+
mixedImageText = 11,
|
|
52
|
+
/** 在客服场景下不会出现这个类型 */
|
|
53
|
+
/** 红包消息 */
|
|
54
|
+
luckyGift = 14,
|
|
55
|
+
/** 文件组消息 */
|
|
56
|
+
fileGroup = 15,
|
|
57
|
+
/** 卡片消息 */
|
|
58
|
+
cardMsg = 16
|
|
59
|
+
}
|
|
60
|
+
declare enum MessageOperation {
|
|
61
|
+
/** 正常 */
|
|
62
|
+
normal = 0,
|
|
63
|
+
/** 撤回 */
|
|
64
|
+
recall = 1
|
|
65
|
+
}
|
|
66
|
+
declare enum ChatSdkErrorCode {
|
|
67
|
+
/** 网络错误 */
|
|
68
|
+
NETWORK_ERROR = -10000,
|
|
69
|
+
/** SDK 错误 */
|
|
70
|
+
SDK_ERROR = -10001
|
|
71
|
+
}
|
|
72
|
+
declare enum ChatSdkEvent {
|
|
73
|
+
/** sdk已准备好,可以调用需要鉴权的接口 */
|
|
74
|
+
SDK_READY = "SDK_READY",
|
|
75
|
+
/** sdk未准备好,无法调用需要鉴权的接口 */
|
|
76
|
+
SDK_NOT_READY = "SDK_NOT_READY",
|
|
77
|
+
/** 当前用户被踢下线 */
|
|
78
|
+
SDK_KICKOUT = "SDK_KICKOUT",
|
|
79
|
+
/** feed更新 */
|
|
80
|
+
FEED_UPDATED = "FEED_UPDATED",
|
|
81
|
+
/** 收到新消息 */
|
|
82
|
+
MESSAGE_RECEIVED = "MESSAGE_RECEIVED",
|
|
83
|
+
/** 消息更新,比如消息被编辑 */
|
|
84
|
+
MESSAGE_UPDATED = "MESSAGE_UPDATED",
|
|
85
|
+
/** 消息已读回执,哪些消息被对方已读了 */
|
|
86
|
+
MESSAGE_RECEIPT_RECEIVED = "MESSAGE_RECEIPT_RECEIVED"
|
|
87
|
+
}
|
|
88
|
+
declare enum RequestDirection {
|
|
89
|
+
/**
|
|
90
|
+
* 向上请求数据
|
|
91
|
+
*/
|
|
92
|
+
up = 0,
|
|
93
|
+
/**
|
|
94
|
+
* 向下请求数据
|
|
95
|
+
*/
|
|
96
|
+
down = 1
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface Message {
|
|
100
|
+
/**
|
|
101
|
+
* 消息id
|
|
102
|
+
*/
|
|
103
|
+
id: string;
|
|
104
|
+
/**
|
|
105
|
+
* chat id
|
|
106
|
+
*/
|
|
107
|
+
chat_id: string;
|
|
108
|
+
/**
|
|
109
|
+
* 引用的消息id
|
|
110
|
+
*/
|
|
111
|
+
reply_msg_id?: string;
|
|
112
|
+
/**
|
|
113
|
+
* 消息位置
|
|
114
|
+
*/
|
|
115
|
+
position: number;
|
|
116
|
+
/**
|
|
117
|
+
* 发送者Id
|
|
118
|
+
*/
|
|
119
|
+
sender_id: string;
|
|
120
|
+
/**
|
|
121
|
+
* 消息来源类别,系统触发,普通触发
|
|
122
|
+
*/
|
|
123
|
+
message_kind_id: MessageKind;
|
|
124
|
+
/**
|
|
125
|
+
* 消息类别
|
|
126
|
+
*/
|
|
127
|
+
message_type_id: MessageType;
|
|
128
|
+
/**
|
|
129
|
+
* 消息内容
|
|
130
|
+
*/
|
|
131
|
+
content: string;
|
|
132
|
+
/**
|
|
133
|
+
* 发送时间
|
|
134
|
+
*/
|
|
135
|
+
send_at: string;
|
|
136
|
+
/**
|
|
137
|
+
* 已读信息的用户id
|
|
138
|
+
*/
|
|
139
|
+
reader_ids: string[];
|
|
140
|
+
/**
|
|
141
|
+
* 存放对某些隐藏消息的用户的Id
|
|
142
|
+
*/
|
|
143
|
+
hide_ids: string[];
|
|
144
|
+
/**
|
|
145
|
+
* 消息操作状态
|
|
146
|
+
*/
|
|
147
|
+
operation: MessageOperation;
|
|
148
|
+
/**
|
|
149
|
+
* 消息操作人id
|
|
150
|
+
*/
|
|
151
|
+
operation_user_id: string;
|
|
152
|
+
/**
|
|
153
|
+
* 客户端消息id
|
|
154
|
+
*/
|
|
155
|
+
client_msg_id?: string;
|
|
156
|
+
}
|
|
157
|
+
interface GetMessageListData {
|
|
158
|
+
/**
|
|
159
|
+
* 拉消息起始位置(上一次分页查询返回的nextMessagePosition)
|
|
160
|
+
* 第一次拉消息时不用传
|
|
161
|
+
*/
|
|
162
|
+
messagePosition?: number;
|
|
163
|
+
/**
|
|
164
|
+
* 消息拉取方向
|
|
165
|
+
* 向上拉更旧,向下拉更新
|
|
166
|
+
* 默认为向上拉,即从最新的一条消息向上拉取
|
|
167
|
+
*/
|
|
168
|
+
direction?: RequestDirection;
|
|
169
|
+
/**
|
|
170
|
+
* 需要拉取的消息数量
|
|
171
|
+
*/
|
|
172
|
+
limit: number;
|
|
173
|
+
}
|
|
174
|
+
interface GetMessageListResp {
|
|
175
|
+
/**
|
|
176
|
+
* 是否还有更多
|
|
177
|
+
*/
|
|
178
|
+
hasMore: boolean;
|
|
179
|
+
/**
|
|
180
|
+
* 消息列表
|
|
181
|
+
*/
|
|
182
|
+
list: Message[];
|
|
183
|
+
/**
|
|
184
|
+
* 下一页查询时消息起始位置(hasMore为false时没有该字段)
|
|
185
|
+
*/
|
|
186
|
+
nextMessagePosition?: number;
|
|
187
|
+
}
|
|
188
|
+
/** 发送消息的通用数据 */
|
|
189
|
+
interface SendMessageCommonData {
|
|
190
|
+
/** 客户端消息id */
|
|
191
|
+
clientMsgId: string;
|
|
192
|
+
/** chat id */
|
|
193
|
+
chatId: string;
|
|
194
|
+
}
|
|
195
|
+
/** 发送文本消息的请求数据 */
|
|
196
|
+
interface SendTextMsgData extends SendMessageCommonData {
|
|
197
|
+
/** 消息类型 */
|
|
198
|
+
messageType: MessageType.text;
|
|
199
|
+
/** 文本内容 */
|
|
200
|
+
text: string;
|
|
201
|
+
}
|
|
202
|
+
/** 发送图片消息的请求数据 */
|
|
203
|
+
interface SendImageMsgData extends SendMessageCommonData {
|
|
204
|
+
/** 消息类型 */
|
|
205
|
+
messageType: MessageType.image;
|
|
206
|
+
/** 图片上传后的url */
|
|
207
|
+
objectUrl: string;
|
|
208
|
+
/** 图片宽度 */
|
|
209
|
+
width: number;
|
|
210
|
+
/** 图片高度 */
|
|
211
|
+
height: number;
|
|
212
|
+
}
|
|
213
|
+
/** 发送视频消息的请求数据 */
|
|
214
|
+
interface SendVideoMsgData extends SendMessageCommonData {
|
|
215
|
+
/** 消息类型 */
|
|
216
|
+
messageType: MessageType.video;
|
|
217
|
+
/** 视频上传后的url */
|
|
218
|
+
objectUrl: string;
|
|
219
|
+
/** 视频时长,单位为秒 */
|
|
220
|
+
duration: number;
|
|
221
|
+
/** 视频封面 url */
|
|
222
|
+
coverUrl: string;
|
|
223
|
+
/** 视频宽度 */
|
|
224
|
+
width: number;
|
|
225
|
+
/** 视频高度 */
|
|
226
|
+
height: number;
|
|
227
|
+
}
|
|
228
|
+
/** 发送文件消息 */
|
|
229
|
+
interface SendFileMsgData extends SendMessageCommonData {
|
|
230
|
+
/** 消息类型 */
|
|
231
|
+
messageType: MessageType.file;
|
|
232
|
+
/** 文件上传后的url */
|
|
233
|
+
objectUrl: string;
|
|
234
|
+
/** 文件名 */
|
|
235
|
+
fileName: string;
|
|
236
|
+
/** 文件大小,单位为 byte */
|
|
237
|
+
fileSize: number;
|
|
238
|
+
}
|
|
239
|
+
type SendMessageData = SendTextMsgData | SendImageMsgData | SendVideoMsgData | SendFileMsgData;
|
|
240
|
+
type UploadFileType = 'image' | 'video' | 'application';
|
|
241
|
+
/** 上传文件请求参数 */
|
|
242
|
+
interface UploadFileData {
|
|
243
|
+
/** chat id */
|
|
244
|
+
chatId: string;
|
|
245
|
+
/** 文件名称 */
|
|
246
|
+
fileName: string;
|
|
247
|
+
/** 文件内容 */
|
|
248
|
+
fileContent: File;
|
|
249
|
+
/** 文件类型 */
|
|
250
|
+
fileType: UploadFileType;
|
|
251
|
+
/** 文件原始类型 */
|
|
252
|
+
fileFormat: File['type'];
|
|
253
|
+
/** 上传进度监听回调 */
|
|
254
|
+
onUploadProgress?: AxiosRequestConfig['onUploadProgress'];
|
|
255
|
+
}
|
|
256
|
+
interface SendMessageReadReceiptData {
|
|
257
|
+
/** chat id */
|
|
258
|
+
chatId: string;
|
|
259
|
+
/** 消息id */
|
|
260
|
+
messageIds: string[];
|
|
261
|
+
}
|
|
262
|
+
interface RecallMessageData {
|
|
263
|
+
/** chat id */
|
|
264
|
+
chatId: string;
|
|
265
|
+
/** 消息id */
|
|
266
|
+
messageIds: string[];
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
interface Feed {
|
|
270
|
+
/**
|
|
271
|
+
* feedId
|
|
272
|
+
*/
|
|
273
|
+
id: string;
|
|
274
|
+
/**
|
|
275
|
+
* 会话id
|
|
276
|
+
*/
|
|
277
|
+
chat_id: string;
|
|
278
|
+
/**
|
|
279
|
+
* 单聊时对方的id
|
|
280
|
+
*/
|
|
281
|
+
target_id: string;
|
|
282
|
+
/**
|
|
283
|
+
* 聊天类型
|
|
284
|
+
*/
|
|
285
|
+
chat_type: ChatType;
|
|
286
|
+
/**
|
|
287
|
+
* 会话成员id
|
|
288
|
+
*/
|
|
289
|
+
chat_member: string[];
|
|
290
|
+
/**
|
|
291
|
+
* 头像
|
|
292
|
+
*/
|
|
293
|
+
avatar: string;
|
|
294
|
+
/**
|
|
295
|
+
* 会话名称
|
|
296
|
+
*/
|
|
297
|
+
name: string;
|
|
298
|
+
/**
|
|
299
|
+
* 未读消息ids
|
|
300
|
+
*/
|
|
301
|
+
unread_message_ids?: string[];
|
|
302
|
+
/**
|
|
303
|
+
* 未读数
|
|
304
|
+
*/
|
|
305
|
+
badge: number;
|
|
306
|
+
/**
|
|
307
|
+
* 最后更新时间,'2025-09-18T02:24:25Z'
|
|
308
|
+
*/
|
|
309
|
+
last_updated_at: string;
|
|
310
|
+
/**
|
|
311
|
+
* 首条消息的position
|
|
312
|
+
*/
|
|
313
|
+
first_position: number;
|
|
314
|
+
/**
|
|
315
|
+
* 最后一条消息的position
|
|
316
|
+
*/
|
|
317
|
+
last_message_position: number;
|
|
318
|
+
/**
|
|
319
|
+
* 最后一条消息的发送时间
|
|
320
|
+
*/
|
|
321
|
+
last_message_send_at: string;
|
|
322
|
+
/**
|
|
323
|
+
* 最后一条消息的内容(content)
|
|
324
|
+
*/
|
|
325
|
+
last_message: string;
|
|
326
|
+
/**
|
|
327
|
+
* 最后一条消息的来源类别
|
|
328
|
+
*/
|
|
329
|
+
last_message_kind_id: MessageKind;
|
|
330
|
+
/**
|
|
331
|
+
* 最后一条消息的消息类别
|
|
332
|
+
*/
|
|
333
|
+
last_message_type_id: MessageType;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
interface ChatSDKConfig {
|
|
337
|
+
/**
|
|
338
|
+
* API服务器域名
|
|
339
|
+
*/
|
|
340
|
+
httpHost: string;
|
|
341
|
+
/**
|
|
342
|
+
* websocket服务器域名
|
|
343
|
+
*/
|
|
344
|
+
websocketHost: string;
|
|
345
|
+
/**
|
|
346
|
+
* 为http请求添加x-developer消息头(调试用,除非特别声明,否则忽略这个参数)
|
|
347
|
+
*/
|
|
348
|
+
developer?: string;
|
|
349
|
+
}
|
|
350
|
+
interface LoginData {
|
|
351
|
+
/** 业务方提供的用户凭证,供Typex 后端校验请求的真实性,非游客模式下必填 */
|
|
352
|
+
busiToken?: string;
|
|
353
|
+
/** 业务方用户唯一标识,同一个 userId 将会返回相同的 Typex user_id */
|
|
354
|
+
userId?: string;
|
|
355
|
+
/** 用户昵称 */
|
|
356
|
+
name?: string;
|
|
357
|
+
/** 用户头像 */
|
|
358
|
+
avatar?: string;
|
|
359
|
+
/** 邮箱 */
|
|
360
|
+
email?: string;
|
|
361
|
+
/** 密码(用AppKey+32位随机数作Aes加密后Base64Encode) */
|
|
362
|
+
password?: string;
|
|
363
|
+
/** 用户信息,json字符串 */
|
|
364
|
+
userBuf?: string;
|
|
365
|
+
}
|
|
366
|
+
interface User {
|
|
367
|
+
/** typex_user_id */
|
|
368
|
+
id: string;
|
|
369
|
+
/** 业务方用户唯一标识 */
|
|
370
|
+
userId?: string;
|
|
371
|
+
/** 用户昵称 */
|
|
372
|
+
name: string;
|
|
373
|
+
/** 用户头像 */
|
|
374
|
+
avatar?: string;
|
|
375
|
+
/** 用户邮箱 */
|
|
376
|
+
email?: string;
|
|
377
|
+
/** 密钥 */
|
|
378
|
+
key?: string;
|
|
379
|
+
}
|
|
380
|
+
interface InitialChatData {
|
|
381
|
+
/** 需要和哪些人创建聊天,比如某个客服的id */
|
|
382
|
+
receiverIds: string[];
|
|
383
|
+
/** 聊天的名称,默认为 `用户xxx` */
|
|
384
|
+
name?: string;
|
|
385
|
+
}
|
|
386
|
+
interface ChatSdkEventData {
|
|
387
|
+
[ChatSdkEvent.SDK_READY]: undefined;
|
|
388
|
+
[ChatSdkEvent.SDK_NOT_READY]: undefined;
|
|
389
|
+
[ChatSdkEvent.SDK_KICKOUT]: undefined;
|
|
390
|
+
[ChatSdkEvent.FEED_UPDATED]: Feed;
|
|
391
|
+
[ChatSdkEvent.MESSAGE_RECEIVED]: Message;
|
|
392
|
+
[ChatSdkEvent.MESSAGE_UPDATED]: Message;
|
|
393
|
+
[ChatSdkEvent.MESSAGE_RECEIPT_RECEIVED]: {
|
|
394
|
+
chatId: string;
|
|
395
|
+
messageIds: string[];
|
|
396
|
+
readerUserIds: string[];
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
interface AddGroupMemberResp {
|
|
400
|
+
user_id: string;
|
|
401
|
+
chat_id: string;
|
|
402
|
+
chatter_id: string;
|
|
403
|
+
name: string;
|
|
404
|
+
avatar: string;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
type ChatSdkResponse<T> = HttpResponse<T>;
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* 0-打印所有日志
|
|
411
|
+
* 1-只打印warning和error
|
|
412
|
+
* 2-只打印error
|
|
413
|
+
* 3-不打印日志
|
|
414
|
+
*/
|
|
415
|
+
type LogLevel = 0 | 1 | 2 | 3;
|
|
416
|
+
|
|
417
|
+
declare function createChatSdk(appId: string, token: string, config: ChatSDKConfig): {
|
|
418
|
+
/** 设置日志级别 */
|
|
419
|
+
setLogLevel(level: LogLevel): void;
|
|
420
|
+
/** 登录 */
|
|
421
|
+
login(data?: LoginData): Promise<ChatSdkResponse<User>>;
|
|
422
|
+
/** 退出登录 */
|
|
423
|
+
logout(): Promise<ChatSdkResponse<null>>;
|
|
424
|
+
/** 初始化聊天 */
|
|
425
|
+
initialChat(data: InitialChatData): Promise<ChatSdkResponse<string>>;
|
|
426
|
+
/** 添加群成员 */
|
|
427
|
+
addGroupMember(chatId: string, receiverIds: string[]): Promise<ChatSdkResponse<AddGroupMemberResp[] | null>>;
|
|
428
|
+
/** 获取当前 feed 信息 */
|
|
429
|
+
getCurrentFeed(): Promise<ChatSdkResponse<Feed>>;
|
|
430
|
+
/** 获取消息列表 */
|
|
431
|
+
getMessageList(data: GetMessageListData): Promise<ChatSdkResponse<GetMessageListResp>>;
|
|
432
|
+
/** 上传附件(图片、视频、文件) */
|
|
433
|
+
uploadFile(data: UploadFileData): Promise<HttpResponse<{
|
|
434
|
+
objectKey: string;
|
|
435
|
+
address: string;
|
|
436
|
+
width: number;
|
|
437
|
+
height: number;
|
|
438
|
+
}>>;
|
|
439
|
+
/** 发送消息 */
|
|
440
|
+
sendMessage(data: SendMessageData): Promise<ChatSdkResponse<Message>>;
|
|
441
|
+
/** 发送消息已读回执 */
|
|
442
|
+
sendMessageReadReceipt(data: SendMessageReadReceiptData): Promise<ChatSdkResponse<null>>;
|
|
443
|
+
/** 将所有未读消息标记为已读 */
|
|
444
|
+
markAllMessagesAsRead(chatId: string): Promise<ChatSdkResponse<null>>;
|
|
445
|
+
/** 撤回消息 */
|
|
446
|
+
recallMessage(data: RecallMessageData): Promise<ChatSdkResponse<null>>;
|
|
447
|
+
/** 当前用户id */
|
|
448
|
+
readonly currentUserId: string | undefined;
|
|
449
|
+
/** 当前chat id */
|
|
450
|
+
readonly currentChatId: string | undefined;
|
|
451
|
+
/** 是否已准备 */
|
|
452
|
+
readonly isReady: boolean;
|
|
453
|
+
on<K extends ChatSdkEvent>(event: K, handler: (data: ChatSdkEventData[K]) => void): void;
|
|
454
|
+
once<K extends ChatSdkEvent>(event: K, handler: (data: ChatSdkEventData[K]) => void): void;
|
|
455
|
+
off<K extends ChatSdkEvent>(event: K, handler?: (data: ChatSdkEventData[K]) => void): void;
|
|
456
|
+
};
|
|
457
|
+
type ChatSdk = ReturnType<typeof createChatSdk>;
|
|
458
|
+
|
|
459
|
+
declare const _default: {
|
|
460
|
+
create: typeof createChatSdk;
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
export { ChatSdkErrorCode, ChatSdkEvent, ChatType, MessageKind, MessageOperation, MessageType, RequestDirection, _default as default };
|
|
464
|
+
export type { AddGroupMemberResp, ChatSDKConfig, ChatSdk, ChatSdkEventData, ChatSdkResponse, Feed, GetMessageListData, GetMessageListResp, InitialChatData, LoginData, Message, RecallMessageData, SendFileMsgData, SendImageMsgData, SendMessageCommonData, SendMessageData, SendMessageReadReceiptData, SendTextMsgData, SendVideoMsgData, UploadFileData, UploadFileType, User };
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@typexim/agent-js-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": {
|
|
10
|
+
"types": "./dist/types/index.d.ts",
|
|
11
|
+
"default": "./dist/index.es.js"
|
|
12
|
+
},
|
|
13
|
+
"require": {
|
|
14
|
+
"types": "./dist/types/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"main": "./dist/index.es.js",
|
|
20
|
+
"types": "./dist/types/index.d.ts",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@fingerprintjs/fingerprintjs": "^5.1.0",
|
|
26
|
+
"axios": "1.11.0",
|
|
27
|
+
"crypto-js": "4.2.0",
|
|
28
|
+
"js-cookie": "3.0.5"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@rollup/plugin-alias": "^5.1.1",
|
|
32
|
+
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
33
|
+
"@rollup/plugin-terser": "0.4.4",
|
|
34
|
+
"@rollup/plugin-typescript": "12.1.4",
|
|
35
|
+
"@types/crypto-js": "4.2.2",
|
|
36
|
+
"@types/js-cookie": "3.0.6",
|
|
37
|
+
"cross-env": "10.0.0",
|
|
38
|
+
"rollup": "4.50.1",
|
|
39
|
+
"rollup-plugin-delete": "3.0.1",
|
|
40
|
+
"rollup-plugin-dts": "^6.3.0"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20.0.0"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public",
|
|
47
|
+
"registry": "https://registry.npmjs.org/"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build:dev": "cross-env MODE=dev rollup -c",
|
|
51
|
+
"build:prod": "cross-env MODE=prod rollup -c",
|
|
52
|
+
"dev": "cross-env MODE=dev rollup -c -w",
|
|
53
|
+
"release": "yarn run build:prod && pnpm publish --no-git-checks",
|
|
54
|
+
"yalc:release": "yarn run build:dev && yalc publish"
|
|
55
|
+
}
|
|
56
|
+
}
|