@memtensor/memos-local-openclaw-plugin 1.0.4-beta.12 → 1.0.4-beta.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memtensor/memos-local-openclaw-plugin",
3
- "version": "1.0.4-beta.12",
3
+ "version": "1.0.4-beta.13",
4
4
  "description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -208,20 +208,55 @@ export async function getHubStatus(store: SqliteStore, config: MemosLocalConfig)
208
208
  } catch (err: any) {
209
209
  const is401 = typeof err?.message === "string" && err.message.includes("(401)");
210
210
  if (is401 && conn) {
211
- store.setClientHubConnection({
212
- ...conn,
213
- userToken: "",
214
- lastKnownStatus: "removed",
215
- });
211
+ const teamToken = config.sharing?.client?.teamToken ?? "";
212
+ if (hubAddress && teamToken) {
213
+ try {
214
+ const regResult = await hubRequestJson(normalizeHubUrl(hubAddress), "", "/api/v1/hub/registration-status", {
215
+ method: "POST",
216
+ body: JSON.stringify({ teamToken, userId: conn.userId }),
217
+ }) as any;
218
+ if (regResult.status === "active" && regResult.userToken) {
219
+ store.setClientHubConnection({
220
+ ...conn,
221
+ hubUrl: normalizeHubUrl(hubAddress),
222
+ userToken: regResult.userToken,
223
+ connectedAt: Date.now(),
224
+ lastKnownStatus: "active",
225
+ });
226
+ try {
227
+ const me = await hubRequestJson(normalizeHubUrl(hubAddress), regResult.userToken, "/api/v1/hub/me", { method: "GET" }) as any;
228
+ return {
229
+ connected: true,
230
+ hubUrl: normalizeHubUrl(hubAddress),
231
+ user: {
232
+ id: String(me.id),
233
+ username: String(me.username ?? ""),
234
+ role: String(me.role ?? "member") as UserRole,
235
+ status: String(me.status ?? "active"),
236
+ groups: Array.isArray(me.groups) ? me.groups : [],
237
+ },
238
+ };
239
+ } catch { /* fall through to token-only return */ }
240
+ return {
241
+ connected: true,
242
+ hubUrl: normalizeHubUrl(hubAddress),
243
+ user: { id: conn.userId, username: conn.username || "", role: conn.role as UserRole || "member", status: "active" },
244
+ };
245
+ }
246
+ const realStatus = regResult.status as string;
247
+ store.setClientHubConnection({ ...conn, userToken: "", lastKnownStatus: realStatus });
248
+ return {
249
+ connected: false,
250
+ hubUrl: normalizeHubUrl(hubAddress),
251
+ user: { id: conn.userId, username: conn.username || "", role: "member", status: realStatus },
252
+ };
253
+ } catch { /* registration-status also failed, fall through */ }
254
+ }
255
+ store.setClientHubConnection({ ...conn, userToken: "", lastKnownStatus: "token_expired" });
216
256
  return {
217
257
  connected: false,
218
258
  hubUrl: normalizeHubUrl(hubAddress),
219
- user: {
220
- id: conn.userId,
221
- username: conn.username || "",
222
- role: "member",
223
- status: "removed",
224
- },
259
+ user: { id: conn.userId, username: conn.username || "", role: "member", status: "token_expired" },
225
260
  };
226
261
  }
227
262
  return { connected: false, user: null };
@@ -976,8 +976,6 @@ input,textarea,select{font-family:inherit;font-size:inherit}
976
976
  .team-guide-steps li::marker{color:var(--pri);font-weight:700;font-size:11px}
977
977
  .team-guide-opt .btn-guide{font-size:11px;padding:5px 14px;border-radius:6px;font-weight:600;border:1px solid rgba(99,102,241,.25);background:rgba(99,102,241,.08);color:var(--pri);cursor:pointer;transition:background .15s,border-color .15s}
978
978
  .team-guide-opt .btn-guide:hover{background:rgba(99,102,241,.14);border-color:var(--pri)}
979
- .team-guide-dismiss{position:absolute;top:10px;right:12px;background:none;border:none;color:var(--text-muted);font-size:15px;cursor:pointer;padding:4px;line-height:1;opacity:.5;transition:opacity .15s}
980
- .team-guide-dismiss:hover{opacity:1}
981
979
  [data-theme="light"] .team-guide{background:linear-gradient(135deg,rgba(6,182,212,.03),rgba(79,70,229,.02));border-color:rgba(6,182,212,.15)}
982
980
  [data-theme="light"] .team-guide-opt{box-shadow:0 1px 3px rgba(0,0,0,.03)}
983
981
  [data-theme="light"] .team-guide-opt:hover{box-shadow:0 4px 16px rgba(0,0,0,.04)}
@@ -1641,9 +1639,8 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1641
1639
  </div>
1642
1640
  </div>
1643
1641
  <div class="settings-card-body">
1644
- <!-- team setup guide (inside Hub card) -->
1642
+ <!-- team setup guide (inside Hub card) — always visible when sharing is not configured -->
1645
1643
  <div class="team-guide" id="teamSetupGuide">
1646
- <button class="team-guide-dismiss" onclick="dismissTeamGuide()" title="Dismiss">&times;</button>
1647
1644
  <div class="team-guide-title">\u{1F680} <span data-i18n="guide.title">Get Started with Team Collaboration</span></div>
1648
1645
  <div class="team-guide-subtitle" data-i18n="guide.subtitle">MemOS supports team memory sharing. Choose one of the following options to enable collaboration, or continue using local-only mode.</div>
1649
1646
  <div class="team-guide-options">
@@ -2126,6 +2123,8 @@ const I18N={
2126
2123
  'notif.userJoin':'New user requests to join the team',
2127
2124
  'notif.userOnline':'User came online',
2128
2125
  'notif.userOffline':'User went offline',
2126
+ 'notif.membershipApproved':'Your team join request has been approved',
2127
+ 'notif.membershipRejected':'Your team join request has been declined',
2129
2128
  'notif.clearAll':'Clear all',
2130
2129
  'notif.timeAgo.just':'just now',
2131
2130
  'notif.timeAgo.min':'{n}m ago',
@@ -2448,6 +2447,9 @@ const I18N={
2448
2447
  'sharing.pendingApproval.hint':'Your join request has been submitted. Please wait for the team admin to approve.',
2449
2448
  'sharing.rejected.hint':'Your join request was rejected by the team admin. Please contact the admin or retry.',
2450
2449
  'sharing.removed.hint':'You have been removed from the team by the admin. You can re-apply to join.',
2450
+ 'sharing.joinTeam':'Join Team',
2451
+ 'sharing.joinSent.pending':'Join request sent! Waiting for admin approval.',
2452
+ 'sharing.joinSent.active':'Successfully joined the team!',
2451
2453
  'sharing.retryJoin':'Retry Join',
2452
2454
  'sharing.retryJoin.hint':'Clears local data and re-submits the join request',
2453
2455
  'sharing.retryJoin.confirm':'This will clear your current connection and re-submit a join request. Continue?',
@@ -2840,6 +2842,8 @@ const I18N={
2840
2842
  'notif.userJoin':'有新用户申请加入团队',
2841
2843
  'notif.userOnline':'用户上线了',
2842
2844
  'notif.userOffline':'用户下线了',
2845
+ 'notif.membershipApproved':'你的团队加入申请已通过',
2846
+ 'notif.membershipRejected':'你的团队加入申请已被拒绝',
2843
2847
  'notif.clearAll':'清除全部',
2844
2848
  'notif.timeAgo.just':'刚刚',
2845
2849
  'notif.timeAgo.min':'{n}分钟前',
@@ -3162,6 +3166,9 @@ const I18N={
3162
3166
  'sharing.pendingApproval.hint':'加入申请已提交,请等待团队管理员审核通过。',
3163
3167
  'sharing.rejected.hint':'您的加入申请已被团队管理员拒绝,请联系管理员或重新申请。',
3164
3168
  'sharing.removed.hint':'您已被管理员从团队中移除,可以重新申请加入。',
3169
+ 'sharing.joinTeam':'加入团队',
3170
+ 'sharing.joinSent.pending':'加入申请已发送,等待管理员审批。',
3171
+ 'sharing.joinSent.active':'成功加入团队!',
3165
3172
  'sharing.retryJoin':'重新申请',
3166
3173
  'sharing.retryJoin.hint':'清除本地连接数据并重新提交加入申请',
3167
3174
  'sharing.retryJoin.confirm':'这将清除当前连接数据并重新提交加入申请,是否继续?',
@@ -3606,6 +3613,9 @@ function selectSharingRole(role){
3606
3613
  var tn=document.getElementById('cfgHubTeamName');
3607
3614
  if(!tn.value.trim()) tn.value='My Team';
3608
3615
  }
3616
+ var card=document.getElementById('settingsSharingConfig');
3617
+ var saveBtn=card&&card.querySelector('.settings-actions .btn-primary');
3618
+ if(saveBtn&&typeof _hubSaveBtnLabel==='function') saveBtn.textContent=_hubSaveBtnLabel();
3609
3619
  }
3610
3620
  var _cachedLocalIP='';
3611
3621
  function updateHubShareInfo(){
@@ -3697,15 +3707,14 @@ function switchView(view){
3697
3707
  else if(view==='analytics') loadMetrics();
3698
3708
  else if(view==='logs') loadLogs();
3699
3709
  else if(view==='settings'){loadConfig().then(function(){
3700
- var notDismissed=localStorage.getItem('memos-team-guide-dismissed')!=='1';
3701
3710
  var sharingOn=document.getElementById('cfgSharingEnabled');
3702
3711
  var sharingNotEnabled=!sharingOn||!sharingOn.checked;
3703
- if(notDismissed&&sharingNotEnabled){
3712
+ if(sharingNotEnabled){
3704
3713
  switchSettingsTab('hub',document.querySelector('.settings-tab-btn[data-tab="hub"]'));
3705
3714
  }
3706
3715
  });loadModelHealth();}
3707
3716
  else if(view==='import'){if(!window._migrateRunning) migrateScan(false);}
3708
- else if(view==='admin'){loadAdminData();}
3717
+ else if(view==='admin'){_lastAdminFingerprint='';loadAdminData();}
3709
3718
  }
3710
3719
 
3711
3720
  function onMemoryScopeChange(){
@@ -3775,10 +3784,13 @@ async function loadSharingStatus(forcePending){
3775
3784
  if(_lastSharingConnStatus==='pending'&&curStatus==='connected'){
3776
3785
  toast(t('sharing.approved.toast'),'success');
3777
3786
  loadMemories();loadTasks();loadSkills();
3787
+ if(_notifSSE){_notifSSE.close();_notifSSE=null;_notifSSEConnected=false;}
3788
+ connectNotifSSE();
3789
+ loadNotifications();
3778
3790
  }
3779
3791
  _lastSharingConnStatus=curStatus;
3780
3792
  if(curStatus==='pending'&&!_clientPendingPollTimer){
3781
- _clientPendingPollTimer=setInterval(function(){loadSharingStatus(false);},10000);
3793
+ _clientPendingPollTimer=setInterval(function(){loadSharingStatus(false);},5000);
3782
3794
  }
3783
3795
  if(curStatus!=='pending'&&_clientPendingPollTimer){
3784
3796
  clearInterval(_clientPendingPollTimer);
@@ -4059,7 +4071,7 @@ async function approveSharingUser(userId,username){
4059
4071
  try{
4060
4072
  const r=await fetch('/api/sharing/approve-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,username:username})});
4061
4073
  const d=await r.json();
4062
- if(d.ok){toast(t('toast.userApproved'),'success');loadSharingPendingUsers();loadSharingStatus(true);} else {toast(d.error||t('toast.approveFail'),'error');}
4074
+ if(d.ok){toast(t('toast.userApproved'),'success');loadSharingPendingUsers();loadSharingStatus(true);_lastAdminFingerprint='';loadAdminData();} else {toast(d.error||t('toast.approveFail'),'error');}
4063
4075
  }catch(e){toast(t('toast.approveFail')+': '+e.message,'error');}
4064
4076
  }
4065
4077
 
@@ -4067,24 +4079,17 @@ async function rejectSharingUser(userId,username){
4067
4079
  try{
4068
4080
  const r=await fetch('/api/sharing/reject-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,username:username})});
4069
4081
  const d=await r.json();
4070
- if(d.ok){toast(t('toast.userRejected'),'success');loadSharingPendingUsers();} else {toast(d.error||t('toast.rejectFail'),'error');}
4082
+ if(d.ok){toast(t('toast.userRejected'),'success');loadSharingPendingUsers();_lastAdminFingerprint='';loadAdminData();} else {toast(d.error||t('toast.rejectFail'),'error');}
4071
4083
  }catch(e){toast(t('toast.rejectFail')+': '+e.message,'error');}
4072
4084
  }
4073
4085
 
4074
4086
  /* ─── Team Setup Guide ─── */
4075
- var TEAM_GUIDE_DISMISSED_KEY='memos-team-guide-dismissed';
4076
4087
  function updateTeamGuide(sharingData){
4077
4088
  var el=document.getElementById('teamSetupGuide');
4078
4089
  if(!el) return;
4079
- if(localStorage.getItem(TEAM_GUIDE_DISMISSED_KEY)==='1'){el.style.display='none';return;}
4080
4090
  var isConfigured=sharingData&&sharingData.enabled;
4081
4091
  el.style.display=isConfigured?'none':'block';
4082
4092
  }
4083
- function dismissTeamGuide(){
4084
- localStorage.setItem(TEAM_GUIDE_DISMISSED_KEY,'1');
4085
- var el=document.getElementById('teamSetupGuide');
4086
- if(el) el.style.display='none';
4087
- }
4088
4093
  function guideGoToHub(role){
4089
4094
  switchSettingsTab('hub',document.querySelector('.settings-tab-btn[data-tab="hub"]'));
4090
4095
  var chk=document.getElementById('cfgSharingEnabled');
@@ -4200,7 +4205,7 @@ async function loadAdminData(){
4200
4205
  var _newMemories=Array.isArray(memoriesR.memories)?memoriesR.memories:[];
4201
4206
  var pending=isAdmin?(Array.isArray(pendingR.users)?pendingR.users:[]):[];
4202
4207
  var _fp=_newUsers.length+':'+_newTasks.length+':'+_newSkills.length+':'+_newMemories.length+':'+pending.length
4203
- +':'+_newUsers.map(function(u){return u.id+'|'+(u.isOnline?1:0)+'|'+(u.role||'')+'|'+(u.username||'')+'|'+(u.memoryCount||0)+'|'+(u.taskCount||0)+'|'+(u.skillCount||0)}).join(',')
4208
+ +':'+_newUsers.map(function(u){return u.id+'|'+(u.isOnline?1:0)+'|'+(u.role||'')+'|'+(u.status||'')+'|'+(u.username||'')+'|'+(u.memoryCount||0)+'|'+(u.taskCount||0)+'|'+(u.skillCount||0)}).join(',')
4204
4209
  +':'+_newMemories.map(function(m){return m.id}).join(',')
4205
4210
  +':'+_newTasks.map(function(t){return t.id+'|'+(t.status||'')}).join(',')
4206
4211
  +':'+_newSkills.map(function(s){return s.id+'|'+(s.status||'')}).join(',')
@@ -4396,7 +4401,7 @@ async function adminApproveUser(userId,username){
4396
4401
  try{
4397
4402
  var r=await fetch('/api/sharing/approve-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,username:username})});
4398
4403
  var d=await r.json();
4399
- if(d.ok){toast(t('toast.userApproved'),'success');loadAdminData();}else{toast(d.error||t('toast.approveFail'),'error');}
4404
+ if(d.ok){toast(t('toast.userApproved'),'success');_lastAdminFingerprint='';loadAdminData();}else{toast(d.error||t('toast.approveFail'),'error');}
4400
4405
  }catch(e){toast(t('toast.approveFail')+': '+e.message,'error');}
4401
4406
  }
4402
4407
  async function adminRejectUser(userId){
@@ -4404,7 +4409,7 @@ async function adminRejectUser(userId){
4404
4409
  try{
4405
4410
  var r=await fetch('/api/sharing/reject-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId})});
4406
4411
  var d=await r.json();
4407
- if(d.ok){toast(t('toast.userRejected'),'success');loadAdminData();}else{toast(d.error||t('toast.rejectFail'),'error');}
4412
+ if(d.ok){toast(t('toast.userRejected'),'success');_lastAdminFingerprint='';loadAdminData();}else{toast(d.error||t('toast.rejectFail'),'error');}
4408
4413
  }catch(e){toast(t('toast.rejectFail')+': '+e.message,'error');}
4409
4414
  }
4410
4415
  async function adminToggleRole(userId,newRole){
@@ -4413,7 +4418,7 @@ async function adminToggleRole(userId,newRole){
4413
4418
  try{
4414
4419
  var r=await fetch('/api/sharing/change-role',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,role:newRole})});
4415
4420
  var d=await r.json();
4416
- if(d.ok){toast(t('toast.roleChanged'),'success');loadAdminData();}
4421
+ if(d.ok){toast(t('toast.roleChanged'),'success');_lastAdminFingerprint='';loadAdminData();}
4417
4422
  else if(d.error==='cannot_demote_owner'){toast(t('admin.ownerHint'),'error');}
4418
4423
  else{toast(d.error||t('toast.roleChangeFail'),'error');}
4419
4424
  }catch(e){toast(t('toast.roleChangeFail')+': '+e.message,'error');}
@@ -4465,7 +4470,7 @@ async function adminRemoveUser(userId,username){
4465
4470
  try{
4466
4471
  var r=await fetch('/api/sharing/remove-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,cleanResources:clean})});
4467
4472
  var d=await r.json();
4468
- if(d.ok){toast(t('toast.userRemoved'),'success');loadAdminData();}
4473
+ if(d.ok){toast(t('toast.userRemoved'),'success');_lastAdminFingerprint='';loadAdminData();}
4469
4474
  else if(d.error==='cannot_remove_owner'){toast(t('admin.ownerHint'),'error');}
4470
4475
  else{toast(d.error||t('toast.removeFail'),'error');}
4471
4476
  }catch(e){toast(t('toast.removeFail')+': '+e.message,'error');}
@@ -6597,16 +6602,16 @@ async function doSaveConfig(cfg, btnEl, savedId){
6597
6602
  function done(){btnEl.disabled=false;btnEl.textContent=t('settings.save');}
6598
6603
  try{
6599
6604
  const r=await fetch('/api/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(cfg)});
6600
- if(r.status===401){done();toast(t('settings.session.expired'),'error');return false;}
6605
+ if(r.status===401){done();toast(t('settings.session.expired'),'error');return null;}
6601
6606
  if(!r.ok) throw new Error(await r.text());
6607
+ var data=await r.json().catch(function(){return {ok:true};});
6602
6608
  flashSaved(savedId);
6603
- toast(t('settings.saved'),'success');
6604
6609
  done();
6605
- return true;
6610
+ return data;
6606
6611
  }catch(e){
6607
6612
  toast(t('settings.save.fail')+': '+e.message,'error');
6608
6613
  done();
6609
- return false;
6614
+ return null;
6610
6615
  }
6611
6616
  }
6612
6617
 
@@ -6701,11 +6706,19 @@ async function saveModelsConfig(){
6701
6706
  await doSaveConfig(cfg, saveBtn, 'modelsSaved');
6702
6707
  }
6703
6708
 
6709
+ function _hubSaveBtnLabel(){
6710
+ var on=document.getElementById('cfgSharingEnabled');
6711
+ if(on&&on.checked&&_sharingRole==='client'){
6712
+ var prevClient=sharingStatusCache&&sharingStatusCache.enabled&&sharingStatusCache.role==='client';
6713
+ return prevClient?t('settings.save'):t('sharing.joinTeam');
6714
+ }
6715
+ return t('settings.save');
6716
+ }
6704
6717
  async function saveHubConfig(){
6705
6718
  var card=document.getElementById('settingsSharingConfig');
6706
6719
  var saveBtn=card.querySelector('.settings-actions .btn-primary');
6707
6720
  saveBtn.disabled=true;saveBtn.textContent=t('settings.test.loading');
6708
- function done(){saveBtn.disabled=false;saveBtn.textContent=t('settings.save');}
6721
+ function done(){saveBtn.disabled=false;saveBtn.textContent=_hubSaveBtnLabel();}
6709
6722
 
6710
6723
  const cfg={};
6711
6724
  var sharingEnabled=document.getElementById('cfgSharingEnabled').checked;
@@ -6772,14 +6785,25 @@ async function saveHubConfig(){
6772
6785
  if(!(await confirmModal(switchMsg,{danger:true}))){done();return;}
6773
6786
  }
6774
6787
 
6775
- var ok=await doSaveConfig(cfg, saveBtn, 'hubSaved');
6776
- if(ok){
6788
+ var result=await doSaveConfig(cfg, saveBtn, 'hubSaved');
6789
+ if(result){
6777
6790
  if(sharingEnabled&&_sharingRole==='hub'){
6778
6791
  var adminNameEl=document.getElementById('cfgHubAdminName');
6779
6792
  if(adminNameEl&&adminNameEl.value.trim()){
6780
6793
  try{await fetch('/api/sharing/update-username',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:adminNameEl.value.trim()})});}catch(e){}
6781
6794
  }
6782
6795
  }
6796
+ if(sharingEnabled&&_sharingRole==='client'&&result.joinStatus){
6797
+ if(result.joinStatus==='pending'){
6798
+ toast(t('sharing.joinSent.pending'),'success');
6799
+ }else if(result.joinStatus==='active'){
6800
+ toast(t('sharing.joinSent.active'),'success');
6801
+ }else{
6802
+ toast(t('settings.saved'),'success');
6803
+ }
6804
+ }else{
6805
+ toast(t('settings.saved'),'success');
6806
+ }
6783
6807
  _lastSidebarFingerprint='';
6784
6808
  _lastSettingsFingerprint='';
6785
6809
  _lastSharingConnStatus='';
@@ -7354,6 +7378,12 @@ function notifTypeText(n){
7354
7378
  if(n.type==='user_offline'){
7355
7379
  return t('notif.userOffline');
7356
7380
  }
7381
+ if(n.type==='membership_approved'){
7382
+ return t('notif.membershipApproved');
7383
+ }
7384
+ if(n.type==='membership_rejected'){
7385
+ return t('notif.membershipRejected');
7386
+ }
7357
7387
  return n.message||n.type;
7358
7388
  }
7359
7389
 
@@ -2440,6 +2440,7 @@ export class ViewerServer {
2440
2440
  res.write("data: {\"type\":\"connected\"}\n\n");
2441
2441
  this.notifSSEClients.push(res);
2442
2442
  if (!this.notifPollTimer) this.startNotifPoll();
2443
+ else this.notifPollImmediate();
2443
2444
  req.on("close", () => {
2444
2445
  this.notifSSEClients = this.notifSSEClients.filter((c) => c !== res);
2445
2446
  if (this.notifSSEClients.length === 0) this.stopNotifPoll();
@@ -2475,6 +2476,20 @@ export class ViewerServer {
2475
2476
  if (this.notifPollTimer) { clearInterval(this.notifPollTimer); this.notifPollTimer = undefined; }
2476
2477
  }
2477
2478
 
2479
+ private notifPollImmediate(): void {
2480
+ const hub = this.resolveHubConnection();
2481
+ if (!hub) return;
2482
+ hubRequestJson(hub.hubUrl, hub.userToken, "/api/v1/hub/notifications?unread=1")
2483
+ .then((data: any) => {
2484
+ const count = data?.unreadCount ?? 0;
2485
+ if (count !== this.lastKnownNotifCount) {
2486
+ this.lastKnownNotifCount = count;
2487
+ this.broadcastNotifSSE({ type: "update", unreadCount: count });
2488
+ }
2489
+ })
2490
+ .catch(() => {});
2491
+ }
2492
+
2478
2493
  private startHubHeartbeat(): void {
2479
2494
  this.stopHubHeartbeat();
2480
2495
  const sendHeartbeat = async () => {
@@ -2660,11 +2675,16 @@ export class ViewerServer {
2660
2675
  const finalSharing = config.sharing as Record<string, unknown> | undefined;
2661
2676
  const nowClient = Boolean(finalSharing?.enabled) && finalSharing?.role === "client";
2662
2677
  const previouslyClient = oldSharingEnabled && oldSharingRole === "client";
2678
+ let joinStatus: string | undefined;
2663
2679
  if (nowClient && !previouslyClient) {
2664
- this.autoJoinOnSave(finalSharing).catch(e => this.log.warn(`Auto-join on save failed: ${e}`));
2680
+ try {
2681
+ joinStatus = await this.autoJoinOnSave(finalSharing);
2682
+ } catch (e) {
2683
+ this.log.warn(`Auto-join on save failed: ${e}`);
2684
+ }
2665
2685
  }
2666
2686
 
2667
- this.jsonResponse(res, { ok: true });
2687
+ this.jsonResponse(res, { ok: true, joinStatus });
2668
2688
  } catch (e) {
2669
2689
  this.log.warn(`handleSaveConfig error: ${e}`);
2670
2690
  res.writeHead(500, { "Content-Type": "application/json" });
@@ -2673,11 +2693,11 @@ export class ViewerServer {
2673
2693
  });
2674
2694
  }
2675
2695
 
2676
- private async autoJoinOnSave(sharing: Record<string, unknown>): Promise<void> {
2696
+ private async autoJoinOnSave(sharing: Record<string, unknown>): Promise<string | undefined> {
2677
2697
  const clientCfg = sharing.client as Record<string, unknown> | undefined;
2678
2698
  const hubAddress = String(clientCfg?.hubAddress || "");
2679
2699
  const teamToken = String(clientCfg?.teamToken || "");
2680
- if (!hubAddress || !teamToken) return;
2700
+ if (!hubAddress || !teamToken) return undefined;
2681
2701
  const hubUrl = normalizeHubUrl(hubAddress);
2682
2702
  const os = await import("os");
2683
2703
  const nickname = String(clientCfg?.nickname || "");
@@ -2704,6 +2724,7 @@ export class ViewerServer {
2704
2724
  if (result.userToken) {
2705
2725
  this.startHubHeartbeat();
2706
2726
  }
2727
+ return result.status;
2707
2728
  }
2708
2729
 
2709
2730
  private async notifyHubLeave(): Promise<void> {