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

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.14",
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 };
package/src/config.ts CHANGED
@@ -128,7 +128,8 @@ export function resolveConfig(raw: Partial<MemosLocalConfig> | undefined, stateD
128
128
  userToken: cfg.sharing?.client?.userToken ?? "",
129
129
  teamToken: cfg.sharing?.client?.teamToken ?? "",
130
130
  pendingUserId: cfg.sharing?.client?.pendingUserId ?? "",
131
- } : { hubAddress: "", userToken: "", teamToken: "", pendingUserId: "" };
131
+ nickname: cfg.sharing?.client?.nickname ?? "",
132
+ } : { hubAddress: "", userToken: "", teamToken: "", pendingUserId: "", nickname: "" };
132
133
  return { enabled, role, hub, client, capabilities: sharingCapabilities };
133
134
  })(),
134
135
  };
package/src/hub/server.ts CHANGED
@@ -314,6 +314,7 @@ export class HubServer {
314
314
  { userId: user.id, username: user.username, role: user.role, status: user.status },
315
315
  this.authSecret,
316
316
  );
317
+ this.userManager.approveUser(user.id, token);
317
318
  return this.json(res, 200, { status: "active", userToken: token });
318
319
  }
319
320
  return this.json(res, 200, { status: user.status });
@@ -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':'这将清除当前连接数据并重新提交加入申请,是否继续?',
@@ -3592,20 +3599,25 @@ function selectSharingRole(role){
3592
3599
  var tp=document.getElementById('sharingTeamPanel');
3593
3600
  var ap=document.getElementById('sharingAdminPanel');
3594
3601
  if(role==='client'){
3595
- if(sp) sp.style.display='none';
3602
+ if(sp) { sp.style.display='none'; sp.innerHTML=''; }
3596
3603
  if(tp) tp.style.display='none';
3597
3604
  if(ap) ap.style.display='none';
3598
3605
  }else{
3599
- if(sp) sp.style.display='';
3606
+ if(sp) { sp.style.display='none'; sp.innerHTML=''; }
3600
3607
  if(tp) tp.style.display='';
3601
- if(ap) ap.style.display='';
3608
+ if(ap) tp.style.display='';
3602
3609
  }
3610
+ _lastSettingsFingerprint='';
3611
+ setTimeout(function(){ loadSharingStatus(true); },200);
3603
3612
  if(role==='hub'){
3604
3613
  var tk=document.getElementById('cfgHubTeamToken');
3605
3614
  if(!tk.value.trim()) tk.value=_genToken(18);
3606
3615
  var tn=document.getElementById('cfgHubTeamName');
3607
3616
  if(!tn.value.trim()) tn.value='My Team';
3608
3617
  }
3618
+ var card=document.getElementById('settingsSharingConfig');
3619
+ var saveBtn=card&&card.querySelector('.settings-actions .btn-primary');
3620
+ if(saveBtn&&typeof _hubSaveBtnLabel==='function') saveBtn.textContent=_hubSaveBtnLabel();
3609
3621
  }
3610
3622
  var _cachedLocalIP='';
3611
3623
  function updateHubShareInfo(){
@@ -3697,19 +3709,19 @@ function switchView(view){
3697
3709
  else if(view==='analytics') loadMetrics();
3698
3710
  else if(view==='logs') loadLogs();
3699
3711
  else if(view==='settings'){loadConfig().then(function(){
3700
- var notDismissed=localStorage.getItem('memos-team-guide-dismissed')!=='1';
3701
3712
  var sharingOn=document.getElementById('cfgSharingEnabled');
3702
3713
  var sharingNotEnabled=!sharingOn||!sharingOn.checked;
3703
- if(notDismissed&&sharingNotEnabled){
3714
+ if(sharingNotEnabled){
3704
3715
  switchSettingsTab('hub',document.querySelector('.settings-tab-btn[data-tab="hub"]'));
3705
3716
  }
3706
3717
  });loadModelHealth();}
3707
3718
  else if(view==='import'){if(!window._migrateRunning) migrateScan(false);}
3708
- else if(view==='admin'){loadAdminData();}
3719
+ else if(view==='admin'){_lastAdminFingerprint='';loadAdminData();}
3709
3720
  }
3710
3721
 
3711
3722
  function onMemoryScopeChange(){
3712
3723
  memorySearchScope=document.getElementById('memorySearchScope')?.value||'local';
3724
+ try{localStorage.setItem('memos_memorySearchScope',memorySearchScope);}catch(e){}
3713
3725
  currentPage=1;
3714
3726
  activeSession=null;activeRole='';
3715
3727
  _lastMemoriesFingerprint='';
@@ -3775,10 +3787,13 @@ async function loadSharingStatus(forcePending){
3775
3787
  if(_lastSharingConnStatus==='pending'&&curStatus==='connected'){
3776
3788
  toast(t('sharing.approved.toast'),'success');
3777
3789
  loadMemories();loadTasks();loadSkills();
3790
+ if(_notifSSE){_notifSSE.close();_notifSSE=null;_notifSSEConnected=false;}
3791
+ connectNotifSSE();
3792
+ loadNotifications();
3778
3793
  }
3779
3794
  _lastSharingConnStatus=curStatus;
3780
3795
  if(curStatus==='pending'&&!_clientPendingPollTimer){
3781
- _clientPendingPollTimer=setInterval(function(){loadSharingStatus(false);},10000);
3796
+ _clientPendingPollTimer=setInterval(function(){loadSharingStatus(false);},5000);
3782
3797
  }
3783
3798
  if(curStatus!=='pending'&&_clientPendingPollTimer){
3784
3799
  clearInterval(_clientPendingPollTimer);
@@ -3832,6 +3847,7 @@ function renderSharingSidebar(data){
3832
3847
  html+='<span class="label">'+t('sharing.sidebar.identity')+'</span><span class="value">'+esc(conn.user.username||'-')+'</span>';
3833
3848
  if(conn.teamName) html+='<span class="label">'+t('sharing.team')+'</span><span class="value">'+esc(conn.teamName)+'</span>';
3834
3849
  html+='</div>';
3850
+ html+='<div style="margin-top:8px"><button class="btn btn-sm btn-primary" onclick="retryHubJoin()" style="font-size:11px">'+t('sharing.retryJoin')+'</button></div>';
3835
3851
  statusEl.innerHTML=html;
3836
3852
  hintEl.textContent=t('sharing.rejected.hint');
3837
3853
  }else if(conn.removed&&conn.user){
@@ -3840,6 +3856,7 @@ function renderSharingSidebar(data){
3840
3856
  html+='<span class="label">'+t('sharing.sidebar.identity')+'</span><span class="value">'+esc(conn.user.username||'-')+'</span>';
3841
3857
  if(conn.teamName) html+='<span class="label">'+t('sharing.team')+'</span><span class="value">'+esc(conn.teamName)+'</span>';
3842
3858
  html+='</div>';
3859
+ html+='<div style="margin-top:8px"><button class="btn btn-sm btn-primary" onclick="retryHubJoin()" style="font-size:11px">'+t('sharing.retryJoin')+'</button></div>';
3843
3860
  statusEl.innerHTML=html;
3844
3861
  hintEl.textContent=t('sharing.removed.hint');
3845
3862
  }else if(conn.connected&&conn.user){
@@ -3876,6 +3893,9 @@ function renderSharingSettings(data){
3876
3893
  if(!data||!data.enabled){
3877
3894
  statusEl.innerHTML='';teamEl.innerHTML='';adminEl.innerHTML='';
3878
3895
  if(panelsWrap) panelsWrap.style.display='none';
3896
+ var adminNavTab0=document.querySelector('.tab[data-view="admin"]');
3897
+ if(adminNavTab0) adminNavTab0.style.display='none';
3898
+ if(_activeView==='admin') switchView('memories');
3879
3899
  return;
3880
3900
  }
3881
3901
  if(panelsWrap) panelsWrap.style.display='';
@@ -3883,9 +3903,20 @@ function renderSharingSettings(data){
3883
3903
  var user=conn.user||{};
3884
3904
  var actualRole=data.role||_sharingRole||'client';
3885
3905
  if(data.role) _sharingRole=data.role;
3906
+ var prevIsAdmin=!!window._isHubAdmin;
3886
3907
  var isAdmin=(data.admin&&data.admin.canManageUsers)||(conn.connected&&user.role==='admin')||(actualRole==='hub');
3887
3908
  window._isHubAdmin=isAdmin;
3888
3909
  if(isAdmin) startAdminPoll();
3910
+ var adminNavTab=document.querySelector('.tab[data-view="admin"]');
3911
+ if(adminNavTab){
3912
+ var showTab=(actualRole==='hub')||(conn.connected);
3913
+ adminNavTab.style.display=showTab?'':'none';
3914
+ if(!showTab&&_activeView==='admin') switchView('memories');
3915
+ }
3916
+ if(prevIsAdmin&&!isAdmin&&_activeView==='admin'){
3917
+ _lastAdminFingerprint='';
3918
+ loadAdminData();
3919
+ }
3889
3920
  var hubAdminBtn=document.getElementById('hubAdminEntryBtn');
3890
3921
 
3891
3922
  if(actualRole==='hub'){
@@ -4059,7 +4090,7 @@ async function approveSharingUser(userId,username){
4059
4090
  try{
4060
4091
  const r=await fetch('/api/sharing/approve-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,username:username})});
4061
4092
  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');}
4093
+ if(d.ok){toast(t('toast.userApproved'),'success');loadSharingPendingUsers();loadSharingStatus(true);_lastAdminFingerprint='';loadAdminData();} else {toast(d.error||t('toast.approveFail'),'error');}
4063
4094
  }catch(e){toast(t('toast.approveFail')+': '+e.message,'error');}
4064
4095
  }
4065
4096
 
@@ -4067,24 +4098,17 @@ async function rejectSharingUser(userId,username){
4067
4098
  try{
4068
4099
  const r=await fetch('/api/sharing/reject-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,username:username})});
4069
4100
  const d=await r.json();
4070
- if(d.ok){toast(t('toast.userRejected'),'success');loadSharingPendingUsers();} else {toast(d.error||t('toast.rejectFail'),'error');}
4101
+ if(d.ok){toast(t('toast.userRejected'),'success');loadSharingPendingUsers();_lastAdminFingerprint='';loadAdminData();} else {toast(d.error||t('toast.rejectFail'),'error');}
4071
4102
  }catch(e){toast(t('toast.rejectFail')+': '+e.message,'error');}
4072
4103
  }
4073
4104
 
4074
4105
  /* ─── Team Setup Guide ─── */
4075
- var TEAM_GUIDE_DISMISSED_KEY='memos-team-guide-dismissed';
4076
4106
  function updateTeamGuide(sharingData){
4077
4107
  var el=document.getElementById('teamSetupGuide');
4078
4108
  if(!el) return;
4079
- if(localStorage.getItem(TEAM_GUIDE_DISMISSED_KEY)==='1'){el.style.display='none';return;}
4080
4109
  var isConfigured=sharingData&&sharingData.enabled;
4081
4110
  el.style.display=isConfigured?'none':'block';
4082
4111
  }
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
4112
  function guideGoToHub(role){
4089
4113
  switchSettingsTab('hub',document.querySelector('.settings-tab-btn[data-tab="hub"]'));
4090
4114
  var chk=document.getElementById('cfgSharingEnabled');
@@ -4200,7 +4224,7 @@ async function loadAdminData(){
4200
4224
  var _newMemories=Array.isArray(memoriesR.memories)?memoriesR.memories:[];
4201
4225
  var pending=isAdmin?(Array.isArray(pendingR.users)?pendingR.users:[]):[];
4202
4226
  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(',')
4227
+ +':'+_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
4228
  +':'+_newMemories.map(function(m){return m.id}).join(',')
4205
4229
  +':'+_newTasks.map(function(t){return t.id+'|'+(t.status||'')}).join(',')
4206
4230
  +':'+_newSkills.map(function(s){return s.id+'|'+(s.status||'')}).join(',')
@@ -4396,7 +4420,7 @@ async function adminApproveUser(userId,username){
4396
4420
  try{
4397
4421
  var r=await fetch('/api/sharing/approve-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,username:username})});
4398
4422
  var d=await r.json();
4399
- if(d.ok){toast(t('toast.userApproved'),'success');loadAdminData();}else{toast(d.error||t('toast.approveFail'),'error');}
4423
+ if(d.ok){toast(t('toast.userApproved'),'success');_lastAdminFingerprint='';loadAdminData();}else{toast(d.error||t('toast.approveFail'),'error');}
4400
4424
  }catch(e){toast(t('toast.approveFail')+': '+e.message,'error');}
4401
4425
  }
4402
4426
  async function adminRejectUser(userId){
@@ -4404,7 +4428,7 @@ async function adminRejectUser(userId){
4404
4428
  try{
4405
4429
  var r=await fetch('/api/sharing/reject-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId})});
4406
4430
  var d=await r.json();
4407
- if(d.ok){toast(t('toast.userRejected'),'success');loadAdminData();}else{toast(d.error||t('toast.rejectFail'),'error');}
4431
+ if(d.ok){toast(t('toast.userRejected'),'success');_lastAdminFingerprint='';loadAdminData();}else{toast(d.error||t('toast.rejectFail'),'error');}
4408
4432
  }catch(e){toast(t('toast.rejectFail')+': '+e.message,'error');}
4409
4433
  }
4410
4434
  async function adminToggleRole(userId,newRole){
@@ -4413,7 +4437,14 @@ async function adminToggleRole(userId,newRole){
4413
4437
  try{
4414
4438
  var r=await fetch('/api/sharing/change-role',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,role:newRole})});
4415
4439
  var d=await r.json();
4416
- if(d.ok){toast(t('toast.roleChanged'),'success');loadAdminData();}
4440
+ if(d.ok){
4441
+ toast(t('toast.roleChanged'),'success');
4442
+ _lastAdminFingerprint='';
4443
+ _lastSettingsFingerprint='';
4444
+ _lastSidebarFingerprint='';
4445
+ await loadSharingStatus(false);
4446
+ loadAdminData();
4447
+ }
4417
4448
  else if(d.error==='cannot_demote_owner'){toast(t('admin.ownerHint'),'error');}
4418
4449
  else{toast(d.error||t('toast.roleChangeFail'),'error');}
4419
4450
  }catch(e){toast(t('toast.roleChangeFail')+': '+e.message,'error');}
@@ -4465,7 +4496,7 @@ async function adminRemoveUser(userId,username){
4465
4496
  try{
4466
4497
  var r=await fetch('/api/sharing/remove-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,cleanResources:clean})});
4467
4498
  var d=await r.json();
4468
- if(d.ok){toast(t('toast.userRemoved'),'success');loadAdminData();}
4499
+ if(d.ok){toast(t('toast.userRemoved'),'success');_lastAdminFingerprint='';loadAdminData();}
4469
4500
  else if(d.error==='cannot_remove_owner'){toast(t('admin.ownerHint'),'error');}
4470
4501
  else{toast(d.error||t('toast.removeFail'),'error');}
4471
4502
  }catch(e){toast(t('toast.removeFail')+': '+e.message,'error');}
@@ -6597,16 +6628,16 @@ async function doSaveConfig(cfg, btnEl, savedId){
6597
6628
  function done(){btnEl.disabled=false;btnEl.textContent=t('settings.save');}
6598
6629
  try{
6599
6630
  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;}
6631
+ if(r.status===401){done();toast(t('settings.session.expired'),'error');return null;}
6601
6632
  if(!r.ok) throw new Error(await r.text());
6633
+ var data=await r.json().catch(function(){return {ok:true};});
6602
6634
  flashSaved(savedId);
6603
- toast(t('settings.saved'),'success');
6604
6635
  done();
6605
- return true;
6636
+ return data;
6606
6637
  }catch(e){
6607
6638
  toast(t('settings.save.fail')+': '+e.message,'error');
6608
6639
  done();
6609
- return false;
6640
+ return null;
6610
6641
  }
6611
6642
  }
6612
6643
 
@@ -6701,11 +6732,19 @@ async function saveModelsConfig(){
6701
6732
  await doSaveConfig(cfg, saveBtn, 'modelsSaved');
6702
6733
  }
6703
6734
 
6735
+ function _hubSaveBtnLabel(){
6736
+ var on=document.getElementById('cfgSharingEnabled');
6737
+ if(on&&on.checked&&_sharingRole==='client'){
6738
+ var prevClient=sharingStatusCache&&sharingStatusCache.enabled&&sharingStatusCache.role==='client';
6739
+ return prevClient?t('settings.save'):t('sharing.joinTeam');
6740
+ }
6741
+ return t('settings.save');
6742
+ }
6704
6743
  async function saveHubConfig(){
6705
6744
  var card=document.getElementById('settingsSharingConfig');
6706
6745
  var saveBtn=card.querySelector('.settings-actions .btn-primary');
6707
6746
  saveBtn.disabled=true;saveBtn.textContent=t('settings.test.loading');
6708
- function done(){saveBtn.disabled=false;saveBtn.textContent=t('settings.save');}
6747
+ function done(){saveBtn.disabled=false;saveBtn.textContent=_hubSaveBtnLabel();}
6709
6748
 
6710
6749
  const cfg={};
6711
6750
  var sharingEnabled=document.getElementById('cfgSharingEnabled').checked;
@@ -6772,14 +6811,25 @@ async function saveHubConfig(){
6772
6811
  if(!(await confirmModal(switchMsg,{danger:true}))){done();return;}
6773
6812
  }
6774
6813
 
6775
- var ok=await doSaveConfig(cfg, saveBtn, 'hubSaved');
6776
- if(ok){
6814
+ var result=await doSaveConfig(cfg, saveBtn, 'hubSaved');
6815
+ if(result){
6777
6816
  if(sharingEnabled&&_sharingRole==='hub'){
6778
6817
  var adminNameEl=document.getElementById('cfgHubAdminName');
6779
6818
  if(adminNameEl&&adminNameEl.value.trim()){
6780
6819
  try{await fetch('/api/sharing/update-username',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:adminNameEl.value.trim()})});}catch(e){}
6781
6820
  }
6782
6821
  }
6822
+ if(sharingEnabled&&_sharingRole==='client'&&result.joinStatus){
6823
+ if(result.joinStatus==='pending'){
6824
+ toast(t('sharing.joinSent.pending'),'success');
6825
+ }else if(result.joinStatus==='active'){
6826
+ toast(t('sharing.joinSent.active'),'success');
6827
+ }else{
6828
+ toast(t('settings.saved'),'success');
6829
+ }
6830
+ }else{
6831
+ toast(t('settings.saved'),'success');
6832
+ }
6783
6833
  _lastSidebarFingerprint='';
6784
6834
  _lastSettingsFingerprint='';
6785
6835
  _lastSharingConnStatus='';
@@ -7354,6 +7404,12 @@ function notifTypeText(n){
7354
7404
  if(n.type==='user_offline'){
7355
7405
  return t('notif.userOffline');
7356
7406
  }
7407
+ if(n.type==='membership_approved'){
7408
+ return t('notif.membershipApproved');
7409
+ }
7410
+ if(n.type==='membership_rejected'){
7411
+ return t('notif.membershipRejected');
7412
+ }
7357
7413
  return n.message||n.type;
7358
7414
  }
7359
7415
 
@@ -8810,6 +8866,14 @@ async function checkForUpdate(){
8810
8866
  }
8811
8867
 
8812
8868
  /* ─── Init ─── */
8869
+ try{
8870
+ var savedScope=localStorage.getItem('memos_memorySearchScope');
8871
+ if(savedScope&&(savedScope==='local'||savedScope==='allLocal'||savedScope==='hub')){
8872
+ memorySearchScope=savedScope;
8873
+ var scopeEl=document.getElementById('memorySearchScope');
8874
+ if(scopeEl) scopeEl.value=savedScope;
8875
+ }
8876
+ }catch(e){}
8813
8877
  document.getElementById('modalOverlay').addEventListener('click',e=>{if(e.target.id==='modalOverlay')closeModal()});
8814
8878
  document.getElementById('searchInput').addEventListener('keydown',e=>{if(e.key==='Escape'){e.target.value='';currentPage=1;if(memorySearchScope==='hub')loadHubMemories();else loadMemories();}});
8815
8879
  applyI18n();
@@ -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> {