@memtensor/memos-local-openclaw-plugin 1.0.2 → 1.0.4-beta.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.
Files changed (138) hide show
  1. package/README.md +38 -21
  2. package/dist/client/connector.d.ts +26 -0
  3. package/dist/client/connector.d.ts.map +1 -0
  4. package/dist/client/connector.js +127 -0
  5. package/dist/client/connector.js.map +1 -0
  6. package/dist/client/hub.d.ts +61 -0
  7. package/dist/client/hub.d.ts.map +1 -0
  8. package/dist/client/hub.js +148 -0
  9. package/dist/client/hub.js.map +1 -0
  10. package/dist/client/skill-sync.d.ts +29 -0
  11. package/dist/client/skill-sync.d.ts.map +1 -0
  12. package/dist/client/skill-sync.js +216 -0
  13. package/dist/client/skill-sync.js.map +1 -0
  14. package/dist/config.d.ts +2 -1
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +70 -3
  17. package/dist/config.js.map +1 -1
  18. package/dist/embedding/index.d.ts +4 -2
  19. package/dist/embedding/index.d.ts.map +1 -1
  20. package/dist/embedding/index.js +21 -4
  21. package/dist/embedding/index.js.map +1 -1
  22. package/dist/hub/auth.d.ts +19 -0
  23. package/dist/hub/auth.d.ts.map +1 -0
  24. package/dist/hub/auth.js +70 -0
  25. package/dist/hub/auth.js.map +1 -0
  26. package/dist/hub/server.d.ts +41 -0
  27. package/dist/hub/server.d.ts.map +1 -0
  28. package/dist/hub/server.js +742 -0
  29. package/dist/hub/server.js.map +1 -0
  30. package/dist/hub/user-manager.d.ts +28 -0
  31. package/dist/hub/user-manager.d.ts.map +1 -0
  32. package/dist/hub/user-manager.js +112 -0
  33. package/dist/hub/user-manager.js.map +1 -0
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +4 -3
  37. package/dist/index.js.map +1 -1
  38. package/dist/ingest/providers/index.d.ts +10 -2
  39. package/dist/ingest/providers/index.d.ts.map +1 -1
  40. package/dist/ingest/providers/index.js +242 -8
  41. package/dist/ingest/providers/index.js.map +1 -1
  42. package/dist/ingest/providers/openai.d.ts +1 -0
  43. package/dist/ingest/providers/openai.d.ts.map +1 -1
  44. package/dist/ingest/providers/openai.js +1 -0
  45. package/dist/ingest/providers/openai.js.map +1 -1
  46. package/dist/ingest/task-processor.js +1 -1
  47. package/dist/ingest/task-processor.js.map +1 -1
  48. package/dist/openclaw-api.d.ts +53 -0
  49. package/dist/openclaw-api.d.ts.map +1 -0
  50. package/dist/openclaw-api.js +189 -0
  51. package/dist/openclaw-api.js.map +1 -0
  52. package/dist/recall/engine.js +2 -2
  53. package/dist/recall/engine.js.map +1 -1
  54. package/dist/shared/llm-call.d.ts +4 -1
  55. package/dist/shared/llm-call.d.ts.map +1 -1
  56. package/dist/shared/llm-call.js +15 -0
  57. package/dist/shared/llm-call.js.map +1 -1
  58. package/dist/sharing/types.contract.d.ts +2 -0
  59. package/dist/sharing/types.contract.d.ts.map +1 -0
  60. package/dist/sharing/types.contract.js +3 -0
  61. package/dist/sharing/types.contract.js.map +1 -0
  62. package/dist/sharing/types.d.ts +80 -0
  63. package/dist/sharing/types.d.ts.map +1 -0
  64. package/dist/sharing/types.js +3 -0
  65. package/dist/sharing/types.js.map +1 -0
  66. package/dist/skill/evaluator.d.ts.map +1 -1
  67. package/dist/skill/evaluator.js +2 -2
  68. package/dist/skill/evaluator.js.map +1 -1
  69. package/dist/skill/generator.d.ts.map +1 -1
  70. package/dist/skill/generator.js +4 -4
  71. package/dist/skill/generator.js.map +1 -1
  72. package/dist/skill/upgrader.js +1 -1
  73. package/dist/skill/upgrader.js.map +1 -1
  74. package/dist/skill/validator.js +1 -1
  75. package/dist/skill/validator.js.map +1 -1
  76. package/dist/storage/sqlite.d.ts +294 -0
  77. package/dist/storage/sqlite.d.ts.map +1 -1
  78. package/dist/storage/sqlite.js +902 -8
  79. package/dist/storage/sqlite.js.map +1 -1
  80. package/dist/tools/index.d.ts +1 -0
  81. package/dist/tools/index.d.ts.map +1 -1
  82. package/dist/tools/index.js +3 -1
  83. package/dist/tools/index.js.map +1 -1
  84. package/dist/tools/memory-search.d.ts +3 -2
  85. package/dist/tools/memory-search.d.ts.map +1 -1
  86. package/dist/tools/memory-search.js +48 -7
  87. package/dist/tools/memory-search.js.map +1 -1
  88. package/dist/tools/network-memory-detail.d.ts +4 -0
  89. package/dist/tools/network-memory-detail.d.ts.map +1 -0
  90. package/dist/tools/network-memory-detail.js +34 -0
  91. package/dist/tools/network-memory-detail.js.map +1 -0
  92. package/dist/types.d.ts +47 -2
  93. package/dist/types.d.ts.map +1 -1
  94. package/dist/types.js.map +1 -1
  95. package/dist/update-check.d.ts.map +1 -1
  96. package/dist/update-check.js +0 -1
  97. package/dist/update-check.js.map +1 -1
  98. package/dist/viewer/html.d.ts.map +1 -1
  99. package/dist/viewer/html.js +2396 -289
  100. package/dist/viewer/html.js.map +1 -1
  101. package/dist/viewer/server.d.ts +43 -0
  102. package/dist/viewer/server.d.ts.map +1 -1
  103. package/dist/viewer/server.js +1180 -33
  104. package/dist/viewer/server.js.map +1 -1
  105. package/index.ts +445 -25
  106. package/openclaw.plugin.json +2 -1
  107. package/package.json +2 -1
  108. package/scripts/postinstall.cjs +282 -45
  109. package/skill/memos-memory-guide/SKILL.md +26 -2
  110. package/src/client/connector.ts +124 -0
  111. package/src/client/hub.ts +189 -0
  112. package/src/client/skill-sync.ts +202 -0
  113. package/src/config.ts +92 -3
  114. package/src/embedding/index.ts +25 -3
  115. package/src/hub/auth.ts +78 -0
  116. package/src/hub/server.ts +734 -0
  117. package/src/hub/user-manager.ts +126 -0
  118. package/src/index.ts +7 -4
  119. package/src/ingest/providers/index.ts +279 -8
  120. package/src/ingest/providers/openai.ts +1 -1
  121. package/src/ingest/task-processor.ts +1 -1
  122. package/src/openclaw-api.ts +287 -0
  123. package/src/recall/engine.ts +2 -2
  124. package/src/shared/llm-call.ts +19 -1
  125. package/src/sharing/types.contract.ts +40 -0
  126. package/src/sharing/types.ts +102 -0
  127. package/src/skill/evaluator.ts +3 -2
  128. package/src/skill/generator.ts +6 -4
  129. package/src/skill/upgrader.ts +1 -1
  130. package/src/skill/validator.ts +1 -1
  131. package/src/storage/sqlite.ts +1167 -7
  132. package/src/tools/index.ts +1 -0
  133. package/src/tools/memory-search.ts +57 -8
  134. package/src/tools/network-memory-detail.ts +34 -0
  135. package/src/types.ts +48 -2
  136. package/src/update-check.ts +0 -1
  137. package/src/viewer/html.ts +2396 -289
  138. package/src/viewer/server.ts +1087 -34
@@ -148,6 +148,101 @@ input,textarea,select{font-family:inherit;font-size:inherit}
148
148
  .search-bar input:focus{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-glow)}
149
149
  .search-bar .search-icon{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:var(--text-muted);font-size:14px;pointer-events:none}
150
150
  .search-meta{font-size:12px;color:var(--text-sec);margin-bottom:14px;padding:0 2px}
151
+ .scope-select{padding:10px 12px;border:1px solid var(--border);border-radius:10px;background:var(--bg-card);color:var(--text);font-size:13px;min-width:110px;outline:none}
152
+ .sharing-inline-meta{font-size:12px;color:var(--text-muted);margin:-8px 0 14px 2px}
153
+ .sharing-sidebar-card{margin:14px 0 18px;border:1px solid var(--border);background:var(--bg-card);border-radius:12px;padding:12px;box-shadow:var(--shadow-sm)}
154
+ .sharing-sidebar-card .title{font-size:12px;font-weight:700;color:var(--text);margin-bottom:8px;text-transform:uppercase;letter-spacing:.04em}
155
+ .sharing-sidebar-card .status{font-size:13px;color:var(--text-sec);line-height:1.5}
156
+ .sharing-sidebar-card .status strong{color:var(--text)}
157
+ .sharing-sidebar-card .hint{margin-top:8px;font-size:11px;color:var(--text-muted)}
158
+ .sharing-sidebar-card .user-row{display:flex;align-items:center;gap:8px;margin-bottom:10px}
159
+ .sharing-sidebar-card .user-row .username{font-size:13px;font-weight:600;color:var(--text)}
160
+ .sharing-sidebar-card .role-badge{display:inline-block;font-size:10px;font-weight:600;padding:2px 8px;border-radius:9999px;line-height:1.4;letter-spacing:.02em}
161
+ .sharing-sidebar-card .role-badge.admin{background:rgba(52,199,89,.15);color:#34c759}
162
+ .sharing-sidebar-card .role-badge.client{background:rgba(175,82,222,.15);color:#af52de}
163
+ .sharing-sidebar-card .info-grid{display:grid;grid-template-columns:auto 1fr;gap:4px 10px;font-size:12px;margin-bottom:8px}
164
+ .sharing-sidebar-card .info-grid .label{color:var(--text-muted);font-weight:500;white-space:nowrap}
165
+ .sharing-sidebar-card .info-grid .value{color:var(--text-sec);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
166
+ .sharing-sidebar-card .api-badge{display:inline-block;font-size:10px;font-weight:600;padding:2px 8px;border-radius:9999px;background:rgba(142,142,147,.12);color:var(--text-muted);letter-spacing:.02em}
167
+ [data-theme="light"] .sharing-sidebar-card .role-badge.admin{background:rgba(5,150,105,.1);color:#059669}
168
+ [data-theme="light"] .sharing-sidebar-card .role-badge.client{background:rgba(124,58,237,.1);color:#7c3aed}
169
+ .result-section{margin-bottom:18px;border:1px solid var(--border);border-radius:14px;background:var(--bg-card);overflow:hidden}
170
+ .result-section-header{display:flex;justify-content:space-between;align-items:center;padding:12px 14px;border-bottom:1px solid var(--border);background:rgba(255,255,255,.02)}
171
+ .result-section-title{font-size:14px;font-weight:700;color:var(--text)}
172
+ .result-section-sub{font-size:12px;color:var(--text-muted)}
173
+ .search-hit-list{padding:12px;display:flex;flex-direction:column;gap:10px}
174
+ .search-hit-card,.hub-hit-card,.hub-skill-card{border:1px solid var(--border);border-radius:12px;background:var(--bg);padding:12px;box-shadow:var(--shadow-sm)}
175
+ .search-hit-card .summary,.hub-hit-card .summary,.hub-skill-card .summary{font-size:14px;font-weight:600;color:var(--text);margin-bottom:6px}
176
+ .search-hit-card .excerpt,.hub-hit-card .excerpt,.hub-skill-card .excerpt{font-size:12px;color:var(--text-sec);line-height:1.55;white-space:pre-wrap}
177
+ .search-hit-meta,.hub-hit-meta,.hub-skill-meta{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px;font-size:11px;color:var(--text-muted)}
178
+ .meta-chip{display:inline-flex;align-items:center;gap:5px;padding:4px 8px;border:1px solid var(--border);border-radius:999px;background:var(--bg-card)}
179
+ .hub-hit-actions,.hub-skill-actions,.task-share-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
180
+ .sharing-settings-grid{display:grid;grid-template-columns:1.1fr .9fr;gap:18px}
181
+ .sharing-panel{border:1px solid var(--border);border-radius:14px;background:var(--bg-card);padding:14px;box-shadow:var(--shadow-sm)}
182
+ .sharing-panel h4{font-size:14px;font-weight:700;color:var(--text);margin:0 0 10px 0}
183
+ .sharing-panel .line{font-size:13px;color:var(--text-sec);margin-bottom:8px;line-height:1.55}
184
+ .sharing-panel .line strong{color:var(--text)}
185
+ .pending-user-list{display:flex;flex-direction:column;gap:10px}
186
+ .pending-user-card{border:1px solid var(--border);border-radius:12px;padding:12px;background:var(--bg)}
187
+ .pending-user-name{font-size:14px;font-weight:700;color:var(--text)}
188
+ .pending-user-meta{font-size:12px;color:var(--text-sec);margin-top:4px}
189
+ .pending-user-actions{display:flex;gap:8px;margin-top:10px}
190
+ /* ─── Admin Panel ─── */
191
+ .admin-header{position:relative;padding:28px 28px 20px;background:linear-gradient(135deg,rgba(99,102,241,.08) 0%,rgba(139,92,246,.06) 50%,rgba(6,182,212,.05) 100%);border:1px solid rgba(99,102,241,.12);border-radius:16px;margin-bottom:20px;overflow:hidden}
192
+ .admin-header::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--violet),var(--pri),var(--cyan));border-radius:16px 16px 0 0}
193
+ .admin-header-top{display:flex;justify-content:space-between;align-items:center}
194
+ .admin-header h2{font-size:20px;font-weight:800;color:var(--text);display:flex;align-items:center;gap:10px;margin:0}
195
+ .admin-header h2 .ah-icon{width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,var(--pri),var(--violet));display:flex;align-items:center;justify-content:center;font-size:18px;box-shadow:0 4px 12px rgba(99,102,241,.25)}
196
+ .admin-header-sub{font-size:12px;color:var(--text-muted);margin-top:6px}
197
+ .admin-stat-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px;margin-top:18px}
198
+ .admin-stat-box{position:relative;text-align:center;padding:16px 8px 14px;background:var(--bg-card);border:1px solid var(--border);border-radius:12px;overflow:hidden;transition:all .2s}
199
+ .admin-stat-box:hover{border-color:rgba(99,102,241,.25);transform:translateY(-1px);box-shadow:0 4px 12px rgba(0,0,0,.06)}
200
+ .admin-stat-box::before{content:'';position:absolute;top:0;left:50%;transform:translateX(-50%);width:40px;height:2px;border-radius:0 0 4px 4px}
201
+ .admin-stat-box:nth-child(1)::before{background:var(--green)}
202
+ .admin-stat-box:nth-child(2)::before{background:var(--amber)}
203
+ .admin-stat-box:nth-child(3)::before{background:var(--violet)}
204
+ .admin-stat-box:nth-child(4)::before{background:var(--cyan)}
205
+ .admin-stat-box:nth-child(5)::before{background:var(--pri)}
206
+ .admin-stat-box:nth-child(6)::before{background:var(--rose)}
207
+ .admin-stat-box .as-icon{font-size:16px;margin-bottom:4px;display:block}
208
+ .admin-stat-box .val{font-size:22px;font-weight:800;color:var(--text);font-variant-numeric:tabular-nums}
209
+ .admin-stat-box .lbl{font-size:10px;color:var(--text-muted);margin-top:3px;letter-spacing:.03em}
210
+ .admin-tabs{display:flex;gap:4px;padding:4px;background:var(--bg);border:1px solid var(--border);border-radius:14px;overflow-x:auto;-webkit-overflow-scrolling:touch;margin-bottom:20px}
211
+ .admin-tab{position:relative;display:flex;align-items:center;gap:6px;padding:9px 16px;border:none;background:transparent;color:var(--text-muted);font-size:13px;font-weight:500;cursor:pointer;border-radius:10px;transition:all .2s;white-space:nowrap;font-family:inherit}
212
+ .admin-tab:hover{background:rgba(99,102,241,.06);color:var(--text)}
213
+ .admin-tab.active{background:var(--bg-card);color:var(--text);font-weight:600;box-shadow:0 2px 8px rgba(0,0,0,.06),0 0 0 1px rgba(99,102,241,.1)}
214
+ .admin-tab .at-icon{font-size:14px;line-height:1}
215
+ .admin-tab .at-count{font-size:10px;font-weight:700;padding:1px 6px;border-radius:8px;background:rgba(99,102,241,.1);color:var(--pri);min-width:18px;text-align:center}
216
+ .admin-tab.active .at-count{background:rgba(99,102,241,.15)}
217
+ .admin-panel{display:none}
218
+ .admin-panel.active{display:block}
219
+ .admin-card{border:1px solid var(--border);border-radius:12px;padding:14px 16px;background:var(--bg-card);margin-bottom:10px;transition:all .2s;position:relative;overflow:hidden}
220
+ .admin-card::before{content:'';position:absolute;top:0;left:0;bottom:0;width:3px;border-radius:3px 0 0 3px;background:var(--pri);opacity:.5}
221
+ .admin-card:hover{border-color:rgba(99,102,241,.25);box-shadow:0 2px 12px rgba(0,0,0,.04);transform:translateY(-1px)}
222
+ .admin-card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}
223
+ .admin-card-title{font-size:14px;font-weight:700;color:var(--text)}
224
+ .admin-card-meta{font-size:12px;color:var(--text-muted);line-height:1.5}
225
+ .admin-card-actions{display:flex;gap:8px;margin-top:10px}
226
+ .admin-badge{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:700;padding:3px 10px;border-radius:999px;letter-spacing:.02em}
227
+ .admin-badge.admin{background:rgba(52,199,89,.15);color:#34c759}
228
+ .admin-badge.member{background:rgba(142,142,147,.12);color:var(--text-muted)}
229
+ .admin-badge.pending{background:rgba(255,159,10,.15);color:#ff9f0a}
230
+ .admin-badge.public{background:rgba(99,102,241,.1);color:var(--pri)}
231
+ .admin-badge.group{background:rgba(139,92,246,.1);color:var(--violet)}
232
+ .admin-empty{font-size:13px;color:var(--text-muted);padding:40px 20px;text-align:center;border:1px dashed var(--border);border-radius:12px;background:var(--bg)}
233
+ .admin-empty .ae-icon{font-size:28px;display:block;margin-bottom:8px;opacity:.5}
234
+ [data-theme="light"] .admin-badge.admin{background:rgba(5,150,105,.1);color:#059669}
235
+ [data-theme="light"] .admin-badge.member{background:rgba(0,0,0,.06);color:#6b7280}
236
+ [data-theme="light"] .admin-badge.pending{background:rgba(245,158,11,.1);color:#d97706}
237
+ [data-theme="light"] .admin-header{background:linear-gradient(135deg,rgba(99,102,241,.05) 0%,rgba(139,92,246,.04) 50%,rgba(6,182,212,.03) 100%)}
238
+ .task-detail-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
239
+ .shared-memory-overlay,.shared-memory-overlay.show{display:none}
240
+ .shared-memory-overlay.show{display:flex;position:fixed;inset:0;align-items:center;justify-content:center;background:rgba(0,0,0,.55);z-index:1200;padding:24px}
241
+ .shared-memory-panel{width:min(860px,95vw);max-height:85vh;overflow:auto;border:1px solid var(--border);border-radius:18px;background:var(--bg-card);box-shadow:var(--shadow-lg);padding:20px}
242
+ .shared-memory-panel h3{font-size:18px;color:var(--text);margin-bottom:10px}
243
+ .shared-memory-panel .content{font-size:13px;color:var(--text-sec);line-height:1.7;white-space:pre-wrap;background:var(--bg);border:1px solid var(--border);border-radius:12px;padding:14px;margin-top:12px}
244
+ .hub-source-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;background:rgba(34,197,94,.12);color:var(--green);font-size:11px;font-weight:700;border:1px solid rgba(34,197,94,.22)}
245
+ @media (max-width: 960px){.sharing-settings-grid{grid-template-columns:1fr}.search-bar{flex-wrap:wrap}.scope-select{width:100%}.task-detail-actions{width:100%;justify-content:flex-start}}
151
246
 
152
247
  .filter-bar{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}
153
248
  .filter-chip{padding:5px 14px;border:1px solid var(--border);border-radius:6px;background:transparent;color:var(--text-sec);font-size:12px;font-weight:500;transition:all .15s}
@@ -245,6 +340,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
245
340
  .form-group textarea{min-height:100px;resize:vertical}
246
341
  .modal-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:28px}
247
342
 
343
+
248
344
  /* ─── Toast ─── */
249
345
  .emb-banner{display:flex;align-items:center;gap:10px;padding:12px 20px;font-size:13px;font-weight:500;border-radius:10px;margin:0 32px 0;animation:slideIn .3s ease}
250
346
  .emb-banner.warning{background:rgba(245,158,11,.1);color:#d97706;border:1px solid rgba(245,158,11,.25)}
@@ -438,9 +534,9 @@ input,textarea,select{font-family:inherit;font-size:inherit}
438
534
  .nav-tabs .tab.active{color:var(--text);background:rgba(255,255,255,.1);border-color:var(--border);box-shadow:0 1px 4px rgba(0,0,0,.15)}
439
535
  [data-theme="light"] .nav-tabs{background:rgba(0,0,0,.05)}
440
536
  [data-theme="light"] .nav-tabs .tab.active{background:#fff;border-color:rgba(0,0,0,.1);box-shadow:0 1px 3px rgba(0,0,0,.08);color:var(--text)}
441
- .analytics-view,.settings-view,.logs-view,.migrate-view{display:none;flex:1;min-width:0;flex-direction:column;gap:20px}
442
- .analytics-view.show,.settings-view.show,.logs-view.show,.migrate-view.show{display:flex}
443
- .feed-wrap,.tasks-view,.skills-view,.analytics-view,.settings-view,.logs-view,.migrate-view{max-width:960px}
537
+ .analytics-view,.settings-view,.logs-view,.migrate-view,.admin-view{display:none;flex:1;min-width:0;flex-direction:column;gap:20px}
538
+ .analytics-view.show,.settings-view.show,.logs-view.show,.migrate-view.show,.admin-view.show{display:flex}
539
+ .feed-wrap,.tasks-view,.skills-view,.analytics-view,.settings-view,.logs-view,.migrate-view,.admin-view{max-width:960px}
444
540
 
445
541
  /* ─── Logs ─── */
446
542
  .logs-toolbar{display:flex;align-items:center;justify-content:space-between;padding:8px 0}
@@ -558,6 +654,70 @@ input,textarea,select{font-family:inherit;font-size:inherit}
558
654
  .settings-section{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:24px 28px}
559
655
  .settings-section h3{font-size:13px;font-weight:700;color:var(--text);margin-bottom:16px;display:flex;align-items:center;gap:8px}
560
656
  .settings-section h3 .icon{font-size:16px;opacity:.8}
657
+ .settings-tabs-bar{display:flex;gap:4px;margin-bottom:24px;padding:4px;background:var(--bg);border:1px solid var(--border);border-radius:14px;overflow-x:auto;-webkit-overflow-scrolling:touch}
658
+ .settings-tab-btn{position:relative;display:flex;align-items:center;gap:7px;padding:10px 16px;border:none;background:transparent;color:var(--text-muted);font-size:13px;font-weight:500;cursor:pointer;border-radius:10px;transition:all .25s ease;white-space:nowrap;font-family:inherit}
659
+ .settings-tab-btn:hover{background:rgba(99,102,241,.06);color:var(--text)}
660
+ .settings-tab-btn.active{background:var(--bg-card);color:var(--text);font-weight:600;box-shadow:0 2px 8px rgba(0,0,0,.06),0 0 0 1px rgba(99,102,241,.1)}
661
+ .settings-tab-btn .stab-icon{font-size:15px;line-height:1}
662
+ .settings-tab-btn .stab-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
663
+ .settings-tab-btn[data-tab="embedding"] .stab-dot{background:#6366f1}
664
+ .settings-tab-btn[data-tab="summarizer"] .stab-dot{background:#8b5cf6}
665
+ .settings-tab-btn[data-tab="skill"] .stab-dot{background:#f59e0b}
666
+ .settings-tab-btn[data-tab="hub"] .stab-dot{background:#06b6d4}
667
+ .settings-tab-btn[data-tab="general"] .stab-dot{background:#10b981}
668
+ .settings-tab-btn.active .stab-dot{box-shadow:0 0 6px currentColor}
669
+ [data-theme="light"] .settings-tab-btn.active{box-shadow:0 2px 8px rgba(0,0,0,.05),0 0 0 1px rgba(99,102,241,.08)}
670
+ .settings-cards-grid{display:flex;flex-direction:column;gap:24px}
671
+ .settings-card[data-stab]{display:none}
672
+ .settings-card[data-stab].stab-active{display:block}
673
+ .settings-card{position:relative;background:var(--bg-card);border:1px solid var(--border);border-radius:16px;overflow:hidden;transition:border-color .3s,box-shadow .3s}
674
+ .settings-card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;opacity:.5;transition:opacity .3s}
675
+ .settings-card:hover{border-color:rgba(99,102,241,.25);box-shadow:0 0 24px rgba(99,102,241,.06),0 8px 32px rgba(0,0,0,.1)}
676
+ .settings-card:hover::before{opacity:1}
677
+ .settings-card.card-embedding::before{background:linear-gradient(90deg,#6366f1,#818cf8,#a5b4fc)}
678
+ .settings-card.card-summarizer::before{background:linear-gradient(90deg,#8b5cf6,#a78bfa,#c4b5fd)}
679
+ .settings-card.card-skill::before{background:linear-gradient(90deg,#f59e0b,#fbbf24,#fcd34d)}
680
+ .settings-card.card-hub::before{background:linear-gradient(90deg,#06b6d4,#22d3ee,#67e8f9)}
681
+ .settings-card.card-general::before{background:linear-gradient(90deg,#10b981,#34d399,#6ee7b7)}
682
+ .settings-card-header{display:flex;align-items:center;gap:14px;padding:22px 28px 0}
683
+ .settings-card-icon{width:44px;height:44px;display:flex;align-items:center;justify-content:center;border-radius:12px;font-size:22px;flex-shrink:0;border:1px solid transparent}
684
+ .settings-card-title-wrap{flex:1;min-width:0}
685
+ .settings-card-title{font-size:16px;font-weight:700;color:var(--text);letter-spacing:.01em}
686
+ .settings-card-desc{font-size:11px;color:var(--text-muted);margin-top:3px;font-weight:400;line-height:1.4}
687
+ .settings-card-body{padding:18px 28px 24px}
688
+ .settings-card-divider{height:1px;background:var(--border);margin:18px 0;opacity:.6}
689
+ .settings-card-subtitle{font-size:12px;font-weight:700;color:var(--text-sec);margin-bottom:10px;letter-spacing:.01em}
690
+ .hub-info-card{border:1px solid var(--border);border-radius:12px;padding:14px 16px;background:var(--bg);position:relative;overflow:hidden;margin-bottom:12px}
691
+ .hub-info-card::before{content:'';position:absolute;top:0;left:0;bottom:0;width:3px;border-radius:3px 0 0 3px}
692
+ .hub-info-card.hic-share{border-color:rgba(99,102,241,.2);background:linear-gradient(135deg,rgba(99,102,241,.04),rgba(139,92,246,.03))}
693
+ .hub-info-card.hic-share::before{background:linear-gradient(180deg,var(--pri),var(--violet))}
694
+ .hub-info-card.hic-status::before{background:var(--green)}
695
+ .hub-info-card.hic-team::before{background:var(--violet)}
696
+ .hub-info-card.hic-pending::before{background:var(--amber)}
697
+ .hub-info-card .hic-title{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:700;color:var(--text);margin-bottom:10px}
698
+ .hub-info-card .hic-title .hic-icon{font-size:14px}
699
+ .hub-info-card .hic-grid{display:grid;grid-template-columns:auto 1fr;gap:4px 12px;font-size:12px;align-items:baseline}
700
+ .hub-info-card .hic-grid .hic-label{color:var(--text-muted);white-space:nowrap}
701
+ .hub-info-card .hic-grid .hic-value{color:var(--text);font-weight:500;word-break:break-all}
702
+ .hub-info-card .hic-grid .hic-value.mono{font-family:monospace;font-size:11px;cursor:pointer;user-select:all}
703
+ .hub-info-card .hic-grid .hic-value.mono:hover{color:var(--pri)}
704
+ .hub-info-card .hic-badge{display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;padding:2px 8px;border-radius:9999px}
705
+ .hub-info-card .hic-badge.connected{background:rgba(52,211,153,.12);color:#34d399}
706
+ .hub-info-card .hic-badge.disconnected{background:rgba(239,68,68,.1);color:#ef4444}
707
+ .hub-info-card .hic-badge.admin{background:rgba(52,199,89,.12);color:#34c759}
708
+ .hub-info-card .hic-badge.pending{background:rgba(251,191,36,.12);color:#fbbf24}
709
+ .hub-info-card .hic-dot{width:6px;height:6px;border-radius:50%;display:inline-block}
710
+ .hub-info-card .hic-dot.green{background:#34d399;box-shadow:0 0 6px #34d399}
711
+ .hub-info-card .hic-dot.red{background:#ef4444}
712
+ .hub-info-card .hic-dot.amber{background:#fbbf24;box-shadow:0 0 4px #fbbf24}
713
+ .hub-info-card .hic-empty{font-size:12px;color:var(--text-muted);text-align:center;padding:12px 0}
714
+ .hub-info-card .hic-actions{display:flex;gap:8px;margin-top:10px}
715
+ [data-theme="light"] .hub-info-card.hic-share{background:linear-gradient(135deg,rgba(99,102,241,.03),rgba(139,92,246,.02))}
716
+ .settings-card .settings-section{background:none;border:none;padding:0;border-radius:0;margin-bottom:16px}
717
+ .settings-card .settings-section:last-child{margin-bottom:0}
718
+ .settings-card .settings-section h3{margin-bottom:12px}
719
+ [data-theme="light"] .settings-card{box-shadow:0 1px 3px rgba(0,0,0,.04)}
720
+ [data-theme="light"] .settings-card:hover{box-shadow:0 0 24px rgba(79,70,229,.05),0 8px 32px rgba(0,0,0,.06)}
561
721
  .settings-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
562
722
  @media(max-width:800px){.settings-grid{grid-template-columns:1fr}}
563
723
  .settings-field{display:flex;flex-direction:column;gap:4px}
@@ -565,7 +725,8 @@ input,textarea,select{font-family:inherit;font-size:inherit}
565
725
  .settings-field input,.settings-field select{background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:8px 12px;color:var(--text);font-size:13px;font-family:inherit;transition:border-color .15s}
566
726
  .settings-field input:focus,.settings-field select:focus{outline:none;border-color:var(--pri)}
567
727
  .settings-field input[type="password"]{font-family:'Courier New',monospace;letter-spacing:.05em}
568
- .settings-field .field-hint{font-size:10px;color:var(--text-muted);margin-top:2px}
728
+ .field-hint{font-size:10px;color:var(--text-muted);line-height:1.5}
729
+ .settings-field .field-hint{margin-top:2px}
569
730
  .settings-field.full-width{grid-column:1/-1}
570
731
  .settings-toggle{display:flex;align-items:center;gap:10px;padding:4px 0}
571
732
  .settings-toggle label{font-size:12px;font-weight:500;color:var(--text-sec);text-transform:none;letter-spacing:0}
@@ -589,7 +750,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
589
750
  [data-theme="light"] .settings-actions .btn-primary:hover{background:rgba(79,70,229,.1);border-color:#4f46e5}
590
751
  .settings-saved{display:inline-flex;align-items:center;gap:6px;color:var(--green);font-size:12px;font-weight:600;opacity:0;transition:opacity .3s}
591
752
  .settings-saved.show{opacity:1}
592
- .model-health-bar{margin-bottom:20px;border-radius:var(--radius-lg);overflow:hidden}
753
+ .model-health-bar{margin-bottom:20px;border-radius:var(--radius-lg);overflow:visible}
593
754
  .mh-table{width:100%;border-collapse:separate;border-spacing:0;font-size:12px}
594
755
  .mh-table th{text-align:left;padding:6px 12px;font-size:10px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em;background:var(--bg);border-bottom:1px solid var(--border)}
595
756
  .mh-table td{padding:8px 12px;border-bottom:1px solid var(--border);vertical-align:middle}
@@ -607,7 +768,8 @@ input,textarea,select{font-family:inherit;font-size:inherit}
607
768
  .mh-badge.error{background:rgba(239,68,68,.1);color:#dc2626}
608
769
  .mh-badge.unknown{background:rgba(148,163,184,.1);color:#64748b}
609
770
  .mh-model-name{color:var(--text-muted);font-size:11px;font-family:var(--font-mono,'SFMono-Regular',Consolas,monospace)}
610
- .mh-err-text{font-size:11px;color:var(--rose);max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:help}
771
+ .mh-err-text{font-size:11px;color:var(--rose);max-width:320px;display:inline-block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:help}
772
+ #mhTooltip{display:none;position:fixed;min-width:280px;max-width:480px;max-height:300px;overflow-y:auto;padding:8px 10px;background:var(--bg-card,#1e1e2e);color:var(--text,#e2e8f0);border:1px solid var(--border,#333);border-radius:6px;font-size:11px;line-height:1.5;white-space:pre-wrap;word-break:break-all;box-shadow:0 4px 12px rgba(0,0,0,.25);z-index:10000;pointer-events:none}
611
773
  .mh-time{font-size:10px;color:var(--text-muted);white-space:nowrap}
612
774
  .mh-empty{padding:16px;font-size:12px;color:var(--text-muted);text-align:center}
613
775
  @keyframes healthPulse{0%,100%{opacity:1}50%{opacity:.4}}
@@ -805,6 +967,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
805
967
  <button class="tab" data-view="analytics" onclick="switchView('analytics')" data-i18n="tab.analytics">\u{1F4CA} Analytics</button>
806
968
  <button class="tab" data-view="logs" onclick="switchView('logs')" data-i18n="tab.logs">\u{1F4DD} Logs</button>
807
969
  <button class="tab" data-view="import" onclick="switchView('import')" data-i18n="tab.import">\u{1F4E5} Import</button>
970
+ <button class="tab" data-view="admin" onclick="switchView('admin')" style="display:none" data-i18n="tab.admin">\u{1F6E1} Admin</button>
808
971
  <button class="tab" data-view="settings" onclick="switchView('settings')" data-i18n="tab.settings">\u2699 Settings</button>
809
972
  </nav>
810
973
  </div>
@@ -824,12 +987,16 @@ input,textarea,select{font-family:inherit;font-size:inherit}
824
987
  <div class="stat-card amber"><div class="stat-value" id="statEmbeddings">-</div><div class="stat-label" data-i18n="stat.embeddings">Embeddings</div></div>
825
988
  <div class="stat-card rose"><div class="stat-value" id="statTimeSpan">-</div><div class="stat-label" data-i18n="stat.days">Days</div></div>
826
989
  </div>
827
- <div id="sidebarSessionSection">
828
- <div id="embeddingStatus"></div>
829
- <div class="section-title" data-i18n="sidebar.sessions">Sessions</div>
830
- <div class="session-list" id="sessionList"></div>
831
- <button class="btn btn-sm btn-ghost" style="width:100%;margin-top:20px;justify-content:center;color:var(--text-muted);font-size:11px" onclick="clearAll()" data-i18n="sidebar.clear">\u{1F5D1} Clear All Data</button>
990
+ <div id="sidebarSharingSection" style="display:none">
991
+ <div class="sharing-sidebar-card">
992
+ <div class="title" style="display:flex;align-items:center;gap:8px"><span data-i18n="sidebar.hub">\u{1F310} Team Sharing</span><span id="sharingSidebarConnBadge"></span></div>
993
+ <div class="status" id="sharingSidebarStatus"></div>
994
+ <div class="hint" id="sharingSidebarHint"></div>
995
+ </div>
832
996
  </div>
997
+ <div id="embeddingStatus"></div>
998
+ <div class="session-list" id="sessionList" style="display:none"></div>
999
+ <button class="btn btn-sm btn-ghost" style="width:100%;margin-top:20px;justify-content:center;color:var(--text-muted);font-size:11px" onclick="clearAll()" data-i18n="sidebar.clear">\u{1F5D1} Clear All Data</button>
833
1000
  </div>
834
1001
 
835
1002
  <div class="feed-wrap" id="feedWrap">
@@ -837,8 +1004,18 @@ input,textarea,select{font-family:inherit;font-size:inherit}
837
1004
  <div class="search-bar">
838
1005
  <span class="search-icon">\u{1F50D}</span>
839
1006
  <input type="text" id="searchInput" data-i18n-ph="search.placeholder" placeholder="Search memories (supports semantic search)..." oninput="debounceSearch()">
1007
+ <select id="filterOwner" class="filter-select" onchange="onOwnerFilterChange()">
1008
+ <option value="" data-i18n="filter.allowners">All owners</option>
1009
+ <option value="public" data-i18n="filter.public">Public</option>
1010
+ </select>
1011
+ <select id="memorySearchScope" class="filter-select" onchange="onMemoryScopeChange()">
1012
+ <option value="local">Local</option>
1013
+ <option value="group">Group</option>
1014
+ <option value="all">All</option>
1015
+ </select>
840
1016
  </div>
841
1017
  <div class="search-meta" id="searchMeta"></div>
1018
+ <div class="search-meta" id="sharingSearchMeta"></div>
842
1019
  <div class="filter-bar" id="filterBar">
843
1020
  <button class="filter-chip active" data-role="" onclick="setRoleFilter(this,'')" data-i18n="filter.all">All</button>
844
1021
  <button class="filter-chip" data-role="user" onclick="setRoleFilter(this,'user')">User</button>
@@ -850,9 +1027,8 @@ input,textarea,select{font-family:inherit;font-size:inherit}
850
1027
  <option value="oldest" data-i18n="filter.oldest">Oldest first</option>
851
1028
  </select>
852
1029
  <span class="filter-sep"></span>
853
- <select id="filterOwner" class="filter-select" onchange="applyFilters()">
854
- <option value="" data-i18n="filter.allowners">All owners</option>
855
- <option value="public" data-i18n="filter.public">Public</option>
1030
+ <select id="filterSession" class="filter-select" onchange="filterSession(this.value||null)">
1031
+ <option value="" data-i18n="filter.allsessions">All sessions</option>
856
1032
  </select>
857
1033
  </div>
858
1034
  <div class="date-filter">
@@ -877,6 +1053,11 @@ input,textarea,select{font-family:inherit;font-size:inherit}
877
1053
  <button class="filter-chip" data-task-status="active" onclick="setTaskStatusFilter(this,'active')" data-i18n="tasks.status.active">Active</button>
878
1054
  <button class="filter-chip" data-task-status="completed" onclick="setTaskStatusFilter(this,'completed')" data-i18n="tasks.status.completed">Completed</button>
879
1055
  <button class="filter-chip" data-task-status="skipped" onclick="setTaskStatusFilter(this,'skipped')" data-i18n="tasks.status.skipped">Skipped</button>
1056
+ <select id="taskSearchScope" class="scope-select" onchange="onTaskScopeChange()">
1057
+ <option value="local">Local</option>
1058
+ <option value="group">Group</option>
1059
+ <option value="all">All</option>
1060
+ </select>
880
1061
  <button class="btn btn-sm btn-ghost" onclick="loadTasks()" style="margin-left:auto" data-i18n="refresh">\u21BB Refresh</button>
881
1062
  </div>
882
1063
  </div>
@@ -886,7 +1067,10 @@ input,textarea,select{font-family:inherit;font-size:inherit}
886
1067
  <div class="task-detail-panel" onclick="event.stopPropagation()">
887
1068
  <div class="task-detail-header">
888
1069
  <h2 id="taskDetailTitle"></h2>
889
- <button class="btn btn-icon" onclick="closeTaskDetail()" title="Close">\u2715</button>
1070
+ <div style="display:flex;gap:8px;align-items:center">
1071
+ <div id="taskShareActions" style="display:flex;gap:8px;align-items:center"></div>
1072
+ <button class="btn btn-icon" onclick="closeTaskDetail()" title="Close">\u2715</button>
1073
+ </div>
890
1074
  </div>
891
1075
  <div class="task-detail-meta" id="taskDetailMeta"></div>
892
1076
  <div class="task-skill-section" id="taskSkillSection"></div>
@@ -897,7 +1081,28 @@ input,textarea,select{font-family:inherit;font-size:inherit}
897
1081
  </div>
898
1082
  </div>
899
1083
  </div>
1084
+ <div class="shared-memory-overlay" id="sharedMemoryOverlay" onclick="closeSharedMemoryDetail(event)">
1085
+ <div class="shared-memory-panel" onclick="event.stopPropagation()">
1086
+ <div class="task-detail-header">
1087
+ <h3 id="sharedMemoryTitle">Shared Memory</h3>
1088
+ <button class="btn btn-icon" onclick="closeSharedMemoryDetail()" title="Close">✕</button>
1089
+ </div>
1090
+ <div class="task-detail-meta" id="sharedMemoryMeta"></div>
1091
+ <div class="task-detail-summary" id="sharedMemorySummary"></div>
1092
+ <div class="content" id="sharedMemoryContent"></div>
1093
+ </div>
1094
+ </div>
900
1095
  <div class="skills-view" id="skillsView">
1096
+ <div class="search-bar">
1097
+ <span class="search-icon">🔍</span>
1098
+ <input type="text" id="skillSearchInput" placeholder="Search skills..." data-i18n-ph="skills.search.placeholder" oninput="debounceSkillSearch()">
1099
+ <select id="skillSearchScope" class="scope-select" onchange="onSkillScopeChange()">
1100
+ <option value="local" data-i18n="scope.local">Local</option>
1101
+ <option value="group" data-i18n="scope.group">Group</option>
1102
+ <option value="all" data-i18n="scope.all">All</option>
1103
+ </select>
1104
+ </div>
1105
+ <div class="search-meta" id="skillSearchMeta"></div>
901
1106
  <div class="tasks-header">
902
1107
  <div class="tasks-stats">
903
1108
  <div class="tasks-stat"><span class="tasks-stat-value" id="skillsTotalCount">-</span><span class="tasks-stat-label" data-i18n="skills.total">Total Skills</span></div>
@@ -921,6 +1126,10 @@ input,textarea,select{font-family:inherit;font-size:inherit}
921
1126
  </div>
922
1127
  </div>
923
1128
  <div class="tasks-list" id="skillsList"><div class="spinner"></div></div>
1129
+ <div id="hubSkillsSection" style="display:none;margin-top:16px">
1130
+ <div class="section-title" style="margin-bottom:12px" data-i18n="skills.hub.title">\u{1F310} Hub Skills</div>
1131
+ <div class="tasks-list" id="hubSkillsList"></div>
1132
+ </div>
924
1133
  </div>
925
1134
  <div class="task-detail-overlay" id="skillDetailOverlay" onclick="closeSkillDetail(event)">
926
1135
  <div class="task-detail-panel" onclick="event.stopPropagation()">
@@ -934,6 +1143,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
934
1143
  </div>
935
1144
  <div class="task-detail-meta" id="skillDetailMeta"></div>
936
1145
  <div class="skill-detail-desc" id="skillDetailDesc"></div>
1146
+ <div id="skillShareActions" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:8px 0"></div>
937
1147
  <div class="task-detail-chunks-title" data-i18n="skills.files">Skill Files</div>
938
1148
  <div class="skill-files-list" id="skillFilesList"></div>
939
1149
  <div class="task-detail-chunks-title" id="skillContentTitle" data-i18n="skills.content">SKILL.md Content</div>
@@ -999,178 +1209,313 @@ input,textarea,select{font-family:inherit;font-size:inherit}
999
1209
 
1000
1210
  <!-- ─── Settings View ─── -->
1001
1211
  <div class="settings-view" id="settingsView">
1002
- <div class="settings-group" id="settingsModelConfig">
1003
- <h2 class="settings-group-title"><span data-i18n="settings.modelconfig">Model Configuration</span></h2>
1004
- <div class="settings-section">
1005
- <h3><span class="icon">\u{1F4CA}</span> <span data-i18n="settings.modelhealth">Model Health</span></h3>
1006
- <div class="model-health-bar" id="modelHealthBar">
1007
- <div style="font-size:12px;color:var(--text-muted);width:100%">Loading model status...</div>
1008
- </div>
1009
- </div>
1010
- <div class="settings-section">
1011
- <h3><span class="icon">\u{1F4E1}</span> <span data-i18n="settings.embedding">Embedding Model</span></h3>
1012
- <div class="settings-grid">
1013
- <div class="settings-field">
1014
- <label data-i18n="settings.provider">Provider</label>
1015
- <select id="cfgEmbProvider" onchange="onProviderChange('embedding')">
1016
- <option value="openai_compatible">OpenAI Compatible</option>
1017
- <option value="openai">OpenAI</option>
1018
- <option value="siliconflow">SiliconFlow (\u7845\u57FA\u6D41\u52A8)</option>
1019
- <option value="zhipu">Zhipu AI (\u667A\u8C31)</option>
1020
- <option value="bailian">Alibaba Bailian (\u767E\u70BC)</option>
1021
- <option value="gemini">Gemini</option>
1022
- <option value="azure_openai">Azure OpenAI</option>
1023
- <option value="cohere">Cohere</option>
1024
- <option value="mistral">Mistral</option>
1025
- <option value="voyage">Voyage</option>
1026
- <option value="local">Local</option>
1027
- </select>
1028
- </div>
1029
- <div class="settings-field">
1030
- <label data-i18n="settings.model">Model</label>
1031
- <input type="text" id="cfgEmbModel" placeholder="e.g. bge-m3">
1032
- </div>
1033
- <div class="settings-field full-width">
1034
- <label>Endpoint</label>
1035
- <input type="text" id="cfgEmbEndpoint" placeholder="https://...">
1036
- </div>
1037
- <div class="settings-field">
1038
- <label>API Key</label>
1039
- <input type="password" id="cfgEmbApiKey" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022">
1040
- </div>
1041
- </div>
1042
- <div class="test-conn-row">
1043
- <button class="btn btn-sm btn-ghost" onclick="testModel('embedding')" id="testEmbBtn" data-i18n="settings.test">Test Connection</button>
1044
- <span class="test-result" id="testEmbResult"></span>
1045
- </div>
1212
+ <div class="settings-tabs-bar">
1213
+ <button class="settings-tab-btn active" data-tab="embedding" onclick="switchSettingsTab('embedding',this)"><span class="stab-dot"></span><span data-i18n="settings.embedding">Embedding Model</span></button>
1214
+ <button class="settings-tab-btn" data-tab="summarizer" onclick="switchSettingsTab('summarizer',this)"><span class="stab-dot"></span><span data-i18n="settings.summarizer">Summarizer Model</span></button>
1215
+ <button class="settings-tab-btn" data-tab="skill" onclick="switchSettingsTab('skill',this)"><span class="stab-dot"></span><span data-i18n="settings.skill">Skill Evolution</span></button>
1216
+ <button class="settings-tab-btn" data-tab="hub" onclick="switchSettingsTab('hub',this)"><span class="stab-dot"></span><span data-i18n="settings.hub">Hub & Team</span></button>
1217
+ <button class="settings-tab-btn" data-tab="general" onclick="switchSettingsTab('general',this)"><span class="stab-dot"></span><span data-i18n="settings.general">General</span></button>
1046
1218
  </div>
1047
-
1048
- <div class="settings-section">
1049
- <h3><span class="icon">\u{1F9E0}</span> <span data-i18n="settings.summarizer">Summarizer Model</span></h3>
1050
- <div class="settings-grid">
1051
- <div class="settings-field">
1052
- <label data-i18n="settings.provider">Provider</label>
1053
- <select id="cfgSumProvider" onchange="onProviderChange('summarizer')">
1054
- <option value="openai_compatible">OpenAI Compatible</option>
1055
- <option value="openai">OpenAI</option>
1056
- <option value="siliconflow">SiliconFlow (\u7845\u57FA\u6D41\u52A8)</option>
1057
- <option value="zhipu">Zhipu AI (\u667A\u8C31)</option>
1058
- <option value="deepseek">DeepSeek</option>
1059
- <option value="bailian">Alibaba Bailian (\u767E\u70BC)</option>
1060
- <option value="moonshot">Moonshot (Kimi)</option>
1061
- <option value="anthropic">Anthropic</option>
1062
- <option value="gemini">Gemini</option>
1063
- <option value="azure_openai">Azure OpenAI</option>
1064
- <option value="bedrock">Bedrock</option>
1065
- </select>
1066
- </div>
1067
- <div class="settings-field">
1068
- <label data-i18n="settings.model">Model</label>
1069
- <input type="text" id="cfgSumModel" placeholder="e.g. gpt-4o-mini">
1070
- </div>
1071
- <div class="settings-field full-width">
1072
- <label>Endpoint</label>
1073
- <input type="text" id="cfgSumEndpoint" placeholder="https://...">
1074
- </div>
1075
- <div class="settings-field">
1076
- <label>API Key</label>
1077
- <input type="password" id="cfgSumApiKey" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022">
1219
+ <div class="settings-cards-grid">
1220
+
1221
+ <!-- ══ Card: Embedding Model ══ -->
1222
+ <div class="settings-card card-embedding stab-active" data-stab="embedding">
1223
+ <div class="settings-card-header">
1224
+ <div class="settings-card-icon" style="background:rgba(99,102,241,.1);border-color:rgba(99,102,241,.15)">\u{1F4E1}</div>
1225
+ <div class="settings-card-title-wrap">
1226
+ <div class="settings-card-title" data-i18n="settings.embedding">Embedding Model</div>
1227
+ <div class="settings-card-desc" data-i18n="settings.embedding.desc">Vector embedding model for memory search and retrieval</div>
1228
+ </div>
1078
1229
  </div>
1079
- <div class="settings-field">
1080
- <label data-i18n="settings.temperature">Temperature</label>
1081
- <input type="number" id="cfgSumTemp" step="0.1" min="0" max="2" placeholder="0">
1230
+ <div class="settings-card-body">
1231
+ <div class="settings-grid">
1232
+ <div class="settings-field">
1233
+ <label data-i18n="settings.provider">Provider</label>
1234
+ <select id="cfgEmbProvider" onchange="onProviderChange('embedding')">
1235
+ <option value="openai_compatible">OpenAI Compatible</option>
1236
+ <option value="openai">OpenAI</option>
1237
+ <option value="siliconflow">SiliconFlow (\u7845\u57FA\u6D41\u52A8)</option>
1238
+ <option value="zhipu">Zhipu AI (\u667A\u8C31)</option>
1239
+ <option value="bailian">Alibaba Bailian (\u767E\u70BC)</option>
1240
+ <option value="gemini">Gemini</option>
1241
+ <option value="azure_openai">Azure OpenAI</option>
1242
+ <option value="cohere">Cohere</option>
1243
+ <option value="mistral">Mistral</option>
1244
+ <option value="voyage">Voyage</option>
1245
+ <option value="local">Local</option>
1246
+ <option value="openclaw">OpenClaw Host</option>
1247
+ </select>
1248
+ </div>
1249
+ <div class="settings-field">
1250
+ <label data-i18n="settings.model">Model</label>
1251
+ <input type="text" id="cfgEmbModel" placeholder="e.g. bge-m3">
1252
+ </div>
1253
+ <div class="settings-field full-width">
1254
+ <label>Endpoint</label>
1255
+ <input type="text" id="cfgEmbEndpoint" placeholder="https://...">
1256
+ </div>
1257
+ <div class="settings-field">
1258
+ <label>API Key</label>
1259
+ <input type="password" id="cfgEmbApiKey" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022">
1260
+ </div>
1261
+ </div>
1262
+ <div class="test-conn-row">
1263
+ <button class="btn btn-sm btn-ghost" onclick="testModel('embedding')" id="testEmbBtn" data-i18n="settings.test">Test Connection</button>
1264
+ <span class="test-result" id="testEmbResult"></span>
1265
+ </div>
1082
1266
  </div>
1083
1267
  </div>
1084
- <div class="test-conn-row">
1085
- <button class="btn btn-sm btn-ghost" onclick="testModel('summarizer')" id="testSumBtn" data-i18n="settings.test">Test Connection</button>
1086
- <span class="test-result" id="testSumResult"></span>
1087
- </div>
1088
- </div>
1089
- </div>
1090
1268
 
1091
- <div class="settings-section">
1092
- <h3><span class="icon">\u{1F527}</span> <span data-i18n="settings.skill">Skill Evolution</span></h3>
1093
- <div class="settings-grid">
1094
- <div class="settings-toggle">
1095
- <label class="toggle-switch"><input type="checkbox" id="cfgSkillEnabled"><span class="toggle-slider"></span></label>
1096
- <label data-i18n="settings.skill.enabled">Enable Skill Evolution</label>
1097
- </div>
1098
- <div class="settings-toggle">
1099
- <label class="toggle-switch"><input type="checkbox" id="cfgSkillAutoInstall"><span class="toggle-slider"></span></label>
1100
- <label data-i18n="settings.skill.autoinstall">Auto Install Skills</label>
1101
- </div>
1102
- <div class="settings-field">
1103
- <label data-i18n="settings.skill.confidence">Min Confidence</label>
1104
- <input type="number" id="cfgSkillConfidence" step="0.1" min="0" max="1" placeholder="0.7">
1269
+ <!-- ══ Card: Summarizer Model ══ -->
1270
+ <div class="settings-card card-summarizer" data-stab="summarizer">
1271
+ <div class="settings-card-header">
1272
+ <div class="settings-card-icon" style="background:rgba(139,92,246,.1);border-color:rgba(139,92,246,.15)">\u{1F9E0}</div>
1273
+ <div class="settings-card-title-wrap">
1274
+ <div class="settings-card-title" data-i18n="settings.summarizer">Summarizer Model</div>
1275
+ <div class="settings-card-desc" data-i18n="settings.summarizer.desc">LLM for memory summarization, deduplication and analysis</div>
1276
+ </div>
1105
1277
  </div>
1106
- <div class="settings-field">
1107
- <label data-i18n="settings.skill.minchunks">Min Chunks</label>
1108
- <input type="number" id="cfgSkillMinChunks" placeholder="6">
1278
+ <div class="settings-card-body">
1279
+ <div class="settings-grid">
1280
+ <div class="settings-field">
1281
+ <label data-i18n="settings.provider">Provider</label>
1282
+ <select id="cfgSumProvider" onchange="onProviderChange('summarizer')">
1283
+ <option value="openai_compatible">OpenAI Compatible</option>
1284
+ <option value="openai">OpenAI</option>
1285
+ <option value="siliconflow">SiliconFlow (\u7845\u57FA\u6D41\u52A8)</option>
1286
+ <option value="zhipu">Zhipu AI (\u667A\u8C31)</option>
1287
+ <option value="deepseek">DeepSeek</option>
1288
+ <option value="bailian">Alibaba Bailian (\u767E\u70BC)</option>
1289
+ <option value="moonshot">Moonshot (Kimi)</option>
1290
+ <option value="anthropic">Anthropic</option>
1291
+ <option value="gemini">Gemini</option>
1292
+ <option value="azure_openai">Azure OpenAI</option>
1293
+ <option value="bedrock">Bedrock</option>
1294
+ <option value="openclaw">OpenClaw Host</option>
1295
+ </select>
1296
+ </div>
1297
+ <div class="settings-field">
1298
+ <label data-i18n="settings.model">Model</label>
1299
+ <input type="text" id="cfgSumModel" placeholder="e.g. gpt-4o-mini">
1300
+ </div>
1301
+ <div class="settings-field full-width">
1302
+ <label>Endpoint</label>
1303
+ <input type="text" id="cfgSumEndpoint" placeholder="https://...">
1304
+ </div>
1305
+ <div class="settings-field">
1306
+ <label>API Key</label>
1307
+ <input type="password" id="cfgSumApiKey" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022">
1308
+ </div>
1309
+ <div class="settings-field">
1310
+ <label data-i18n="settings.temperature">Temperature</label>
1311
+ <input type="number" id="cfgSumTemp" step="0.1" min="0" max="2" placeholder="0">
1312
+ </div>
1313
+ </div>
1314
+ <div class="test-conn-row">
1315
+ <button class="btn btn-sm btn-ghost" onclick="testModel('summarizer')" id="testSumBtn" data-i18n="settings.test">Test Connection</button>
1316
+ <span class="test-result" id="testSumResult"></span>
1317
+ </div>
1109
1318
  </div>
1110
1319
  </div>
1111
- <div style="margin-top:16px;padding-top:16px;border-top:1px solid var(--border)">
1112
- <h4 style="font-size:12px;font-weight:600;color:var(--text-sec);margin-bottom:12px"><span data-i18n="settings.skill.model">Skill Dedicated Model</span></h4>
1113
- <div class="field-hint" style="margin-bottom:12px" data-i18n="settings.skill.model.hint">If not configured, the main Summarizer Model above will be used for skill generation. Configure a dedicated model here for higher quality skill output.</div>
1114
- <div class="settings-grid">
1115
- <div class="settings-field">
1116
- <label data-i18n="settings.provider">Provider</label>
1117
- <select id="cfgSkillProvider" onchange="onProviderChange('skill')">
1118
- <option value="">— <span data-i18n="settings.skill.usemain">Use main summarizer</span> —</option>
1119
- <option value="openai_compatible">OpenAI Compatible</option>
1120
- <option value="openai">OpenAI</option>
1121
- <option value="siliconflow">SiliconFlow (\u7845\u57FA\u6D41\u52A8)</option>
1122
- <option value="zhipu">Zhipu AI (\u667A\u8C31)</option>
1123
- <option value="deepseek">DeepSeek</option>
1124
- <option value="bailian">Alibaba Bailian (\u767E\u70BC)</option>
1125
- <option value="moonshot">Moonshot (Kimi)</option>
1126
- <option value="anthropic">Anthropic</option>
1127
- <option value="gemini">Gemini</option>
1128
- <option value="azure_openai">Azure OpenAI</option>
1129
- <option value="bedrock">Bedrock</option>
1130
- </select>
1320
+
1321
+ <!-- ══ Card: Skill Evolution ══ -->
1322
+ <div class="settings-card card-skill" data-stab="skill">
1323
+ <div class="settings-card-header">
1324
+ <div class="settings-card-icon" style="background:rgba(245,158,11,.1);border-color:rgba(245,158,11,.15)">\u{1F527}</div>
1325
+ <div class="settings-card-title-wrap">
1326
+ <div class="settings-card-title" data-i18n="settings.skill">Skill Evolution</div>
1327
+ <div class="settings-card-desc" data-i18n="settings.skill.desc">Auto-extract reusable skills from conversation patterns</div>
1131
1328
  </div>
1132
- <div class="settings-field">
1133
- <label data-i18n="settings.model">Model</label>
1134
- <input type="text" id="cfgSkillModel" placeholder="e.g. claude-4.6-opus">
1329
+ </div>
1330
+ <div class="settings-card-body">
1331
+ <div class="settings-grid">
1332
+ <div class="settings-toggle">
1333
+ <label class="toggle-switch"><input type="checkbox" id="cfgSkillEnabled"><span class="toggle-slider"></span></label>
1334
+ <label data-i18n="settings.skill.enabled">Enable Skill Evolution</label>
1335
+ </div>
1336
+ <div class="settings-toggle">
1337
+ <label class="toggle-switch"><input type="checkbox" id="cfgSkillAutoInstall"><span class="toggle-slider"></span></label>
1338
+ <label data-i18n="settings.skill.autoinstall">Auto Install Skills</label>
1339
+ </div>
1340
+ <div class="settings-field">
1341
+ <label data-i18n="settings.skill.confidence">Min Confidence</label>
1342
+ <input type="number" id="cfgSkillConfidence" step="0.1" min="0" max="1" placeholder="0.7">
1343
+ </div>
1344
+ <div class="settings-field">
1345
+ <label data-i18n="settings.skill.minchunks">Min Chunks</label>
1346
+ <input type="number" id="cfgSkillMinChunks" placeholder="6">
1347
+ </div>
1135
1348
  </div>
1136
- <div class="settings-field full-width">
1137
- <label>Endpoint</label>
1138
- <input type="text" id="cfgSkillEndpoint" placeholder="https://...">
1349
+ <div class="settings-card-divider"></div>
1350
+ <div class="settings-card-subtitle" data-i18n="settings.skill.model">Skill Dedicated Model</div>
1351
+ <div class="field-hint" style="margin-bottom:12px" data-i18n="settings.skill.model.hint">If not configured, the main Summarizer Model above will be used for skill generation. Configure a dedicated model here for higher quality skill output.</div>
1352
+ <div class="settings-grid">
1353
+ <div class="settings-field">
1354
+ <label data-i18n="settings.provider">Provider</label>
1355
+ <select id="cfgSkillProvider" onchange="onProviderChange('skill')">
1356
+ <option value="">\u2014 <span data-i18n="settings.skill.usemain">Use main summarizer</span> \u2014</option>
1357
+ <option value="openai_compatible">OpenAI Compatible</option>
1358
+ <option value="openai">OpenAI</option>
1359
+ <option value="siliconflow">SiliconFlow (\u7845\u57FA\u6D41\u52A8)</option>
1360
+ <option value="zhipu">Zhipu AI (\u667A\u8C31)</option>
1361
+ <option value="deepseek">DeepSeek</option>
1362
+ <option value="bailian">Alibaba Bailian (\u767E\u70BC)</option>
1363
+ <option value="moonshot">Moonshot (Kimi)</option>
1364
+ <option value="anthropic">Anthropic</option>
1365
+ <option value="gemini">Gemini</option>
1366
+ <option value="azure_openai">Azure OpenAI</option>
1367
+ <option value="bedrock">Bedrock</option>
1368
+ <option value="openclaw">OpenClaw Host</option>
1369
+ </select>
1370
+ </div>
1371
+ <div class="settings-field">
1372
+ <label data-i18n="settings.model">Model</label>
1373
+ <input type="text" id="cfgSkillModel" placeholder="e.g. claude-4.6-opus">
1374
+ </div>
1375
+ <div class="settings-field full-width">
1376
+ <label>Endpoint</label>
1377
+ <input type="text" id="cfgSkillEndpoint" placeholder="https://...">
1378
+ </div>
1379
+ <div class="settings-field">
1380
+ <label>API Key</label>
1381
+ <input type="password" id="cfgSkillApiKey" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022">
1382
+ </div>
1139
1383
  </div>
1140
- <div class="settings-field">
1141
- <label>API Key</label>
1142
- <input type="password" id="cfgSkillApiKey" placeholder="\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022">
1384
+ <div class="test-conn-row">
1385
+ <button class="btn btn-sm btn-ghost" onclick="testModel('skill')" id="testSkillBtn" data-i18n="settings.test">Test Connection</button>
1386
+ <span class="test-result" id="testSkillResult"></span>
1143
1387
  </div>
1144
1388
  </div>
1145
- <div class="test-conn-row">
1146
- <button class="btn btn-sm btn-ghost" onclick="testModel('skill')" id="testSkillBtn" data-i18n="settings.test">Test Connection</button>
1147
- <span class="test-result" id="testSkillResult"></span>
1148
- </div>
1149
1389
  </div>
1150
- </div>
1151
1390
 
1152
- <div class="settings-section">
1153
- <h3><span class="icon">\u{1F4CA}</span> <span data-i18n="settings.telemetry">Telemetry</span></h3>
1154
- <div class="settings-grid">
1155
- <div class="settings-toggle">
1156
- <label class="toggle-switch"><input type="checkbox" id="cfgTelemetryEnabled" checked><span class="toggle-slider"></span></label>
1157
- <label data-i18n="settings.telemetry.enabled">Enable Anonymous Telemetry</label>
1391
+ <!-- ══ Card: Hub & Team ══ -->
1392
+ <div class="settings-card card-hub" id="settingsSharingConfig" data-stab="hub">
1393
+ <div class="settings-card-header">
1394
+ <div class="settings-card-icon" style="background:rgba(6,182,212,.1);border-color:rgba(6,182,212,.15)">\u{1F310}</div>
1395
+ <div class="settings-card-title-wrap">
1396
+ <div class="settings-card-title" data-i18n="settings.hub">Hub & Team</div>
1397
+ <div class="settings-card-desc" data-i18n="settings.hub.desc">Share memories, tasks and skills with your team</div>
1398
+ </div>
1158
1399
  </div>
1159
- <div class="settings-field full-width">
1160
- <div class="field-hint" data-i18n="settings.telemetry.hint">Anonymous usage analytics to help improve the plugin. Only sends tool names, latencies, and version info. No memory content, queries, or personal data is ever sent.</div>
1400
+ <div class="settings-card-body">
1401
+ <div class="field-hint" style="margin-bottom:12px" data-i18n="settings.hub.enable.hint">Enable to share memories, tasks and skills with your team. When disabled, all features work normally in local-only mode.</div>
1402
+ <div class="settings-toggle" style="margin-bottom:16px">
1403
+ <label class="toggle-switch">
1404
+ <input type="checkbox" id="cfgSharingEnabled" onchange="onSharingToggle()">
1405
+ <span class="toggle-slider"></span>
1406
+ </label>
1407
+ <label data-i18n="settings.hub.enable.label">Enable Hub Sharing</label>
1408
+ </div>
1409
+
1410
+ <div id="sharingConfigPanel" style="display:none">
1411
+ <div style="margin-bottom:14px">
1412
+ <label style="font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em;display:block;margin-bottom:6px" data-i18n="settings.hub.role">Role</label>
1413
+ <div style="display:flex;gap:8px">
1414
+ <button class="btn btn-sm" id="btnRoleHub" onclick="selectSharingRole('hub')" data-i18n="settings.hub.role.hub">Hub (Server)</button>
1415
+ <button class="btn btn-sm" id="btnRoleClient" onclick="selectSharingRole('client')" data-i18n="settings.hub.role.client">Client (Connect to Hub)</button>
1416
+ </div>
1417
+ <div class="field-hint" style="margin-top:6px" data-i18n="settings.hub.role.hint">Hub: this device runs the central server. Client: connect to an existing Hub.</div>
1418
+ </div>
1419
+
1420
+ <div id="hubModeConfig" style="display:none">
1421
+ <div style="background:rgba(52,199,89,.08);border:1px solid rgba(52,199,89,.2);border-radius:10px;padding:14px 18px;margin-bottom:14px;font-size:12px;line-height:1.7;color:var(--text-sec)">
1422
+ <div style="font-weight:700;color:var(--text);margin-bottom:4px" data-i18n="settings.hub.hubSteps.title">Quick Setup (3 steps)</div>
1423
+ <div><span style="color:var(--accent)">1.</span> <span data-i18n="settings.hub.hubSteps.s1">Fill in Team Name below (or keep default)</span></div>
1424
+ <div><span style="color:var(--accent)">2.</span> <span data-i18n="settings.hub.hubSteps.s2">Click "Save Settings", then restart OpenClaw gateway</span></div>
1425
+ <div><span style="color:var(--accent)">3.</span> <span data-i18n="settings.hub.hubSteps.s3">Share the Hub Address and Team Token below with your team members</span></div>
1426
+ </div>
1427
+ <div class="settings-grid">
1428
+ <div class="settings-field">
1429
+ <label data-i18n="settings.hub.port">Hub Port</label>
1430
+ <input type="number" id="cfgHubPort" placeholder="18800" value="18800">
1431
+ <div class="field-hint" data-i18n="settings.hub.port.hint">Port for Hub service. Default: 18800</div>
1432
+ </div>
1433
+ <div class="settings-field">
1434
+ <label data-i18n="settings.hub.teamName">Team Name</label>
1435
+ <input type="text" id="cfgHubTeamName" placeholder="My Team">
1436
+ <div class="field-hint" data-i18n="settings.hub.teamName.hint">Your team display name</div>
1437
+ </div>
1438
+ <div class="settings-field full-width">
1439
+ <label data-i18n="settings.hub.teamToken">Team Token</label>
1440
+ <input type="text" id="cfgHubTeamToken" readonly style="background:var(--bg);cursor:pointer" onclick="this.select();document.execCommand('copy');toast(t('settings.hub.tokenCopied'),'success')">
1441
+ <div class="field-hint" data-i18n="settings.hub.teamToken.hint">Auto-generated secret for clients to join. Click to copy. Share this with your team members.</div>
1442
+ </div>
1443
+ </div>
1444
+ <div id="hubShareInfo" class="hub-info-card hic-share" style="display:none;margin-top:14px">
1445
+ <div class="hic-title"><span class="hic-icon">\u{1F4CB}</span> <span data-i18n="settings.hub.shareInfo.title">Share this info with your team members:</span></div>
1446
+ <div id="hubShareInfoContent" class="hic-grid"></div>
1447
+ </div>
1448
+ </div>
1449
+
1450
+ <div id="clientModeConfig" style="display:none">
1451
+ <div style="background:rgba(99,102,241,.08);border:1px solid rgba(99,102,241,.2);border-radius:10px;padding:14px 18px;margin-bottom:14px;font-size:12px;line-height:1.7;color:var(--text-sec)">
1452
+ <div style="font-weight:700;color:var(--text);margin-bottom:4px" data-i18n="settings.hub.clientSteps.title">Quick Setup (3 steps)</div>
1453
+ <div><span style="color:var(--accent)">1.</span> <span data-i18n="settings.hub.clientSteps.s1">Ask your Hub admin for Hub Address and Team Token</span></div>
1454
+ <div><span style="color:var(--accent)">2.</span> <span data-i18n="settings.hub.clientSteps.s2">Fill them in below, click "Test Connection" to verify</span></div>
1455
+ <div><span style="color:var(--accent)">3.</span> <span data-i18n="settings.hub.clientSteps.s3">Click "Save Settings", then restart OpenClaw gateway to connect</span></div>
1456
+ </div>
1457
+ <div class="settings-grid">
1458
+ <div class="settings-field full-width">
1459
+ <label data-i18n="settings.hub.hubAddress">Hub Address</label>
1460
+ <input type="text" id="cfgClientHubAddress" placeholder="e.g. 192.168.1.100:18800">
1461
+ <div class="field-hint" data-i18n="settings.hub.hubAddress.hint">Hub server address, e.g. 192.168.1.100:18800 or hub.example.com:18800</div>
1462
+ </div>
1463
+ <div class="settings-field">
1464
+ <label data-i18n="settings.hub.teamTokenClient">Team Token</label>
1465
+ <input type="text" id="cfgClientTeamToken" placeholder="">
1466
+ <div class="field-hint" data-i18n="settings.hub.teamTokenClient.hint">Get this from your Hub admin to join the team</div>
1467
+ </div>
1468
+ <div class="settings-field">
1469
+ <label data-i18n="settings.hub.userToken">User Token</label>
1470
+ <input type="text" id="cfgClientUserToken" placeholder="">
1471
+ <div class="field-hint" data-i18n="settings.hub.userToken.hint">Usually auto-obtained after joining. Leave blank in most cases.</div>
1472
+ </div>
1473
+ </div>
1474
+ <div style="margin-top:12px">
1475
+ <button class="btn btn-sm btn-primary" id="btnTestHubConn" onclick="testHubConnection()" data-i18n="settings.hub.testConnection">Test Connection</button>
1476
+ <span id="hubConnTestResult" style="margin-left:10px;font-size:12px"></span>
1477
+ </div>
1478
+ </div>
1479
+ </div>
1480
+
1481
+ <div class="settings-card-divider"></div>
1482
+ <div id="sharingStatusPanel"></div>
1483
+ <div id="sharingTeamPanel"></div>
1484
+ <div id="sharingAdminPanel"></div>
1161
1485
  </div>
1162
1486
  </div>
1163
- </div>
1164
1487
 
1165
- <div class="settings-section">
1166
- <h3><span class="icon">\u{1F4BE}</span> <span data-i18n="settings.general">General</span></h3>
1167
- <div class="settings-grid">
1168
- <div class="settings-field">
1169
- <label data-i18n="settings.viewerport">Viewer Port</label>
1170
- <input type="number" id="cfgViewerPort" placeholder="18799">
1171
- <div class="field-hint" data-i18n="settings.viewerport.hint">Requires restart to take effect</div>
1488
+ <!-- ══ Card: General ══ -->
1489
+ <div class="settings-card card-general" id="settingsModelConfig" data-stab="general">
1490
+ <div class="settings-card-header">
1491
+ <div class="settings-card-icon" style="background:rgba(16,185,129,.1);border-color:rgba(16,185,129,.15)">\u2699\uFE0F</div>
1492
+ <div class="settings-card-title-wrap">
1493
+ <div class="settings-card-title" data-i18n="settings.general">General</div>
1494
+ <div class="settings-card-desc" data-i18n="settings.general.desc">System status, ports and telemetry</div>
1495
+ </div>
1496
+ </div>
1497
+ <div class="settings-card-body">
1498
+ <div class="settings-card-subtitle" data-i18n="settings.modelhealth">\u{1F4CA} Model Health</div>
1499
+ <div class="model-health-bar" id="modelHealthBar">
1500
+ <div style="font-size:12px;color:var(--text-muted);width:100%">Loading model status...</div>
1501
+ </div>
1502
+ <div class="settings-card-divider"></div>
1503
+ <div class="settings-grid">
1504
+ <div class="settings-field">
1505
+ <label data-i18n="settings.viewerport">Viewer Port</label>
1506
+ <input type="number" id="cfgViewerPort" placeholder="18799">
1507
+ <div class="field-hint" data-i18n="settings.viewerport.hint">Requires restart to take effect</div>
1508
+ </div>
1509
+ </div>
1510
+ <div class="settings-card-divider"></div>
1511
+ <div class="settings-toggle">
1512
+ <label class="toggle-switch"><input type="checkbox" id="cfgTelemetryEnabled" checked><span class="toggle-slider"></span></label>
1513
+ <label data-i18n="settings.telemetry.enabled">Enable Anonymous Telemetry</label>
1514
+ </div>
1515
+ <div class="field-hint" style="margin-top:6px" data-i18n="settings.telemetry.hint">Anonymous usage analytics to help improve the plugin. Only sends tool names, latencies, and version info. No memory content, queries, or personal data is ever sent.</div>
1172
1516
  </div>
1173
1517
  </div>
1518
+
1174
1519
  </div>
1175
1520
 
1176
1521
  <div class="settings-actions">
@@ -1181,6 +1526,30 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1181
1526
  <div style="font-size:11px;color:var(--text-muted);text-align:right;margin-top:4px" data-i18n="settings.restart.hint">Some changes require restarting the OpenClaw gateway to take effect.</div>
1182
1527
  </div>
1183
1528
 
1529
+ <!-- ─── Admin Page ─── -->
1530
+ <div class="admin-view" id="adminView">
1531
+ <div class="admin-header">
1532
+ <div class="admin-header-top">
1533
+ <h2><span class="ah-icon">\u{1F6E1}</span> <span data-i18n="admin.title">Hub Admin Panel</span></h2>
1534
+ <button class="btn btn-sm btn-ghost" onclick="loadAdminData()" style="backdrop-filter:blur(8px)" data-i18n="admin.refresh">\u21BB Refresh</button>
1535
+ </div>
1536
+ <div class="admin-header-sub" data-i18n="admin.subtitle">Manage team members, groups, and shared resources</div>
1537
+ <div class="admin-stat-row" id="adminStats"></div>
1538
+ </div>
1539
+ <div class="admin-tabs" id="adminTabsBar">
1540
+ <button class="admin-tab active" onclick="switchAdminTab('users',this)"><span class="at-icon">\u{1F465}</span> <span data-i18n="admin.tab.users">Users</span> <span class="at-count" id="adminTabCountUsers">0</span></button>
1541
+ <button class="admin-tab" onclick="switchAdminTab('groups',this)"><span class="at-icon">\u{1F4C2}</span> <span data-i18n="admin.tab.groups">Groups</span> <span class="at-count" id="adminTabCountGroups">0</span></button>
1542
+ <button class="admin-tab" onclick="switchAdminTab('sharedMemories',this)"><span class="at-icon">\u{1F4AD}</span> <span data-i18n="admin.tab.sharedMemories">Shared Memories</span> <span class="at-count" id="adminTabCountMemories">0</span></button>
1543
+ <button class="admin-tab" onclick="switchAdminTab('memories',this)"><span class="at-icon">\u{1F4CB}</span> <span data-i18n="admin.tab.memories">Shared Tasks</span> <span class="at-count" id="adminTabCountTasks">0</span></button>
1544
+ <button class="admin-tab" onclick="switchAdminTab('skills',this)"><span class="at-icon">\u{1F9E0}</span> <span data-i18n="admin.tab.skills">Shared Skills</span> <span class="at-count" id="adminTabCountSkills">0</span></button>
1545
+ </div>
1546
+ <div class="admin-panel active" id="adminUsersPanel"></div>
1547
+ <div class="admin-panel" id="adminGroupsPanel"></div>
1548
+ <div class="admin-panel" id="adminSharedMemoriesPanel"></div>
1549
+ <div class="admin-panel" id="adminMemoriesPanel"></div>
1550
+ <div class="admin-panel" id="adminSkillsPanel"></div>
1551
+ </div>
1552
+
1184
1553
  <!-- ─── Import Page ─── -->
1185
1554
  <div class="migrate-view" id="migrateView">
1186
1555
  <div class="settings-section" style="border:1px solid rgba(99,102,241,.15)">
@@ -1374,6 +1743,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1374
1743
 
1375
1744
  <script>
1376
1745
  let activeSession=null,activeRole='',editingId=null,searchTimer=null,memoryCache={},currentPage=1,totalPages=1,totalCount=0,PAGE_SIZE=40,metricsDays=30;
1746
+ let memorySearchScope='local',skillSearchScope='local',taskSearchScope='local';
1377
1747
  let _embeddingWarningShown=false;
1378
1748
 
1379
1749
  /* ─── i18n ─── */
@@ -1419,6 +1789,14 @@ const I18N={
1419
1789
  'skills.active':'Active',
1420
1790
  'skills.installed':'Installed',
1421
1791
  'skills.public':'Public',
1792
+ 'skills.search.placeholder':'Search skills...',
1793
+ 'skills.search.local':'Local',
1794
+ 'skills.search.noresult':'No matching skills found',
1795
+ 'skills.load.error':'Failed to load skills',
1796
+ 'skills.hub.title':'\u{1F310} Hub Skills',
1797
+ 'scope.local':'Local',
1798
+ 'scope.group':'Group',
1799
+ 'scope.all':'All',
1422
1800
  'skills.visibility.public':'Public',
1423
1801
  'skills.visibility.private':'Private',
1424
1802
  'skills.setPublic':'Set Public',
@@ -1457,6 +1835,7 @@ const I18N={
1457
1835
  'filter.newest':'Newest first',
1458
1836
  'filter.oldest':'Oldest first',
1459
1837
  'filter.allowners':'All owners',
1838
+ 'filter.allsessions':'All sessions',
1460
1839
  'filter.public':'Public',
1461
1840
  'filter.private':'Private',
1462
1841
  'filter.allvisibility':'All visibility',
@@ -1542,6 +1921,15 @@ const I18N={
1542
1921
  'settings.summarizer':'Summarizer Model',
1543
1922
  'settings.skill':'Skill Evolution',
1544
1923
  'settings.general':'General',
1924
+ 'settings.embedding.desc':'Vector embedding model for memory search and retrieval',
1925
+ 'settings.summarizer.desc':'LLM for memory summarization, deduplication and analysis',
1926
+ 'settings.skill.desc':'Auto-extract reusable skills from conversation patterns',
1927
+ 'settings.hub.desc':'Share memories, tasks and skills with your team',
1928
+ 'settings.general.desc':'System status, ports and telemetry',
1929
+ 'settings.hostproxy.embedding':'Use Gateway Model',
1930
+ 'settings.hostproxy.completion':'Use Gateway Model',
1931
+ 'settings.hostproxy.skill':'Use Gateway Model',
1932
+ 'settings.hostproxy.hint.short':'Reuse gateway model config',
1545
1933
  'settings.provider':'Provider',
1546
1934
  'settings.model':'Model',
1547
1935
  'settings.temperature':'Temperature',
@@ -1550,12 +1938,12 @@ const I18N={
1550
1938
  'settings.skill.confidence':'Min Confidence',
1551
1939
  'settings.skill.minchunks':'Min Chunks',
1552
1940
  'settings.skill.model':'Skill Dedicated Model',
1553
- 'settings.skill.model.hint':'If not configured, the main Summarizer Model above will be used for skill generation. Configure a dedicated model here for higher quality skill output.',
1941
+ 'settings.skill.model.hint':'Leave empty to reuse the Summarizer model. Set a dedicated one for higher quality.',
1554
1942
  'settings.optional':'Optional',
1555
1943
  'settings.skill.usemain':'Use Main Summarizer',
1556
1944
  'settings.telemetry':'Telemetry',
1557
1945
  'settings.telemetry.enabled':'Enable Anonymous Telemetry',
1558
- 'settings.telemetry.hint':'Anonymous usage analytics to help improve the plugin. Only sends tool names, latencies, and version info. No memory content, queries, or personal data is ever sent.',
1946
+ 'settings.telemetry.hint':'Only collects tool names, latencies and version info. No memory content or personal data.',
1559
1947
  'settings.viewerport':'Viewer Port',
1560
1948
  'settings.viewerport.hint':'Requires restart to take effect',
1561
1949
  'settings.test':'Test Connection',
@@ -1652,7 +2040,7 @@ const I18N={
1652
2040
  'skills.related':'Related Tasks',
1653
2041
  'skills.download':'\u2B07 Download',
1654
2042
  'skills.installed.badge':'Installed',
1655
- 'skills.empty':'No skills yet. Skills are automatically generated from completed tasks that contain reusable experience.',
2043
+ 'skills.empty':'No skills yet. Skills are auto-generated from completed tasks with reusable patterns.',
1656
2044
  'skills.loading':'Loading...',
1657
2045
  'skills.error':'Error loading skill',
1658
2046
  'skills.error.detail':'Failed to load skill: ',
@@ -1673,6 +2061,203 @@ const I18N={
1673
2061
  'tasks.error':'Error',
1674
2062
  'tasks.error.detail':'Failed to load task details',
1675
2063
  'tasks.untitled.related':'Untitled',
2064
+ 'tab.admin':'\u{1F6E1} Admin',
2065
+ 'settings.hub':'Hub & Team',
2066
+ 'settings.hub.enable':'Enable Hub Sharing',
2067
+ 'settings.hub.enable.hint':'When off, everything works locally as usual.',
2068
+ 'settings.hub.enable.label':'Enable Hub Sharing',
2069
+ 'settings.hub.role':'Role',
2070
+ 'settings.hub.role.hub':'Hub (Server)',
2071
+ 'settings.hub.role.client':'Client (Connect to Hub)',
2072
+ 'settings.hub.role.hint':'Hub = run the server; Client = connect to one.',
2073
+ 'settings.hub.port':'Hub Port',
2074
+ 'settings.hub.port.hint':'Port for Hub service. Default: 18800',
2075
+ 'settings.hub.teamName':'Team Name',
2076
+ 'settings.hub.teamName.hint':'Display name for your team',
2077
+ 'settings.hub.teamToken':'Team Token',
2078
+ 'settings.hub.teamToken.hint':'Auto-generated secret for clients to join. Click to copy. Share this with your team members.',
2079
+ 'settings.hub.tokenCopied':'Team Token copied!',
2080
+ 'settings.hub.hubSteps.title':'Quick Setup (3 steps)',
2081
+ 'settings.hub.hubSteps.s1':'Fill in Team Name below (or keep default)',
2082
+ 'settings.hub.hubSteps.s2':'Click "Save Settings", then restart OpenClaw gateway',
2083
+ 'settings.hub.hubSteps.s3':'Share the Hub Address and Team Token below with your team members',
2084
+ 'settings.hub.clientSteps.title':'Quick Setup (3 steps)',
2085
+ 'settings.hub.clientSteps.s1':'Ask your Hub admin for Hub Address and Team Token',
2086
+ 'settings.hub.clientSteps.s2':'Fill them in below, click "Test Connection" to verify',
2087
+ 'settings.hub.clientSteps.s3':'Click "Save Settings", then restart OpenClaw gateway to connect',
2088
+ 'settings.hub.shareInfo.title':'Share this info with your team members:',
2089
+ 'settings.hub.shareInfo.yourIP':'your-IP',
2090
+ 'settings.hub.shareInfo.clickCopy':'Click to copy',
2091
+ 'settings.hub.restartAlert':'Hub sharing config saved! Please restart the OpenClaw gateway for changes to take effect.\\n\\nRun: openclaw gateway stop && openclaw gateway',
2092
+ 'settings.hub.hubAddress':'Hub Address',
2093
+ 'settings.hub.hubAddress.hint':'Hub server address, e.g. 192.168.1.100:18800',
2094
+ 'settings.hub.teamTokenClient':'Team Token',
2095
+ 'settings.hub.teamTokenClient.hint':'Get this from your Hub admin to join the team',
2096
+ 'settings.hub.userToken':'User Token',
2097
+ 'settings.hub.userToken.hint':'Usually auto-obtained after joining. Only fill if given by admin.',
2098
+ 'settings.hub.testConnection':'Test Connection',
2099
+ 'settings.hub.test.noAddr':'Please enter Hub address first',
2100
+ 'settings.hub.test.testing':'Testing...',
2101
+ 'settings.hub.test.ok':'Connected successfully',
2102
+ 'settings.hub.test.fail':'Connection failed',
2103
+ 'settings.hub.connection':'Connection Status',
2104
+ 'settings.hub.team':'Team & Groups',
2105
+ 'settings.hub.adminPending':'Admin Pending Users',
2106
+ 'sidebar.hub':'\u{1F310} Team Sharing',
2107
+ 'sharing.sidebar.connected':'Connected',
2108
+ 'sharing.sidebar.disconnected':'Disconnected',
2109
+ 'sharing.sidebar.starting':'Starting...',
2110
+ 'sharing.sidebar.notConfigured':'Not configured',
2111
+ 'sharing.sidebar.identity':'Identity:',
2112
+ 'sharing.sidebar.admin':'Admin',
2113
+ 'sharing.sidebar.targetHub':'Target Hub:',
2114
+ 'admin.title':'Hub Admin Panel',
2115
+ 'admin.subtitle':'Manage team members, groups, and shared resources',
2116
+ 'admin.refresh':'\u21BB Refresh',
2117
+ 'admin.tab.users':'Users',
2118
+ 'admin.tab.groups':'Groups',
2119
+ 'admin.tab.memories':'Shared Tasks',
2120
+ 'admin.tab.skills':'Shared Skills',
2121
+ 'admin.stat.activeUsers':'Active Users',
2122
+ 'admin.stat.pending':'Pending',
2123
+ 'admin.stat.groups':'Groups',
2124
+ 'admin.stat.sharedTasks':'Shared Tasks',
2125
+ 'admin.stat.sharedSkills':'Shared Skills',
2126
+ 'admin.stat.sharedMemories':'Shared Memories',
2127
+ 'admin.pendingApproval':'Pending Approval',
2128
+ 'admin.activeUsers':'Active Users',
2129
+ 'admin.noActiveUsers':'No active users.',
2130
+ 'admin.approve':'Approve',
2131
+ 'admin.reject':'Reject',
2132
+ 'admin.device':'Device: ',
2133
+ 'admin.groups':'Groups',
2134
+ 'admin.newGroup':'+ New Group',
2135
+ 'admin.groupName':'Group name',
2136
+ 'admin.groupDesc':'Description (optional)',
2137
+ 'admin.create':'Create',
2138
+ 'admin.cancel':'Cancel',
2139
+ 'admin.delete':'Delete',
2140
+ 'admin.members':'Members',
2141
+ 'admin.noGroups':'No groups created yet.',
2142
+ 'admin.noMembers':'No members.',
2143
+ 'admin.add':'Add',
2144
+ 'admin.remove':'Remove',
2145
+ 'admin.sharedTasks':'Shared Tasks',
2146
+ 'admin.noSharedTasks':'No shared tasks on Hub.',
2147
+ 'admin.owner':'Owner: ',
2148
+ 'admin.group':'Group: ',
2149
+ 'admin.chunks':'Chunks: ',
2150
+ 'admin.updated':'Updated: ',
2151
+ 'admin.sharedSkills':'Shared Skills',
2152
+ 'admin.noSharedSkills':'No shared skills on Hub.',
2153
+ 'admin.sharedMemories':'Shared Memories',
2154
+ 'admin.noSharedMemories':'No shared memories on Hub.',
2155
+ 'admin.tab.sharedMemories':'Shared Memories',
2156
+ 'admin.version':'v',
2157
+ 'admin.quality':'Quality: ',
2158
+ 'admin.membersCount':'Members ({n}):',
2159
+ 'admin.noMembersYet':'No members yet.',
2160
+ 'admin.loadFailed':'Failed to load admin data: ',
2161
+ 'admin.groupsFailed':'Failed to load groups: ',
2162
+ 'toast.userApproved':'User approved',
2163
+ 'toast.userRejected':'User rejected',
2164
+ 'toast.approveFail':'Approve failed',
2165
+ 'toast.rejectFail':'Reject failed',
2166
+ 'toast.groupCreated':'Group created',
2167
+ 'toast.groupDeleted':'Group deleted',
2168
+ 'toast.memberAdded':'Member added',
2169
+ 'toast.memberRemoved':'Member removed',
2170
+ 'toast.taskRemoved':'Task removed',
2171
+ 'toast.skillRemoved':'Skill removed',
2172
+ 'toast.memoryRemoved':'Memory removed',
2173
+ 'toast.createFail':'Create failed',
2174
+ 'toast.deleteFail':'Delete failed',
2175
+ 'toast.addFail':'Add failed',
2176
+ 'toast.removeFail':'Remove failed',
2177
+ 'toast.groupNameRequired':'Group name is required',
2178
+ 'confirm.rejectUser':'Reject this user?',
2179
+ 'confirm.removeGroupMember':'Remove this member from the group?',
2180
+ 'confirm.removeMember':'Remove this member?',
2181
+ 'confirm.deleteGroup':'Delete group "{name}"? Members will be removed.',
2182
+ 'confirm.deleteGroupShort':'Delete group "{name}"?',
2183
+ 'confirm.removeTask':'Remove shared task "{name}" from Hub? This cannot be undone.',
2184
+ 'confirm.removeSkill':'Remove shared skill "{name}" from Hub? This cannot be undone.',
2185
+ 'confirm.removeMemory':'Remove shared memory "{name}" from Hub? This cannot be undone.',
2186
+ 'sharing.disabled':'Sharing disabled',
2187
+ 'sharing.disabled.hint':'Enable sharing in plugin config to connect a Hub.',
2188
+ 'sharing.hubAdmin':'Hub Admin',
2189
+ 'sharing.client':'Client',
2190
+ 'sharing.hubMode':'Hub mode',
2191
+ 'sharing.hubMode.status':'Status: not connected to self',
2192
+ 'sharing.hubMode.hint':'Configure sharing.client with hubAddress and userToken pointing to this Hub to enable admin UI.',
2193
+ 'sharing.clientConfigured':'Client configured',
2194
+ 'sharing.clientDisconnected':'Status: disconnected',
2195
+ 'sharing.clientDisconnected.hint':'Viewer will keep showing local data; Hub actions may fail until the connection is restored.',
2196
+ 'sharing.clientNotConfigured':'Client not configured',
2197
+ 'sharing.clientNotConfigured.hint':'Set hubAddress and userToken in sharing.client to enable team features.',
2198
+ 'sharing.settingsDisabled':'Sharing is disabled.',
2199
+ 'sharing.settingsDisabled.hint':'Enable sharing in config to use Hub memory and skill collaboration.',
2200
+ 'sharing.noTeam':'No team connection.',
2201
+ 'sharing.adminUnavailable':'Admin tools unavailable.',
2202
+ 'sharing.adminEnabled':'Admin controls enabled',
2203
+ 'sharing.adminPendingHint':'Pending users will appear below.',
2204
+ 'sharing.notAdmin':'Current user is not an admin.',
2205
+ 'sharing.pendingLoadFail':'Failed to load pending users: ',
2206
+ 'sharing.noPending':'No pending users.',
2207
+ 'sharing.manageGroups':'Manage Groups',
2208
+ 'sharing.openAdmin':'Open Admin Panel',
2209
+ 'sharing.saveUsername':'Save',
2210
+ 'sharing.username.invalid':'Username must be 2-32 characters',
2211
+ 'sharing.username.taken':'Username already taken',
2212
+ 'sharing.username.updated':'Username updated',
2213
+ 'sharing.username.error':'Failed to update username',
2214
+ 'sharing.hubNotConfigured':'Hub is running but client connection is not configured.',
2215
+ 'sharing.hubNotConfigured.hint':'Add sharing.client.hubAddress and sharing.client.userToken pointing to this Hub to enable the admin interface.',
2216
+ 'sharing.notConnected':'Not connected to Hub.',
2217
+ 'sharing.role':'Role:',
2218
+ 'sharing.mode':'Mode:',
2219
+ 'sharing.role.hub':'Server (hosting the Hub)',
2220
+ 'sharing.role.client':'Member (connected to Hub)',
2221
+ 'sharing.clientConfiguredLabel':'Client configured:',
2222
+ 'sharing.configuredHub':'Configured Hub:',
2223
+ 'sharing.connected':'Connected:',
2224
+ 'sharing.yes':'yes',
2225
+ 'sharing.no':'no',
2226
+ 'sharing.user':'User:',
2227
+ 'sharing.team':'Team:',
2228
+ 'sharing.groups':'Groups:',
2229
+ 'sharing.loading':'Loading...',
2230
+ 'sharing.loadingGroups':'Loading groups...',
2231
+ 'sharing.noGroupsYet':'No groups yet.',
2232
+ 'search.localResults':'Local Results',
2233
+ 'search.hubResults':'Hub Results',
2234
+ 'search.noLocal':'No local results.',
2235
+ 'search.noHub':'No Hub results.',
2236
+ 'search.viewDetail':'View Detail',
2237
+ 'search.sharedMemory':'Shared Memory',
2238
+ 'search.loadFailed':'Failed to load shared memory',
2239
+ 'share.alreadyShared':'Shared',
2240
+ 'share.shareBtn':'Share',
2241
+ 'share.updateBtn':'Update',
2242
+ 'share.unshareBtn':'Unshare',
2243
+ 'toast.taskShared':'Task shared',
2244
+ 'toast.taskShareFail':'Task share failed',
2245
+ 'toast.taskUnshared':'Task unshared',
2246
+ 'toast.taskUnshareFail':'Task unshare failed',
2247
+ 'toast.memoryShared':'Memory shared',
2248
+ 'toast.memoryShareFail':'Memory share failed',
2249
+ 'toast.memoryUnshared':'Memory unshared',
2250
+ 'toast.memoryUnshareFail':'Memory unshare failed',
2251
+ 'toast.skillShared':'Skill shared',
2252
+ 'toast.skillShareFail':'Skill share failed',
2253
+ 'toast.skillUnshared':'Skill unshared',
2254
+ 'toast.skillUnshareFail':'Skill unshare failed',
2255
+ 'share.memoryVisibilityPrompt':'Share visibility (public or group):',
2256
+ 'share.memoryUnshareConfirm':'Unshare this memory?',
2257
+ 'share.group':'Group',
2258
+ 'share.public':'Public',
2259
+ 'toast.skillPulled':'Skill pulled to local storage',
2260
+ 'toast.skillPullFail':'Skill pull failed',
1676
2261
  'task.edit':'Edit',
1677
2262
  'task.delete':'Delete',
1678
2263
  'task.save':'Save',
@@ -1693,10 +2278,11 @@ const I18N={
1693
2278
  'skill.save.error':'Failed to save skill: ',
1694
2279
  'update.available':'New version available',
1695
2280
  'update.run':'Run',
1696
- 'update.btn':'Update now',
1697
- 'update.installing':'Installing update...',
1698
- 'update.success':'Update installed! Restarting...',
2281
+ 'update.btn':'Update',
2282
+ 'update.installing':'Installing...',
2283
+ 'update.success':'Updated!',
1699
2284
  'update.failed':'Update failed',
2285
+ 'update.restarting':'Restarting service...',
1700
2286
  'update.dismiss':'Dismiss'
1701
2287
  },
1702
2288
  zh:{
@@ -1740,6 +2326,14 @@ const I18N={
1740
2326
  'skills.active':'生效中',
1741
2327
  'skills.installed':'已安装',
1742
2328
  'skills.public':'公开',
2329
+ 'skills.search.placeholder':'搜索技能...',
2330
+ 'skills.search.local':'本地',
2331
+ 'skills.search.noresult':'未找到匹配的技能',
2332
+ 'skills.load.error':'加载技能失败',
2333
+ 'skills.hub.title':'\u{1F310} Hub 共享技能',
2334
+ 'scope.local':'本地',
2335
+ 'scope.group':'团队',
2336
+ 'scope.all':'全部',
1743
2337
  'skills.visibility.public':'公开',
1744
2338
  'skills.visibility.private':'私有',
1745
2339
  'skills.setPublic':'设为公开',
@@ -1778,6 +2372,7 @@ const I18N={
1778
2372
  'filter.newest':'最新优先',
1779
2373
  'filter.oldest':'最早优先',
1780
2374
  'filter.allowners':'所有归属',
2375
+ 'filter.allsessions':'全部会话',
1781
2376
  'filter.public':'公开',
1782
2377
  'filter.private':'私有',
1783
2378
  'filter.allvisibility':'所有可见性',
@@ -1863,6 +2458,15 @@ const I18N={
1863
2458
  'settings.summarizer':'摘要模型',
1864
2459
  'settings.skill':'技能进化',
1865
2460
  'settings.general':'通用设置',
2461
+ 'settings.embedding.desc':'向量嵌入模型,用于记忆检索和语义搜索',
2462
+ 'settings.summarizer.desc':'大语言模型,用于记忆摘要、去重和分析',
2463
+ 'settings.skill.desc':'从对话模式中自动提取可复用的技能',
2464
+ 'settings.hub.desc':'与团队共享记忆、任务和技能',
2465
+ 'settings.general.desc':'系统状态、端口和数据统计',
2466
+ 'settings.hostproxy.embedding':'复用网关模型',
2467
+ 'settings.hostproxy.completion':'复用网关模型',
2468
+ 'settings.hostproxy.skill':'复用网关模型',
2469
+ 'settings.hostproxy.hint.short':'复用网关已有的模型配置',
1866
2470
  'settings.provider':'服务商',
1867
2471
  'settings.model':'模型',
1868
2472
  'settings.temperature':'温度',
@@ -1871,12 +2475,12 @@ const I18N={
1871
2475
  'settings.skill.confidence':'最低置信度',
1872
2476
  'settings.skill.minchunks':'最少记忆片段',
1873
2477
  'settings.skill.model':'技能专用模型',
1874
- 'settings.skill.model.hint':'不配置时默认使用上方的摘要模型进行技能生成。如需更高质量的技能输出,可在此单独配置一个更强的模型。',
2478
+ 'settings.skill.model.hint':'不配置则复用摘要模型。如需更高质量可单独指定。',
1875
2479
  'settings.optional':'可选',
1876
2480
  'settings.skill.usemain':'使用主摘要模型',
1877
2481
  'settings.telemetry':'数据统计',
1878
2482
  'settings.telemetry.enabled':'启用匿名数据统计',
1879
- 'settings.telemetry.hint':'匿名使用统计,帮助改进插件。仅发送工具名称、响应时间和版本信息,不会发送任何记忆内容、搜索查询或个人数据。',
2483
+ 'settings.telemetry.hint':'仅收集工具名称、响应时间和版本号,不涉及任何记忆内容或个人数据。',
1880
2484
  'settings.viewerport':'Viewer 端口',
1881
2485
  'settings.viewerport.hint':'修改后需重启网关生效',
1882
2486
  'settings.test':'测试连接',
@@ -1973,7 +2577,7 @@ const I18N={
1973
2577
  'skills.related':'关联任务',
1974
2578
  'skills.download':'\u2B07 下载',
1975
2579
  'skills.installed.badge':'已安装',
1976
- 'skills.empty':'暂无技能。技能会从已完成的、包含可复用经验的任务中自动生成。',
2580
+ 'skills.empty':'暂无技能。技能会从已完成的任务中自动提炼生成。',
1977
2581
  'skills.loading':'加载中...',
1978
2582
  'skills.error':'加载技能失败',
1979
2583
  'skills.error.detail':'加载技能失败:',
@@ -1994,6 +2598,203 @@ const I18N={
1994
2598
  'tasks.error':'出错了',
1995
2599
  'tasks.error.detail':'加载任务详情失败',
1996
2600
  'tasks.untitled.related':'未命名',
2601
+ 'tab.admin':'\u{1F6E1} 管理',
2602
+ 'settings.hub':'Hub 与团队',
2603
+ 'settings.hub.enable':'启用 Hub 共享',
2604
+ 'settings.hub.enable.hint':'关闭时仅本地使用,不影响其他功能。',
2605
+ 'settings.hub.enable.label':'启用 Hub 共享',
2606
+ 'settings.hub.role':'角色',
2607
+ 'settings.hub.role.hub':'Hub(服务端)',
2608
+ 'settings.hub.role.client':'Client(连接到 Hub)',
2609
+ 'settings.hub.role.hint':'Hub = 本机做服务端;Client = 连接别人的服务端。',
2610
+ 'settings.hub.port':'Hub 端口',
2611
+ 'settings.hub.port.hint':'Hub 服务端口,默认 18800',
2612
+ 'settings.hub.teamName':'团队名称',
2613
+ 'settings.hub.teamName.hint':'你的团队显示名称',
2614
+ 'settings.hub.teamToken':'团队令牌',
2615
+ 'settings.hub.teamToken.hint':'自动生成的密钥,点击可复制。请将此令牌分享给团队成员。',
2616
+ 'settings.hub.tokenCopied':'团队令牌已复制!',
2617
+ 'settings.hub.hubSteps.title':'快速配置(3 步)',
2618
+ 'settings.hub.hubSteps.s1':'填写下方团队名称(或保持默认)',
2619
+ 'settings.hub.hubSteps.s2':'点击"保存设置",然后重启 OpenClaw 网关',
2620
+ 'settings.hub.hubSteps.s3':'将下方的 Hub 地址和团队令牌分享给团队成员',
2621
+ 'settings.hub.clientSteps.title':'快速配置(3 步)',
2622
+ 'settings.hub.clientSteps.s1':'向 Hub 管理员获取 Hub 地址和团队令牌',
2623
+ 'settings.hub.clientSteps.s2':'填入下方,点击"测试连接"验证连通性',
2624
+ 'settings.hub.clientSteps.s3':'点击"保存设置",然后重启 OpenClaw 网关即可连接',
2625
+ 'settings.hub.shareInfo.title':'请将以下信息分享给团队成员:',
2626
+ 'settings.hub.shareInfo.yourIP':'你的IP',
2627
+ 'settings.hub.shareInfo.clickCopy':'点击复制',
2628
+ 'settings.hub.restartAlert':'Hub 共享配置已保存!请重启 OpenClaw 网关使配置生效。\\n\\n执行命令:openclaw gateway stop && openclaw gateway',
2629
+ 'settings.hub.hubAddress':'Hub 地址',
2630
+ 'settings.hub.hubAddress.hint':'Hub 服务器地址,如 192.168.1.100:18800',
2631
+ 'settings.hub.teamTokenClient':'团队令牌',
2632
+ 'settings.hub.teamTokenClient.hint':'向 Hub 管理员获取此令牌以加入团队',
2633
+ 'settings.hub.userToken':'用户令牌',
2634
+ 'settings.hub.userToken.hint':'通常在加入团队后自动获取,无需手动填写。',
2635
+ 'settings.hub.testConnection':'测试连接',
2636
+ 'settings.hub.test.noAddr':'请先输入 Hub 地址',
2637
+ 'settings.hub.test.testing':'测试中...',
2638
+ 'settings.hub.test.ok':'连接成功',
2639
+ 'settings.hub.test.fail':'连接失败',
2640
+ 'settings.hub.connection':'连接状态',
2641
+ 'settings.hub.team':'团队与分组',
2642
+ 'settings.hub.adminPending':'管理员待审用户',
2643
+ 'sidebar.hub':'\u{1F310} 团队共享',
2644
+ 'sharing.sidebar.connected':'已连接',
2645
+ 'sharing.sidebar.disconnected':'已断开',
2646
+ 'sharing.sidebar.starting':'启动中...',
2647
+ 'sharing.sidebar.notConfigured':'未配置',
2648
+ 'sharing.sidebar.identity':'身份:',
2649
+ 'sharing.sidebar.admin':'管理员',
2650
+ 'sharing.sidebar.targetHub':'目标 Hub:',
2651
+ 'admin.title':'Hub 管理面板',
2652
+ 'admin.subtitle':'管理团队成员、分组和共享资源',
2653
+ 'admin.refresh':'\u21BB 刷新',
2654
+ 'admin.tab.users':'用户',
2655
+ 'admin.tab.groups':'分组',
2656
+ 'admin.tab.memories':'共享任务',
2657
+ 'admin.tab.skills':'共享技能',
2658
+ 'admin.stat.activeUsers':'活跃用户',
2659
+ 'admin.stat.pending':'待审核',
2660
+ 'admin.stat.groups':'分组',
2661
+ 'admin.stat.sharedTasks':'共享任务',
2662
+ 'admin.stat.sharedSkills':'共享技能',
2663
+ 'admin.stat.sharedMemories':'共享记忆',
2664
+ 'admin.pendingApproval':'待审批',
2665
+ 'admin.activeUsers':'活跃用户',
2666
+ 'admin.noActiveUsers':'暂无活跃用户。',
2667
+ 'admin.approve':'批准',
2668
+ 'admin.reject':'拒绝',
2669
+ 'admin.device':'设备:',
2670
+ 'admin.groups':'分组',
2671
+ 'admin.newGroup':'+ 新建分组',
2672
+ 'admin.groupName':'分组名称',
2673
+ 'admin.groupDesc':'描述(可选)',
2674
+ 'admin.create':'创建',
2675
+ 'admin.cancel':'取消',
2676
+ 'admin.delete':'删除',
2677
+ 'admin.members':'成员',
2678
+ 'admin.noGroups':'暂无分组。',
2679
+ 'admin.noMembers':'暂无成员。',
2680
+ 'admin.add':'添加',
2681
+ 'admin.remove':'移除',
2682
+ 'admin.sharedTasks':'共享任务',
2683
+ 'admin.noSharedTasks':'Hub 上暂无共享任务。',
2684
+ 'admin.owner':'归属:',
2685
+ 'admin.group':'分组:',
2686
+ 'admin.chunks':'记忆片段:',
2687
+ 'admin.updated':'更新于:',
2688
+ 'admin.sharedSkills':'共享技能',
2689
+ 'admin.noSharedSkills':'Hub 上暂无共享技能。',
2690
+ 'admin.sharedMemories':'共享记忆',
2691
+ 'admin.noSharedMemories':'Hub 上暂无共享记忆。',
2692
+ 'admin.tab.sharedMemories':'共享记忆',
2693
+ 'admin.version':'v',
2694
+ 'admin.quality':'质量:',
2695
+ 'admin.membersCount':'成员({n}):',
2696
+ 'admin.noMembersYet':'暂无成员。',
2697
+ 'admin.loadFailed':'加载管理数据失败:',
2698
+ 'admin.groupsFailed':'加载分组失败:',
2699
+ 'toast.userApproved':'用户已批准',
2700
+ 'toast.userRejected':'用户已拒绝',
2701
+ 'toast.approveFail':'批准失败',
2702
+ 'toast.rejectFail':'拒绝失败',
2703
+ 'toast.groupCreated':'分组已创建',
2704
+ 'toast.groupDeleted':'分组已删除',
2705
+ 'toast.memberAdded':'成员已添加',
2706
+ 'toast.memberRemoved':'成员已移除',
2707
+ 'toast.taskRemoved':'任务已移除',
2708
+ 'toast.skillRemoved':'技能已移除',
2709
+ 'toast.memoryRemoved':'记忆已移除',
2710
+ 'toast.createFail':'创建失败',
2711
+ 'toast.deleteFail':'删除失败',
2712
+ 'toast.addFail':'添加失败',
2713
+ 'toast.removeFail':'移除失败',
2714
+ 'toast.groupNameRequired':'请输入分组名称',
2715
+ 'confirm.rejectUser':'确定要拒绝此用户吗?',
2716
+ 'confirm.removeGroupMember':'确定要将此成员移出分组吗?',
2717
+ 'confirm.removeMember':'确定要移除此成员吗?',
2718
+ 'confirm.deleteGroup':'确定要删除分组「{name}」吗?成员将被移除。',
2719
+ 'confirm.deleteGroupShort':'确定要删除分组「{name}」吗?',
2720
+ 'confirm.removeTask':'确定要从 Hub 移除共享任务「{name}」吗?此操作不可撤销。',
2721
+ 'confirm.removeSkill':'确定要从 Hub 移除共享技能「{name}」吗?此操作不可撤销。',
2722
+ 'confirm.removeMemory':'确定要从 Hub 移除共享记忆「{name}」吗?此操作不可撤销。',
2723
+ 'sharing.disabled':'共享已禁用',
2724
+ 'sharing.disabled.hint':'在插件配置中启用共享以连接 Hub。',
2725
+ 'sharing.hubAdmin':'Hub 管理员',
2726
+ 'sharing.client':'客户端',
2727
+ 'sharing.hubMode':'Hub 模式',
2728
+ 'sharing.hubMode.status':'状态:未连接到自身',
2729
+ 'sharing.hubMode.hint':'配置 sharing.client 的 hubAddress 和 userToken 指向此 Hub 以启用管理界面。',
2730
+ 'sharing.clientConfigured':'客户端已配置',
2731
+ 'sharing.clientDisconnected':'状态:已断开',
2732
+ 'sharing.clientDisconnected.hint':'查看器将继续显示本地数据;Hub 操作可能在连接恢复前失败。',
2733
+ 'sharing.clientNotConfigured':'客户端未配置',
2734
+ 'sharing.clientNotConfigured.hint':'设置 sharing.client 中的 hubAddress 和 userToken 以启用团队功能。',
2735
+ 'sharing.settingsDisabled':'共享已禁用。',
2736
+ 'sharing.settingsDisabled.hint':'在配置中启用共享以使用 Hub 记忆和技能协作。',
2737
+ 'sharing.noTeam':'无团队连接。',
2738
+ 'sharing.adminUnavailable':'管理工具不可用。',
2739
+ 'sharing.adminEnabled':'管理控制已启用',
2740
+ 'sharing.adminPendingHint':'待审用户将显示在下方。',
2741
+ 'sharing.notAdmin':'当前用户不是管理员。',
2742
+ 'sharing.pendingLoadFail':'加载待审用户失败:',
2743
+ 'sharing.noPending':'暂无待审用户。',
2744
+ 'sharing.manageGroups':'管理分组',
2745
+ 'sharing.openAdmin':'打开管理面板',
2746
+ 'sharing.saveUsername':'保存',
2747
+ 'sharing.username.invalid':'用户名需 2-32 个字符',
2748
+ 'sharing.username.taken':'用户名已被占用',
2749
+ 'sharing.username.updated':'用户名已更新',
2750
+ 'sharing.username.error':'更新用户名失败',
2751
+ 'sharing.hubNotConfigured':'Hub 正在运行,但客户端连接未配置。',
2752
+ 'sharing.hubNotConfigured.hint':'添加 sharing.client.hubAddress 和 sharing.client.userToken 指向此 Hub 以启用管理界面。',
2753
+ 'sharing.notConnected':'未连接到 Hub。',
2754
+ 'sharing.role':'角色:',
2755
+ 'sharing.mode':'身份:',
2756
+ 'sharing.role.hub':'服务端(Hub 主机)',
2757
+ 'sharing.role.client':'成员(连接到 Hub)',
2758
+ 'sharing.clientConfiguredLabel':'客户端已配置:',
2759
+ 'sharing.configuredHub':'配置的 Hub:',
2760
+ 'sharing.connected':'已连接:',
2761
+ 'sharing.yes':'是',
2762
+ 'sharing.no':'否',
2763
+ 'sharing.user':'用户:',
2764
+ 'sharing.team':'团队:',
2765
+ 'sharing.groups':'分组:',
2766
+ 'sharing.loading':'加载中...',
2767
+ 'sharing.loadingGroups':'正在加载分组...',
2768
+ 'sharing.noGroupsYet':'暂无分组。',
2769
+ 'search.localResults':'本地结果',
2770
+ 'search.hubResults':'Hub 结果',
2771
+ 'search.noLocal':'无本地结果。',
2772
+ 'search.noHub':'无 Hub 结果。',
2773
+ 'search.viewDetail':'查看详情',
2774
+ 'search.sharedMemory':'共享记忆',
2775
+ 'search.loadFailed':'加载共享记忆失败',
2776
+ 'share.alreadyShared':'已共享',
2777
+ 'share.shareBtn':'共享',
2778
+ 'share.updateBtn':'更新共享',
2779
+ 'share.unshareBtn':'取消共享',
2780
+ 'toast.taskShared':'任务已共享',
2781
+ 'toast.taskShareFail':'任务共享失败',
2782
+ 'toast.taskUnshared':'任务已取消共享',
2783
+ 'toast.taskUnshareFail':'取消共享失败',
2784
+ 'toast.memoryShared':'记忆已共享',
2785
+ 'toast.memoryShareFail':'记忆共享失败',
2786
+ 'toast.memoryUnshared':'记忆已取消共享',
2787
+ 'toast.memoryUnshareFail':'记忆取消共享失败',
2788
+ 'toast.skillShared':'技能已共享',
2789
+ 'toast.skillShareFail':'技能共享失败',
2790
+ 'toast.skillUnshared':'技能已取消共享',
2791
+ 'toast.skillUnshareFail':'技能取消共享失败',
2792
+ 'share.memoryVisibilityPrompt':'共享可见性(public 或 group):',
2793
+ 'share.memoryUnshareConfirm':'取消共享此记忆?',
2794
+ 'share.group':'团队',
2795
+ 'share.public':'公开',
2796
+ 'toast.skillPulled':'技能已拉取到本地',
2797
+ 'toast.skillPullFail':'技能拉取失败',
1997
2798
  'task.edit':'编辑',
1998
2799
  'task.delete':'删除',
1999
2800
  'task.save':'保存',
@@ -2014,10 +2815,11 @@ const I18N={
2014
2815
  'skill.save.error':'保存技能失败:',
2015
2816
  'update.available':'发现新版本',
2016
2817
  'update.run':'执行命令',
2017
- 'update.btn':'立即更新',
2018
- 'update.installing':'正在安装更新...',
2019
- 'update.success':'更新成功!正在重启...',
2818
+ 'update.btn':'更新',
2819
+ 'update.installing':'安装中...',
2820
+ 'update.success':'更新完成',
2020
2821
  'update.failed':'更新失败',
2822
+ 'update.restarting':'正在重启服务...',
2021
2823
  'update.dismiss':'关闭'
2022
2824
  }
2023
2825
  };
@@ -2120,11 +2922,86 @@ async function doReset(){
2120
2922
  else{err.textContent=d.error||t('reset.err.fail')}
2121
2923
  }
2122
2924
 
2925
+ var _sharingRole='client';
2926
+ function _genToken(len){
2927
+ var a=new Uint8Array(len||18);crypto.getRandomValues(a);
2928
+ return btoa(String.fromCharCode.apply(null,a)).replace(/\\+/g,'-').replace(/\\//g,'_').replace(/=+$/,'');
2929
+ }
2930
+ function onSharingToggle(){
2931
+ var on=document.getElementById('cfgSharingEnabled').checked;
2932
+ document.getElementById('sharingConfigPanel').style.display=on?'block':'none';
2933
+ if(on) selectSharingRole(_sharingRole);
2934
+ }
2935
+ function selectSharingRole(role){
2936
+ _sharingRole=role;
2937
+ document.getElementById('btnRoleHub').className='btn btn-sm'+(role==='hub'?' btn-primary':'');
2938
+ document.getElementById('btnRoleClient').className='btn btn-sm'+(role==='client'?' btn-primary':'');
2939
+ document.getElementById('hubModeConfig').style.display=role==='hub'?'block':'none';
2940
+ document.getElementById('clientModeConfig').style.display=role==='client'?'block':'none';
2941
+ if(role==='hub'){
2942
+ var tk=document.getElementById('cfgHubTeamToken');
2943
+ if(!tk.value.trim()) tk.value=_genToken(18);
2944
+ var tn=document.getElementById('cfgHubTeamName');
2945
+ if(!tn.value.trim()) tn.value='My Team';
2946
+ }
2947
+ }
2948
+ var _cachedLocalIP='';
2949
+ function updateHubShareInfo(){
2950
+ var panel=document.getElementById('hubShareInfo');
2951
+ var content=document.getElementById('hubShareInfoContent');
2952
+ if(!panel||!content) return;
2953
+ var token=document.getElementById('cfgHubTeamToken').value.trim();
2954
+ var port=document.getElementById('cfgHubPort').value.trim()||'18800';
2955
+ if(!token||_sharingRole!=='hub'){panel.style.display='none';return;}
2956
+ panel.style.display='block';
2957
+ var renderShare=function(ip){
2958
+ var addr=ip?(ip+':'+esc(port)):('&lt;'+t('settings.hub.shareInfo.yourIP')+'&gt;:'+esc(port));
2959
+ var tip=t('settings.hub.shareInfo.clickCopy');
2960
+ content.innerHTML=
2961
+ '<span class="hic-label">Hub '+t('settings.hub.hubAddress')+'</span><span class="hic-value mono" onclick="navigator.clipboard.writeText(this.textContent)" title="'+tip+'">'+addr+'</span>'+
2962
+ '<span class="hic-label">Team Token</span><span class="hic-value mono" onclick="navigator.clipboard.writeText(this.textContent)" title="'+tip+'">'+esc(token)+'</span>';
2963
+ };
2964
+ if(_cachedLocalIP){renderShare(_cachedLocalIP);return;}
2965
+ renderShare('');
2966
+ fetch('/api/local-ips').then(function(r){return r.json()}).then(function(d){
2967
+ if(d.ips&&d.ips.length>0){_cachedLocalIP=d.ips[0];renderShare(_cachedLocalIP);}
2968
+ }).catch(function(){});
2969
+ }
2970
+ async function testHubConnection(){
2971
+ var btn=document.getElementById('btnTestHubConn');
2972
+ var result=document.getElementById('hubConnTestResult');
2973
+ var addr=document.getElementById('cfgClientHubAddress').value.trim();
2974
+ if(!addr){result.innerHTML='<span style="color:var(--rose)">\u274C '+t('settings.hub.test.noAddr')+'</span>';return;}
2975
+ btn.disabled=true;result.innerHTML=t('settings.hub.test.testing');
2976
+ try{
2977
+ var url=addr.match(/^https?:\\/\\//)?addr:'http://'+addr;
2978
+ url=url.replace(/\\/+$/,'');
2979
+ var r=await fetch('/api/sharing/test-hub',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({hubUrl:url})});
2980
+ var d=await r.json();
2981
+ if(d.ok){
2982
+ result.innerHTML='<span style="color:var(--green)">\u2705 '+t('settings.hub.test.ok')+(d.teamName?' — '+esc(d.teamName):'')+'</span>';
2983
+ }else{
2984
+ result.innerHTML='<span style="color:var(--rose)">\u274C '+(d.error||t('settings.hub.test.fail'))+'</span>';
2985
+ }
2986
+ }catch(e){
2987
+ result.innerHTML='<span style="color:var(--rose)">\u274C '+esc(String(e))+'</span>';
2988
+ }finally{btn.disabled=false;}
2989
+ }
2990
+
2123
2991
  function enterApp(){
2124
2992
  document.getElementById('app').style.display='flex';
2125
2993
  loadAll();
2126
2994
  }
2127
2995
 
2996
+ function switchSettingsTab(tab,btn){
2997
+ document.querySelectorAll('.settings-tab-btn').forEach(function(b){b.classList.remove('active')});
2998
+ if(btn){btn.classList.add('active')}
2999
+ else{var b=document.querySelector('.settings-tab-btn[data-tab="'+tab+'"]');if(b)b.classList.add('active')}
3000
+ document.querySelectorAll('.settings-card[data-stab]').forEach(function(c){
3001
+ if(c.dataset.stab===tab){c.classList.add('stab-active')}else{c.classList.remove('stab-active')}
3002
+ });
3003
+ }
3004
+
2128
3005
  function switchView(view){
2129
3006
  document.querySelectorAll('.nav-tabs .tab').forEach(t=>t.classList.toggle('active',t.dataset.view===view));
2130
3007
  const feedWrap=document.getElementById('feedWrap');
@@ -2134,6 +3011,7 @@ function switchView(view){
2134
3011
  const logsView=document.getElementById('logsView');
2135
3012
  const settingsView=document.getElementById('settingsView');
2136
3013
  const migrateView=document.getElementById('migrateView');
3014
+ const adminView=document.getElementById('adminView');
2137
3015
  const sidebar=document.getElementById('sidebar');
2138
3016
  feedWrap.classList.add('hide');
2139
3017
  analyticsView.classList.remove('show');
@@ -2142,19 +3020,17 @@ function switchView(view){
2142
3020
  logsView.classList.remove('show');
2143
3021
  settingsView.classList.remove('show');
2144
3022
  migrateView.classList.remove('show');
2145
- const sessionSection=document.getElementById('sidebarSessionSection');
3023
+ if(adminView) adminView.classList.remove('show');
3024
+ var sessionSection=document.getElementById('sidebarSessionSection');
2146
3025
  if(view==='memories'){
2147
3026
  feedWrap.classList.remove('hide');
2148
- sessionSection.style.visibility='';
2149
- sessionSection.style.pointerEvents='';
3027
+ if(sessionSection){sessionSection.style.visibility='';sessionSection.style.pointerEvents='';}
2150
3028
  } else if(view==='tasks'||view==='skills'){
2151
- sessionSection.style.visibility='hidden';
2152
- sessionSection.style.pointerEvents='none';
3029
+ if(sessionSection){sessionSection.style.visibility='hidden';sessionSection.style.pointerEvents='none';}
2153
3030
  if(view==='tasks'){tasksView.classList.add('show');loadTasks();}
2154
3031
  else{skillsView.classList.add('show');loadSkills();}
2155
3032
  } else {
2156
- sessionSection.style.visibility='hidden';
2157
- sessionSection.style.pointerEvents='none';
3033
+ if(sessionSection){sessionSection.style.visibility='hidden';sessionSection.style.pointerEvents='none';}
2158
3034
  if(view==='analytics'){
2159
3035
  analyticsView.classList.add('show');
2160
3036
  loadMetrics();
@@ -2168,8 +3044,945 @@ function switchView(view){
2168
3044
  } else if(view==='import'){
2169
3045
  migrateView.classList.add('show');
2170
3046
  if(!window._migrateRunning) migrateScan(false);
3047
+ } else if(view==='admin'){
3048
+ if(adminView){adminView.classList.add('show');loadAdminData();}
3049
+ }
3050
+ }
3051
+ }
3052
+
3053
+ function onMemoryScopeChange(){
3054
+ memorySearchScope=document.getElementById('memorySearchScope')?.value||'local';
3055
+ if(document.getElementById('searchInput').value.trim()) doSearch(document.getElementById('searchInput').value);
3056
+ else if(memorySearchScope!=='local') { document.getElementById('sharingSearchMeta').textContent=''; loadHubMemories(); }
3057
+ else { document.getElementById('sharingSearchMeta').textContent=''; loadMemories(); }
3058
+ }
3059
+
3060
+ function onSkillScopeChange(){
3061
+ skillSearchScope=document.getElementById('skillSearchScope')?.value||'local';
3062
+ loadSkills();
3063
+ }
3064
+
3065
+ function onTaskScopeChange(){
3066
+ taskSearchScope=document.getElementById('taskSearchScope')?.value||'local';
3067
+ tasksPage=0;
3068
+ loadTasks();
3069
+ }
3070
+
3071
+ async function loadSharingStatus(forcePending){
3072
+ try{
3073
+ const r=await fetch('/api/sharing/status');
3074
+ const d=await r.json();
3075
+ sharingStatusCache=d;
3076
+ renderSharingSidebar(d);
3077
+ renderSharingSettings(d);
3078
+ if(forcePending && d && d.admin && d.admin.canManageUsers) loadSharingPendingUsers();
3079
+ }catch(e){
3080
+ renderSharingSidebar(null);
3081
+ renderSharingSettings(null);
3082
+ }
3083
+ }
3084
+
3085
+ function renderSharingSidebar(data){
3086
+ var section=document.getElementById('sidebarSharingSection');
3087
+ var statusEl=document.getElementById('sharingSidebarStatus');
3088
+ var hintEl=document.getElementById('sharingSidebarHint');
3089
+ var badgeEl=document.getElementById('sharingSidebarConnBadge');
3090
+ if(!statusEl||!hintEl) return;
3091
+ if(!data||!data.enabled){
3092
+ if(section) section.style.display='none';
3093
+ return;
3094
+ }
3095
+ if(section) section.style.display='block';
3096
+ var conn=data.connection||{};
3097
+ function setBadge(color,text,glow){
3098
+ if(!badgeEl)return;
3099
+ badgeEl.innerHTML='<span style="display:inline-flex;align-items:center;gap:4px;font-size:10px;font-weight:600;padding:2px 8px;border-radius:9999px;background:'+color+'15;color:'+color+'"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:'+color+(glow?';box-shadow:0 0 4px '+color:'')+'"></span>'+esc(text)+'</span>';
3100
+ }
3101
+ if(conn.connected&&conn.user){
3102
+ var groups=(conn.user.groups||[]).map(function(g){return g.name;}).join(', ')||'';
3103
+ var isAdmin=conn.user.role==='admin';
3104
+ setBadge('#34d399',t('sharing.sidebar.connected'),true);
3105
+ var html='<div class="info-grid">';
3106
+ html+='<span class="label">'+t('sharing.sidebar.identity')+'</span><span class="value">'+esc(conn.user.username)+(isAdmin?' <span class="role-badge admin">'+t('sharing.sidebar.admin')+'</span>':'')+'</span>';
3107
+ html+='<span class="label">'+t('sharing.team')+'</span><span class="value">'+esc(conn.teamName||'-')+'</span>';
3108
+ if(groups) html+='<span class="label">'+t('sharing.groups')+'</span><span class="value">'+esc(groups)+'</span>';
3109
+ html+='</div>';
3110
+ statusEl.innerHTML=html;
3111
+ hintEl.innerHTML='';
3112
+ var adminTab=document.querySelector('.tab[data-view="admin"]');
3113
+ if(adminTab) adminTab.style.display=isAdmin?'':'none';
3114
+ }else if(data.role==='hub'){
3115
+ setBadge('#fbbf24',t('sharing.sidebar.starting'),false);
3116
+ statusEl.innerHTML='';
3117
+ hintEl.textContent=t('sharing.hubMode.hint');
3118
+ var adminTab=document.querySelector('.tab[data-view="admin"]');
3119
+ if(adminTab) adminTab.style.display=(data.admin&&data.admin.canManageUsers)?'':'none';
3120
+ }else if(data.clientConfigured){
3121
+ setBadge('#ef4444',t('sharing.sidebar.disconnected'),false);
3122
+ statusEl.innerHTML='<div style="font-size:11px;color:var(--text-muted)">'+t('sharing.sidebar.targetHub')+' '+esc(data.hubUrl||'')+'</div>';
3123
+ hintEl.textContent=t('sharing.clientDisconnected.hint');
3124
+ }else{
3125
+ setBadge('#888',t('sharing.sidebar.notConfigured'),false);
3126
+ statusEl.innerHTML='';
3127
+ hintEl.textContent=t('sharing.clientNotConfigured.hint');
3128
+ }
3129
+ }
3130
+
3131
+ function renderSharingSettings(data){
3132
+ var statusEl=document.getElementById('sharingStatusPanel');
3133
+ var teamEl=document.getElementById('sharingTeamPanel');
3134
+ var adminEl=document.getElementById('sharingAdminPanel');
3135
+ if(!statusEl||!teamEl||!adminEl) return;
3136
+ if(!data||!data.enabled){
3137
+ statusEl.innerHTML='';teamEl.innerHTML='';adminEl.innerHTML='';
3138
+ return;
3139
+ }
3140
+ var conn=data.connection||{};
3141
+ var user=conn.user||{};
3142
+ var groups=(user.groups||[]).map(function(g){return g.name;}).join(', ')||'-';
3143
+ var connBadge=conn.connected
3144
+ ?'<span class="hic-badge connected"><span class="hic-dot green"></span>'+t('sharing.sidebar.connected')+'</span>'
3145
+ :'<span class="hic-badge disconnected"><span class="hic-dot red"></span>'+t('sharing.sidebar.disconnected')+'</span>';
3146
+ var roleLabel=data.role==='hub'?t('sharing.role.hub'):t('sharing.role.client');
3147
+ var sh='<div class="hub-info-card hic-status"><div class="hic-title"><span class="hic-icon">\u{1F517}</span>'+t('settings.hub.connection')+' '+connBadge+'</div><div class="hic-grid">';
3148
+ sh+='<span class="hic-label">'+t('sharing.mode')+'</span><span class="hic-value">'+roleLabel+'</span>';
3149
+ if(conn.connected&&user.username){
3150
+ sh+='<span class="hic-label">'+t('sharing.user')+'</span><span class="hic-value" style="display:flex;align-items:center;gap:6px">'+
3151
+ '<input type="text" id="hubUsernameInput" value="'+esc(user.username)+'" style="border:1px solid var(--border);border-radius:6px;padding:3px 8px;font-size:12px;background:var(--bg);color:var(--text);width:120px;font-family:inherit" />'+
3152
+ (user.role==='admin'?' <span class="hic-badge admin">admin</span>':'')+
3153
+ '<button class="btn btn-sm" onclick="updateHubUsername()" style="padding:2px 10px;font-size:11px">'+t('sharing.saveUsername')+'</button>'+
3154
+ '</span>';
3155
+ }
3156
+ sh+='</div></div>';
3157
+ statusEl.innerHTML=sh;
3158
+
3159
+ var th='';
3160
+ if(conn.connected&&conn.user){
3161
+ th='<div class="hub-info-card hic-team"><div class="hic-title"><span class="hic-icon">\u{1F465}</span>'+t('settings.hub.team')+'</div><div class="hic-grid">';
3162
+ th+='<span class="hic-label">'+t('sharing.team')+'</span><span class="hic-value">'+esc(conn.teamName||'-')+'</span>';
3163
+ th+='<span class="hic-label">'+t('sharing.groups')+'</span><span class="hic-value">'+esc(groups)+'</span>';
3164
+ th+='</div>';
3165
+ if(user.role==='admin'){
3166
+ th+='<div class="hic-actions">'+
3167
+ '<button class="btn btn-sm" onclick="loadGroupManager()">'+t('sharing.manageGroups')+'</button>'+
3168
+ '<button class="btn btn-sm btn-primary" onclick="switchView(\\\'admin\\\')">'+t('sharing.openAdmin')+'</button></div>';
2171
3169
  }
3170
+ th+='<div id="groupManagerPanel" style="margin-top:12px;display:none"></div></div>';
3171
+ }else if(data.role==='hub'&&!data.clientConfigured){
3172
+ th='<div class="hub-info-card hic-team"><div class="hic-title"><span class="hic-icon">\u{1F465}</span>'+t('settings.hub.team')+'</div>';
3173
+ th+='<div class="hic-empty">'+t('sharing.hubNotConfigured')+'</div></div>';
3174
+ }
3175
+ teamEl.innerHTML=th;
3176
+
3177
+ var isAdmin=(data.admin&&data.admin.canManageUsers)||(conn.connected&&user.role==='admin')||(data.role==='hub');
3178
+ if(isAdmin){
3179
+ adminEl.innerHTML='<div class="hub-info-card hic-pending"><div class="hic-title"><span class="hic-icon">\u{1F6E1}</span>'+t('settings.hub.adminPending')+' <span class="hic-badge pending"><span class="hic-dot amber"></span>'+t('sharing.adminEnabled')+'</span></div><div class="hic-empty">'+t('sharing.adminPendingHint')+'</div></div>';
3180
+ }else{
3181
+ adminEl.innerHTML='';
2172
3182
  }
3183
+ var adminTab=document.querySelector('.tab[data-view="admin"]');
3184
+ if(adminTab) adminTab.style.display=isAdmin?'':'none';
3185
+ }
3186
+
3187
+ async function updateHubUsername(){
3188
+ var input=document.getElementById('hubUsernameInput');
3189
+ if(!input) return;
3190
+ var newName=input.value.trim();
3191
+ if(!newName||newName.length<2||newName.length>32){
3192
+ toast(t('sharing.username.invalid'),'error');
3193
+ return;
3194
+ }
3195
+ try{
3196
+ var r=await fetch('/api/sharing/update-username',{
3197
+ method:'POST',
3198
+ headers:{'Content-Type':'application/json'},
3199
+ body:JSON.stringify({username:newName})
3200
+ });
3201
+ var d=await r.json();
3202
+ if(d.error==='username_taken'){
3203
+ toast(t('sharing.username.taken'),'error');
3204
+ return;
3205
+ }
3206
+ if(d.error){
3207
+ toast(d.error,'error');
3208
+ return;
3209
+ }
3210
+ toast(t('sharing.username.updated'),'success');
3211
+ loadSharingStatus(false);
3212
+ }catch(e){
3213
+ toast(t('sharing.username.error'),'error');
3214
+ }
3215
+ }
3216
+
3217
+ async function loadSharingPendingUsers(){
3218
+ var el=document.getElementById('sharingAdminPanel');
3219
+ if(!el) return;
3220
+ el.innerHTML=t('sharing.loading');
3221
+ try{
3222
+ const r=await fetch('/api/sharing/pending-users');
3223
+ const d=await r.json();
3224
+ const users=Array.isArray(d.users)?d.users:[];
3225
+ renderSharingPendingUsers(users, d.error, sharingStatusCache&&sharingStatusCache.admin?sharingStatusCache.admin.rejectSupported:false);
3226
+ }catch(e){
3227
+ el.innerHTML='<div class="line">'+t('sharing.pendingLoadFail')+esc(String(e))+'</div>';
3228
+ }
3229
+ }
3230
+
3231
+ function renderSharingPendingUsers(users, error, rejectSupported){
3232
+ var el=document.getElementById('sharingAdminPanel');
3233
+ if(!el) return;
3234
+ var wrap='<div class="hub-info-card hic-pending"><div class="hic-title"><span class="hic-icon">\u{1F6E1}</span>'+t('settings.hub.adminPending')+' <span class="hic-badge pending"><span class="hic-dot amber"></span>'+(users?users.length:0)+'</span></div>';
3235
+ if(error){
3236
+ el.innerHTML=wrap+'<div class="hic-empty">'+esc(error)+'</div></div>';
3237
+ return;
3238
+ }
3239
+ if(!users||users.length===0){
3240
+ el.innerHTML=wrap+'<div class="hic-empty">'+t('sharing.noPending')+'</div></div>';
3241
+ return;
3242
+ }
3243
+ el.innerHTML=wrap+users.map(function(user){
3244
+ return '<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 0;border-top:1px solid var(--border)">'+
3245
+ '<div><div style="font-size:13px;font-weight:600;color:var(--text)">'+esc(user.username||user.id||'')+'</div>'+
3246
+ '<div style="font-size:11px;color:var(--text-muted)">'+t('admin.device')+esc(user.deviceName||'unknown')+'</div></div>'+
3247
+ '<div class="hic-actions" style="margin:0">'+
3248
+ '<button class="btn btn-sm btn-primary" onclick="approveSharingUser(&quot;'+escAttr(user.id)+'&quot;,&quot;'+escAttr(user.username||'')+'&quot;)">'+t('admin.approve')+'</button>'+
3249
+ (rejectSupported?'<button class="btn btn-sm btn-ghost" style="color:var(--rose)" onclick="rejectSharingUser(&quot;'+escAttr(user.id)+'&quot;,&quot;'+escAttr(user.username||'')+'&quot;)">'+t('admin.reject')+'</button>':'')+
3250
+ '</div></div>';
3251
+ }).join('')+'</div>';
3252
+ }
3253
+
3254
+ async function approveSharingUser(userId,username){
3255
+ try{
3256
+ const r=await fetch('/api/sharing/approve-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,username:username})});
3257
+ const d=await r.json();
3258
+ if(d.ok){toast(t('toast.userApproved'),'success');loadSharingPendingUsers();loadSharingStatus(true);} else {toast(d.error||t('toast.approveFail'),'error');}
3259
+ }catch(e){toast(t('toast.approveFail')+': '+e.message,'error');}
3260
+ }
3261
+
3262
+ async function rejectSharingUser(userId,username){
3263
+ try{
3264
+ const r=await fetch('/api/sharing/reject-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,username:username})});
3265
+ const d=await r.json();
3266
+ if(d.ok){toast(t('toast.userRejected'),'success');loadSharingPendingUsers();} else {toast(d.error||t('toast.rejectFail'),'error');}
3267
+ }catch(e){toast(t('toast.rejectFail')+': '+e.message,'error');}
3268
+ }
3269
+
3270
+ /* ─── Group Manager ─── */
3271
+ var groupManagerUsers=[];
3272
+ async function loadGroupManager(){
3273
+ var panel=document.getElementById('groupManagerPanel');
3274
+ if(!panel) return;
3275
+ panel.style.display='block';
3276
+ panel.innerHTML=t('sharing.loadingGroups');
3277
+ try{
3278
+ var [gr,ur]=await Promise.all([
3279
+ fetch('/api/sharing/groups').then(function(r){return r.json();}),
3280
+ fetch('/api/sharing/users').then(function(r){return r.json();})
3281
+ ]);
3282
+ var groups=Array.isArray(gr.groups)?gr.groups:[];
3283
+ groupManagerUsers=Array.isArray(ur.users)?ur.users:[];
3284
+ renderGroupManager(panel,groups);
3285
+ }catch(e){panel.innerHTML=t('admin.groupsFailed')+esc(String(e));}
3286
+ }
3287
+ function renderGroupManager(panel,groups){
3288
+ var html='<div style="margin-bottom:8px;display:flex;gap:8px;align-items:center;font-size:12px">'+
3289
+ '<strong>'+t('admin.groups')+' ('+groups.length+')</strong>'+
3290
+ '<button class="btn btn-sm" onclick="showCreateGroupForm()" style="font-size:11px">'+t('admin.newGroup')+'</button>'+
3291
+ '</div>';
3292
+ html+='<div id="createGroupForm" style="display:none;margin-bottom:10px;padding:10px;background:var(--bg);border:1px solid var(--border);border-radius:8px;font-size:12px">'+
3293
+ '<input id="newGroupName" type="text" placeholder="'+t('admin.groupName')+'" style="width:60%;padding:4px 8px;border:1px solid var(--border);border-radius:6px;font-size:12px;margin-right:6px;background:var(--bg);color:var(--text)">'+
3294
+ '<input id="newGroupDesc" type="text" placeholder="'+t('admin.groupDesc')+'" style="width:60%;padding:4px 8px;border:1px solid var(--border);border-radius:6px;font-size:12px;margin-right:6px;margin-top:6px;background:var(--bg);color:var(--text)">'+
3295
+ '<div style="margin-top:6px"><button class="btn btn-sm btn-primary" onclick="createGroup()" style="font-size:11px">'+t('admin.create')+'</button> '+
3296
+ '<button class="btn btn-sm btn-ghost" onclick="hideCreateGroupForm()" style="font-size:11px">'+t('admin.cancel')+'</button></div>'+
3297
+ '</div>';
3298
+ if(groups.length===0){
3299
+ html+='<div style="font-size:12px;color:var(--text-muted);padding:6px 0">'+t('sharing.noGroupsYet')+'</div>';
3300
+ }else{
3301
+ html+='<div style="display:flex;flex-direction:column;gap:6px">';
3302
+ for(var i=0;i<groups.length;i++){
3303
+ var g=groups[i];
3304
+ html+='<div style="padding:8px 12px;background:var(--bg);border:1px solid var(--border);border-radius:8px;font-size:12px">'+
3305
+ '<div style="display:flex;justify-content:space-between;align-items:center">'+
3306
+ '<div><strong style="font-size:12px">'+esc(g.name)+'</strong>'+(g.description?' — <span style="color:var(--text-sec);font-size:11px">'+esc(g.description)+'</span>':'')+'</div>'+
3307
+ '<div style="display:flex;gap:6px">'+
3308
+ '<button class="btn btn-sm" onclick="toggleGroupMembers(&quot;'+escAttr(g.id)+'&quot;)" style="font-size:11px;padding:2px 8px">'+t('admin.members')+'</button>'+
3309
+ '<button class="btn btn-sm btn-ghost" onclick="deleteGroup(&quot;'+escAttr(g.id)+'&quot;,&quot;'+escAttr(g.name)+'&quot;)" style="color:#ef4444;font-size:11px;padding:2px 8px">'+t('admin.delete')+'</button>'+
3310
+ '</div>'+
3311
+ '</div>'+
3312
+ '<div id="groupMembers_'+escAttr(g.id)+'" style="display:none;margin-top:6px;font-size:12px"></div>'+
3313
+ '</div>';
3314
+ }
3315
+ html+='</div>';
3316
+ }
3317
+ panel.innerHTML=html;
3318
+ }
3319
+ function showCreateGroupForm(){var f=document.getElementById('createGroupForm');if(f)f.style.display='block';}
3320
+ function hideCreateGroupForm(){var f=document.getElementById('createGroupForm');if(f)f.style.display='none';}
3321
+ async function createGroup(){
3322
+ var name=(document.getElementById('newGroupName')).value.trim();
3323
+ var desc=(document.getElementById('newGroupDesc')).value.trim();
3324
+ if(!name){toast(t('toast.groupNameRequired'),'error');return;}
3325
+ try{
3326
+ var r=await fetch('/api/sharing/groups',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:name,description:desc})});
3327
+ var d=await r.json();
3328
+ if(d.ok){toast(t('toast.groupCreated'),'success');hideCreateGroupForm();loadGroupManager();}else{toast(d.error||t('toast.createFail'),'error');}
3329
+ }catch(e){toast(t('toast.createFail')+': '+e.message,'error');}
3330
+ }
3331
+ async function deleteGroup(groupId,groupName){
3332
+ if(!confirm(t('confirm.deleteGroup').replace('{name}',groupName))) return;
3333
+ try{
3334
+ var r=await fetch('/api/sharing/groups/'+encodeURIComponent(groupId),{method:'DELETE'});
3335
+ var d=await r.json();
3336
+ if(d.ok){toast(t('toast.groupDeleted'),'success');loadGroupManager();}else{toast(d.error||t('toast.deleteFail'),'error');}
3337
+ }catch(e){toast(t('toast.deleteFail')+': '+e.message,'error');}
3338
+ }
3339
+ async function toggleGroupMembers(groupId){
3340
+ var el=document.getElementById('groupMembers_'+groupId);
3341
+ if(!el) return;
3342
+ if(el.style.display!=='none'){el.style.display='none';return;}
3343
+ el.style.display='block';
3344
+ el.innerHTML=t('sharing.loading');
3345
+ try{
3346
+ var r=await fetch('/api/sharing/groups/'+encodeURIComponent(groupId)+'/members');
3347
+ var d=await r.json();
3348
+ var members=Array.isArray(d.members)?d.members:[];
3349
+ renderGroupMembers(el,groupId,members);
3350
+ }catch(e){el.innerHTML=t('admin.groupsFailed')+esc(String(e));}
3351
+ }
3352
+ function renderGroupMembers(el,groupId,members){
3353
+ var html='<div style="font-size:12px;margin-bottom:6px;color:var(--text-sec)">'+t('admin.membersCount').replace('{n}',members.length)+'</div>';
3354
+ if(members.length>0){
3355
+ html+='<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px">';
3356
+ for(var i=0;i<members.length;i++){
3357
+ var m=members[i];
3358
+ html+='<span style="display:inline-flex;align-items:center;gap:4px;padding:3px 10px;background:var(--bg-card);border:1px solid var(--border);border-radius:12px;font-size:12px">'+
3359
+ esc(m.username||m.userId)+
3360
+ ' <button style="background:none;border:none;color:#ef4444;cursor:pointer;font-size:11px;padding:0 2px" onclick="removeGroupMember(&quot;'+escAttr(groupId)+'&quot;,&quot;'+escAttr(m.userId)+'&quot;)">&times;</button>'+
3361
+ '</span>';
3362
+ }
3363
+ html+='</div>';
3364
+ }else{
3365
+ html+='<div style="font-size:12px;color:var(--text-muted);margin-bottom:8px">'+t('admin.noMembersYet')+'</div>';
3366
+ }
3367
+ var memberIds=new Set(members.map(function(m){return m.userId;}));
3368
+ var available=groupManagerUsers.filter(function(u){return !memberIds.has(u.id);});
3369
+ if(available.length>0){
3370
+ html+='<div style="display:flex;gap:6px;align-items:center">'+
3371
+ '<select id="addMemberSelect_'+escAttr(groupId)+'" style="padding:4px 8px;border:1px solid var(--border);border-radius:6px;font-size:12px">';
3372
+ for(var j=0;j<available.length;j++){
3373
+ html+='<option value="'+escAttr(available[j].id)+'">'+esc(available[j].username)+'</option>';
3374
+ }
3375
+ html+='</select>'+
3376
+ '<button class="btn btn-sm" onclick="addGroupMember(&quot;'+escAttr(groupId)+'&quot;)">'+t('admin.add')+'</button>'+
3377
+ '</div>';
3378
+ }
3379
+ el.innerHTML=html;
3380
+ }
3381
+ async function addGroupMember(groupId){
3382
+ var sel=document.getElementById('addMemberSelect_'+groupId);
3383
+ if(!sel) return;
3384
+ var userId=sel.value;
3385
+ if(!userId) return;
3386
+ try{
3387
+ var r=await fetch('/api/sharing/groups/'+encodeURIComponent(groupId)+'/members',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId})});
3388
+ var d=await r.json();
3389
+ if(d.ok){toast(t('toast.memberAdded'),'success');reloadGroupMembers(groupId);}else{toast(d.error||t('toast.addFail'),'error');}
3390
+ }catch(e){toast(t('toast.addFail')+': '+e.message,'error');}
3391
+ }
3392
+ async function removeGroupMember(groupId,userId){
3393
+ if(!confirm(t('confirm.removeGroupMember'))) return;
3394
+ try{
3395
+ var r=await fetch('/api/sharing/groups/'+encodeURIComponent(groupId)+'/members',{method:'DELETE',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId})});
3396
+ var d=await r.json();
3397
+ if(d.ok){toast(t('toast.memberRemoved'),'success');reloadGroupMembers(groupId);}else{toast(d.error||t('toast.removeFail'),'error');}
3398
+ }catch(e){toast(t('toast.removeFail')+': '+e.message,'error');}
3399
+ }
3400
+ async function reloadGroupMembers(groupId){
3401
+ var el=document.getElementById('groupMembers_'+groupId);
3402
+ if(!el) return;
3403
+ el.style.display='block';
3404
+ el.innerHTML=t('sharing.loading');
3405
+ try{
3406
+ var r=await fetch('/api/sharing/groups/'+encodeURIComponent(groupId)+'/members');
3407
+ var d=await r.json();
3408
+ var members=Array.isArray(d.members)?d.members:[];
3409
+ renderGroupMembers(el,groupId,members);
3410
+ }catch(e){el.innerHTML=t('admin.groupsFailed')+esc(String(e));}
3411
+ }
3412
+
3413
+ /* ─── Hub Admin Panel ─── */
3414
+ var adminDataCache={users:[],groups:[],tasks:[],skills:[],memories:[]};
3415
+
3416
+ function switchAdminTab(tab,btn){
3417
+ document.querySelectorAll('.admin-tabs .admin-tab').forEach(function(t){t.classList.remove('active');});
3418
+ btn.classList.add('active');
3419
+ document.querySelectorAll('.admin-panel').forEach(function(p){p.classList.remove('active');});
3420
+ var panel=document.getElementById('admin'+tab.charAt(0).toUpperCase()+tab.slice(1)+'Panel');
3421
+ if(panel) panel.classList.add('active');
3422
+ }
3423
+
3424
+ async function loadAdminData(){
3425
+ try{
3426
+ var [usersR,groupsR,tasksR,skillsR,pendingR,memoriesR]=await Promise.all([
3427
+ fetch('/api/sharing/users').then(function(r){return r.json();}),
3428
+ fetch('/api/sharing/groups').then(function(r){return r.json();}),
3429
+ fetch('/api/admin/shared-tasks').then(function(r){return r.json();}),
3430
+ fetch('/api/admin/shared-skills').then(function(r){return r.json();}),
3431
+ fetch('/api/sharing/pending-users').then(function(r){return r.json();}),
3432
+ fetch('/api/admin/shared-memories').then(function(r){return r.json();})
3433
+ ]);
3434
+ adminDataCache.users=Array.isArray(usersR.users)?usersR.users:[];
3435
+ adminDataCache.groups=Array.isArray(groupsR.groups)?groupsR.groups:[];
3436
+ adminDataCache.tasks=Array.isArray(tasksR.tasks)?tasksR.tasks:[];
3437
+ adminDataCache.skills=Array.isArray(skillsR.skills)?skillsR.skills:[];
3438
+ adminDataCache.memories=Array.isArray(memoriesR.memories)?memoriesR.memories:[];
3439
+ var pending=Array.isArray(pendingR.users)?pendingR.users:[];
3440
+ renderAdminStats(pending.length);
3441
+ renderAdminUsers(adminDataCache.users, pending);
3442
+ renderAdminGroups(adminDataCache.groups);
3443
+ renderAdminMemories(adminDataCache.tasks);
3444
+ renderAdminSkills(adminDataCache.skills);
3445
+ renderAdminSharedMemories(adminDataCache.memories);
3446
+ }catch(e){
3447
+ var statsEl=document.getElementById('adminStats');
3448
+ if(statsEl) statsEl.innerHTML='<div class="admin-empty">'+t('admin.loadFailed')+esc(String(e))+'</div>';
3449
+ }
3450
+ }
3451
+
3452
+ function renderAdminStats(pendingCount){
3453
+ var el=document.getElementById('adminStats');
3454
+ if(!el) return;
3455
+ el.innerHTML=
3456
+ '<div class="admin-stat-box"><span class="as-icon">\u{1F465}</span><div class="val">'+adminDataCache.users.length+'</div><div class="lbl">'+t('admin.stat.activeUsers')+'</div></div>'+
3457
+ '<div class="admin-stat-box"><span class="as-icon">\u{23F3}</span><div class="val">'+pendingCount+'</div><div class="lbl">'+t('admin.stat.pending')+'</div></div>'+
3458
+ '<div class="admin-stat-box"><span class="as-icon">\u{1F4C2}</span><div class="val">'+adminDataCache.groups.length+'</div><div class="lbl">'+t('admin.stat.groups')+'</div></div>'+
3459
+ '<div class="admin-stat-box"><span class="as-icon">\u{1F4CB}</span><div class="val">'+adminDataCache.tasks.length+'</div><div class="lbl">'+t('admin.stat.sharedTasks')+'</div></div>'+
3460
+ '<div class="admin-stat-box"><span class="as-icon">\u{1F9E0}</span><div class="val">'+adminDataCache.skills.length+'</div><div class="lbl">'+t('admin.stat.sharedSkills')+'</div></div>'+
3461
+ '<div class="admin-stat-box"><span class="as-icon">\u{1F4AD}</span><div class="val">'+(adminDataCache.memories||[]).length+'</div><div class="lbl">'+t('admin.stat.sharedMemories')+'</div></div>';
3462
+ var tc=document.getElementById('adminTabCountUsers');if(tc)tc.textContent=adminDataCache.users.length+pendingCount;
3463
+ tc=document.getElementById('adminTabCountGroups');if(tc)tc.textContent=adminDataCache.groups.length;
3464
+ tc=document.getElementById('adminTabCountMemories');if(tc)tc.textContent=(adminDataCache.memories||[]).length;
3465
+ tc=document.getElementById('adminTabCountTasks');if(tc)tc.textContent=adminDataCache.tasks.length;
3466
+ tc=document.getElementById('adminTabCountSkills');if(tc)tc.textContent=adminDataCache.skills.length;
3467
+ }
3468
+
3469
+ function renderAdminUsers(users,pending){
3470
+ var el=document.getElementById('adminUsersPanel');
3471
+ if(!el) return;
3472
+ var html='';
3473
+ if(pending&&pending.length>0){
3474
+ html+='<div style="margin-bottom:16px"><h3 style="font-size:14px;font-weight:600;color:var(--amber);margin-bottom:10px">'+t('admin.pendingApproval')+' ('+pending.length+')</h3>';
3475
+ for(var p=0;p<pending.length;p++){
3476
+ var pu=pending[p];
3477
+ html+='<div class="admin-card"><div class="admin-card-header"><div class="admin-card-title">'+esc(pu.username||pu.id||'Unknown')+'</div><span class="admin-badge pending">pending</span></div>'+
3478
+ '<div class="admin-card-meta">'+t('admin.device')+esc(pu.deviceName||'unknown')+'</div>'+
3479
+ '<div class="admin-card-actions">'+
3480
+ '<button class="btn btn-sm btn-primary" onclick="adminApproveUser(&quot;'+escAttr(pu.id)+'&quot;,&quot;'+escAttr(pu.username||'')+'&quot;)">'+t('admin.approve')+'</button>'+
3481
+ '<button class="btn btn-sm btn-ghost" onclick="adminRejectUser(&quot;'+escAttr(pu.id)+'&quot;)" style="color:var(--rose)">'+t('admin.reject')+'</button>'+
3482
+ '</div></div>';
3483
+ }
3484
+ html+='</div>';
3485
+ }
3486
+ html+='<h3 style="font-size:14px;font-weight:600;color:var(--text);margin-bottom:10px">'+t('admin.activeUsers')+' ('+users.length+')</h3>';
3487
+ if(users.length===0){
3488
+ html+='<div class="admin-empty"><span class="ae-icon">\u{1F465}</span>'+t('admin.noActiveUsers')+'</div>';
3489
+ }else{
3490
+ for(var i=0;i<users.length;i++){
3491
+ var u=users[i];
3492
+ html+='<div class="admin-card"><div class="admin-card-header"><div class="admin-card-title">'+esc(u.username||u.id)+'</div>'+
3493
+ '<span class="admin-badge '+(u.role==='admin'?'admin':'member')+'">'+esc(u.role||'member')+'</span></div>'+
3494
+ '<div class="admin-card-meta">ID: '+esc(u.id)+(u.status?' \u00B7 Status: '+esc(u.status):'')+'</div></div>';
3495
+ }
3496
+ }
3497
+ el.innerHTML=html;
3498
+ }
3499
+
3500
+ async function adminApproveUser(userId,username){
3501
+ try{
3502
+ var r=await fetch('/api/sharing/approve-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId,username:username})});
3503
+ var d=await r.json();
3504
+ if(d.ok){toast(t('toast.userApproved'),'success');loadAdminData();}else{toast(d.error||t('toast.approveFail'),'error');}
3505
+ }catch(e){toast(t('toast.approveFail')+': '+e.message,'error');}
3506
+ }
3507
+ async function adminRejectUser(userId){
3508
+ if(!confirm(t('confirm.rejectUser'))) return;
3509
+ try{
3510
+ var r=await fetch('/api/sharing/reject-user',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId})});
3511
+ var d=await r.json();
3512
+ if(d.ok){toast(t('toast.userRejected'),'success');loadAdminData();}else{toast(d.error||t('toast.rejectFail'),'error');}
3513
+ }catch(e){toast(t('toast.rejectFail')+': '+e.message,'error');}
3514
+ }
3515
+
3516
+ function renderAdminGroups(groups){
3517
+ var el=document.getElementById('adminGroupsPanel');
3518
+ if(!el) return;
3519
+ var html='<div style="margin-bottom:12px;display:flex;justify-content:space-between;align-items:center">'+
3520
+ '<h3 style="font-size:14px;font-weight:600;color:var(--text)">'+t('admin.groups')+' ('+groups.length+')</h3>'+
3521
+ '<button class="btn btn-sm btn-primary" onclick="showAdminCreateGroup()">'+t('admin.newGroup')+'</button></div>';
3522
+ html+='<div id="adminCreateGroupForm" style="display:none;margin-bottom:14px;padding:14px;background:var(--bg);border:1px solid var(--border);border-radius:10px">'+
3523
+ '<div style="display:flex;flex-direction:column;gap:8px">'+
3524
+ '<input id="adminNewGroupName" type="text" placeholder="'+t('admin.groupName')+'" style="padding:8px 12px;border:1px solid var(--border);border-radius:8px;font-size:13px;background:var(--bg-card);color:var(--text)">'+
3525
+ '<input id="adminNewGroupDesc" type="text" placeholder="'+t('admin.groupDesc')+'" style="padding:8px 12px;border:1px solid var(--border);border-radius:8px;font-size:13px;background:var(--bg-card);color:var(--text)">'+
3526
+ '<div style="display:flex;gap:8px"><button class="btn btn-sm btn-primary" onclick="adminCreateGroup()">'+t('admin.create')+'</button>'+
3527
+ '<button class="btn btn-sm btn-ghost" onclick="hideAdminCreateGroup()">'+t('admin.cancel')+'</button></div></div></div>';
3528
+ if(groups.length===0){
3529
+ html+='<div class="admin-empty"><span class="ae-icon">\u{1F4C2}</span>'+t('admin.noGroups')+'</div>';
3530
+ }else{
3531
+ for(var i=0;i<groups.length;i++){
3532
+ var g=groups[i];
3533
+ html+='<div class="admin-card"><div class="admin-card-header"><div class="admin-card-title">'+esc(g.name)+'</div>'+
3534
+ '<button class="btn btn-sm btn-ghost" onclick="adminDeleteGroup(&quot;'+escAttr(g.id)+'&quot;,&quot;'+escAttr(g.name)+'&quot;)" style="color:var(--rose);font-size:11px">'+t('admin.delete')+'</button></div>'+
3535
+ (g.description?'<div class="admin-card-meta">'+esc(g.description)+'</div>':'')+
3536
+ '<div id="adminGroupMembers_'+escAttr(g.id)+'" style="margin-top:10px"></div>'+
3537
+ '</div>';
3538
+ }
3539
+ }
3540
+ el.innerHTML=html;
3541
+ for(var i=0;i<groups.length;i++){adminLoadGroupMembers(groups[i].id);}
3542
+ }
3543
+ function showAdminCreateGroup(){var f=document.getElementById('adminCreateGroupForm');if(f)f.style.display='block';}
3544
+ function hideAdminCreateGroup(){var f=document.getElementById('adminCreateGroupForm');if(f)f.style.display='none';}
3545
+ async function adminCreateGroup(){
3546
+ var name=(document.getElementById('adminNewGroupName')).value.trim();
3547
+ var desc=(document.getElementById('adminNewGroupDesc')).value.trim();
3548
+ if(!name){toast(t('toast.groupNameRequired'),'error');return;}
3549
+ try{
3550
+ var r=await fetch('/api/sharing/groups',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:name,description:desc})});
3551
+ var d=await r.json();
3552
+ if(d.ok||d.id){toast(t('toast.groupCreated'),'success');hideAdminCreateGroup();loadAdminData();}else{toast(d.error||t('toast.createFail'),'error');}
3553
+ }catch(e){toast(t('toast.createFail')+': '+e.message,'error');}
3554
+ }
3555
+ async function adminDeleteGroup(groupId,groupName){
3556
+ if(!confirm(t('confirm.deleteGroupShort').replace('{name}',groupName))) return;
3557
+ try{
3558
+ var r=await fetch('/api/sharing/groups/'+encodeURIComponent(groupId),{method:'DELETE'});
3559
+ var d=await r.json();
3560
+ if(d.ok){toast(t('toast.groupDeleted'),'success');loadAdminData();}else{toast(d.error||t('toast.deleteFail'),'error');}
3561
+ }catch(e){toast(t('toast.deleteFail')+': '+e.message,'error');}
3562
+ }
3563
+ async function adminLoadGroupMembers(groupId){
3564
+ var el=document.getElementById('adminGroupMembers_'+groupId);
3565
+ if(!el) return;
3566
+ el.innerHTML=t('sharing.loading');
3567
+ try{
3568
+ var r=await fetch('/api/sharing/groups/'+encodeURIComponent(groupId)+'/members');
3569
+ var d=await r.json();
3570
+ var members=Array.isArray(d.members)?d.members:[];
3571
+ var html='<div style="font-size:12px;margin-bottom:6px;color:var(--text-sec)">'+t('admin.membersCount').replace('{n}',members.length)+'</div>';
3572
+ if(members.length>0){
3573
+ html+='<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px">';
3574
+ for(var i=0;i<members.length;i++){
3575
+ var m=members[i];
3576
+ html+='<span style="display:inline-flex;align-items:center;gap:4px;padding:3px 10px;background:var(--bg-card);border:1px solid var(--border);border-radius:12px;font-size:12px">'+
3577
+ esc(m.username||m.userId)+
3578
+ ' <button style="background:none;border:none;color:#ef4444;cursor:pointer;font-size:11px;padding:0 2px" onclick="adminRemoveGroupMember(&quot;'+escAttr(groupId)+'&quot;,&quot;'+escAttr(m.userId)+'&quot;)">&times;</button></span>';
3579
+ }
3580
+ html+='</div>';
3581
+ }else{
3582
+ html+='<div style="font-size:12px;color:var(--text-muted);margin-bottom:8px">'+t('admin.noMembers')+'</div>';
3583
+ }
3584
+ var memberIds=new Set(members.map(function(m){return m.userId;}));
3585
+ var available=adminDataCache.users.filter(function(u){return !memberIds.has(u.id);});
3586
+ if(available.length>0){
3587
+ html+='<div style="display:flex;gap:6px;align-items:center">'+
3588
+ '<select id="adminAddMember_'+escAttr(groupId)+'" style="padding:4px 8px;border:1px solid var(--border);border-radius:6px;font-size:12px;background:var(--bg-card);color:var(--text)">';
3589
+ for(var j=0;j<available.length;j++){
3590
+ html+='<option value="'+escAttr(available[j].id)+'">'+esc(available[j].username)+'</option>';
3591
+ }
3592
+ html+='</select><button class="btn btn-sm" onclick="adminAddGroupMember(&quot;'+escAttr(groupId)+'&quot;)">'+t('admin.add')+'</button></div>';
3593
+ }
3594
+ el.innerHTML=html;
3595
+ }catch(e){el.innerHTML=t('admin.groupsFailed')+esc(String(e));}
3596
+ }
3597
+ async function adminAddGroupMember(groupId){
3598
+ var sel=document.getElementById('adminAddMember_'+groupId);
3599
+ if(!sel) return;
3600
+ try{
3601
+ var r=await fetch('/api/sharing/groups/'+encodeURIComponent(groupId)+'/members',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:sel.value})});
3602
+ var d=await r.json();
3603
+ if(d.ok){toast(t('toast.memberAdded'),'success');adminLoadGroupMembers(groupId);}else{toast(d.error||t('toast.addFail'),'error');}
3604
+ }catch(e){toast(t('toast.addFail')+': '+e.message,'error');}
3605
+ }
3606
+ async function adminRemoveGroupMember(groupId,userId){
3607
+ if(!confirm(t('confirm.removeMember'))) return;
3608
+ try{
3609
+ var r=await fetch('/api/sharing/groups/'+encodeURIComponent(groupId)+'/members',{method:'DELETE',headers:{'Content-Type':'application/json'},body:JSON.stringify({userId:userId})});
3610
+ var d=await r.json();
3611
+ if(d.ok){toast(t('toast.memberRemoved'),'success');adminLoadGroupMembers(groupId);}else{toast(d.error||t('toast.removeFail'),'error');}
3612
+ }catch(e){toast(t('toast.removeFail')+': '+e.message,'error');}
3613
+ }
3614
+
3615
+ function renderAdminMemories(tasks){
3616
+ var el=document.getElementById('adminMemoriesPanel');
3617
+ if(!el) return;
3618
+ adminTasksCache=tasks;
3619
+ var html='<h3 style="font-size:14px;font-weight:600;color:var(--text);margin-bottom:10px">'+t('admin.sharedTasks')+' ('+tasks.length+')</h3>';
3620
+ if(tasks.length===0){
3621
+ html+='<div class="admin-empty"><span class="ae-icon">\u{1F4CB}</span>'+t('admin.noSharedTasks')+'</div>';
3622
+ }else{
3623
+ for(var i=0;i<tasks.length;i++){
3624
+ var tk=tasks[i];
3625
+ html+='<div class="admin-card" onclick="openHubTaskDetailFromCache(\\\'admin\\\','+i+')" style="cursor:pointer"><div class="admin-card-header"><div class="admin-card-title">'+esc(tk.title||tk.id)+'</div>'+
3626
+ '<span class="admin-badge '+(tk.visibility==='public'?'public':'group')+'">'+esc(tk.visibility||'public')+'</span></div>'+
3627
+ '<div class="admin-card-meta">'+
3628
+ t('admin.owner')+esc(tk.ownerName||tk.sourceUserId||'unknown')+
3629
+ (tk.groupName?' \u00B7 '+t('admin.group')+esc(tk.groupName):'')+
3630
+ (tk.chunkCount!=null?' \u00B7 '+t('admin.chunks')+tk.chunkCount:'')+
3631
+ ' \u00B7 '+t('admin.updated')+new Date(tk.updatedAt||tk.createdAt).toLocaleDateString()+
3632
+ '</div>'+
3633
+ '<div class="admin-card-actions">'+
3634
+ '<button class="btn btn-sm btn-ghost" onclick="event.stopPropagation();adminDeleteTask(&quot;'+escAttr(tk.id)+'&quot;,&quot;'+escAttr(tk.title||tk.id)+'&quot;)" style="color:var(--rose)">'+t('admin.remove')+'</button>'+
3635
+ '</div></div>';
3636
+ }
3637
+ }
3638
+ el.innerHTML=html;
3639
+ }
3640
+
3641
+ async function adminDeleteTask(taskId,taskTitle){
3642
+ if(!confirm(t('confirm.removeTask').replace('{name}',taskTitle))) return;
3643
+ try{
3644
+ var r=await fetch('/api/admin/shared-tasks/'+encodeURIComponent(taskId),{method:'DELETE'});
3645
+ var d=await r.json();
3646
+ if(d.ok){toast(t('toast.taskRemoved'),'success');loadAdminData();}else{toast(d.error||t('toast.removeFail'),'error');}
3647
+ }catch(e){toast(t('toast.removeFail')+': '+e.message,'error');}
3648
+ }
3649
+
3650
+ function renderAdminSkills(skills){
3651
+ var el=document.getElementById('adminSkillsPanel');
3652
+ if(!el) return;
3653
+ adminSkillsCache=skills;
3654
+ var html='<h3 style="font-size:14px;font-weight:600;color:var(--text);margin-bottom:10px">'+t('admin.sharedSkills')+' ('+skills.length+')</h3>';
3655
+ if(skills.length===0){
3656
+ html+='<div class="admin-empty"><span class="ae-icon">\u{1F9E0}</span>'+t('admin.noSharedSkills')+'</div>';
3657
+ }else{
3658
+ for(var i=0;i<skills.length;i++){
3659
+ var s=skills[i];
3660
+ html+='<div class="admin-card" onclick="openHubSkillDetailFromCache(\\\'admin\\\','+i+')" style="cursor:pointer"><div class="admin-card-header"><div class="admin-card-title">'+esc(s.name||s.id)+'</div>'+
3661
+ '<span class="admin-badge '+(s.visibility==='public'?'public':'group')+'">'+esc(s.visibility||'public')+'</span></div>'+
3662
+ '<div class="admin-card-meta">'+
3663
+ (s.description?esc(s.description)+'<br>':'')+
3664
+ t('admin.owner')+esc(s.ownerName||s.sourceUserId||'unknown')+
3665
+ (s.groupName?' \u00B7 '+t('admin.group')+esc(s.groupName):'')+
3666
+ (s.version!=null?' \u00B7 '+t('admin.version')+s.version:'')+
3667
+ (s.qualityScore!=null?' \u00B7 '+t('admin.quality')+s.qualityScore:'')+
3668
+ '</div>'+
3669
+ '<div class="admin-card-actions">'+
3670
+ '<button class="btn btn-sm btn-ghost" onclick="event.stopPropagation();adminDeleteSkill(&quot;'+escAttr(s.id)+'&quot;,&quot;'+escAttr(s.name||s.id)+'&quot;)" style="color:var(--rose)">'+t('admin.remove')+'</button>'+
3671
+ '</div></div>';
3672
+ }
3673
+ }
3674
+ el.innerHTML=html;
3675
+ }
3676
+
3677
+ async function adminDeleteSkill(skillId,skillName){
3678
+ if(!confirm(t('confirm.removeSkill').replace('{name}',skillName))) return;
3679
+ try{
3680
+ var r=await fetch('/api/admin/shared-skills/'+encodeURIComponent(skillId),{method:'DELETE'});
3681
+ var d=await r.json();
3682
+ if(d.ok){toast(t('toast.skillRemoved'),'success');loadAdminData();}else{toast(d.error||t('toast.removeFail'),'error');}
3683
+ }catch(e){toast(t('toast.removeFail')+': '+e.message,'error');}
3684
+ }
3685
+
3686
+ function renderAdminSharedMemories(memories){
3687
+ var el=document.getElementById('adminSharedMemoriesPanel');
3688
+ if(!el) return;
3689
+ adminMemoriesCache=memories||[];
3690
+ var html='<h3 style="font-size:14px;font-weight:600;color:var(--text);margin-bottom:10px">'+t('admin.sharedMemories')+' ('+(memories||[]).length+')</h3>';
3691
+ if(!memories||memories.length===0){
3692
+ html+='<div class="admin-empty"><span class="ae-icon">\u{1F4AD}</span>'+t('admin.noSharedMemories')+'</div>';
3693
+ }else{
3694
+ for(var i=0;i<memories.length;i++){
3695
+ var m=memories[i];
3696
+ html+='<div class="admin-card" onclick="openHubMemoryDetail(\\\'admin\\\','+i+')" style="cursor:pointer"><div class="admin-card-header"><div class="admin-card-title">'+esc(m.summary||m.content?.slice(0,80)||m.id)+'</div>'+
3697
+ '<span class="admin-badge '+(m.visibility==='public'?'public':'group')+'">'+esc(m.visibility||'public')+'</span></div>'+
3698
+ '<div class="admin-card-meta">'+
3699
+ t('admin.owner')+esc(m.ownerName||m.sourceUserId||'unknown')+
3700
+ (m.groupName?' \u00B7 '+t('admin.group')+esc(m.groupName):'')+
3701
+ (m.kind?' \u00B7 Kind: '+esc(m.kind):'')+
3702
+ (m.role?' \u00B7 Role: '+esc(m.role):'')+
3703
+ ' \u00B7 '+t('admin.updated')+new Date(m.updatedAt||m.createdAt).toLocaleDateString()+
3704
+ '</div>'+
3705
+ '<div class="admin-card-actions">'+
3706
+ '<button class="btn btn-sm btn-ghost" onclick="event.stopPropagation();adminDeleteMemory(&quot;'+escAttr(m.id)+'&quot;,&quot;'+escAttr(m.summary||m.id)+'&quot;)" style="color:var(--rose)">'+t('admin.remove')+'</button>'+
3707
+ '</div></div>';
3708
+ }
3709
+ }
3710
+ el.innerHTML=html;
3711
+ }
3712
+
3713
+ async function adminDeleteMemory(memoryId,memoryTitle){
3714
+ if(!confirm(t('confirm.removeMemory').replace('{name}',memoryTitle))) return;
3715
+ try{
3716
+ var r=await fetch('/api/admin/shared-memories/'+encodeURIComponent(memoryId),{method:'DELETE'});
3717
+ var d=await r.json();
3718
+ if(d.ok){toast(t('toast.memoryRemoved'),'success');loadAdminData();}else{toast(d.error||t('toast.removeFail'),'error');}
3719
+ }catch(e){toast(t('toast.removeFail')+': '+e.message,'error');}
3720
+ }
3721
+
3722
+ function renderSharingMemorySearchResults(data,query){
3723
+ const list=document.getElementById('memoryList');
3724
+ const localHits=(data&&data.local&&Array.isArray(data.local.hits))?data.local.hits:[];
3725
+ const hubHits=(data&&data.hub&&Array.isArray(data.hub.hits))?data.hub.hits:[];
3726
+ document.getElementById('searchMeta').textContent='Search results for "'+query+'"';
3727
+ document.getElementById('sharingSearchMeta').textContent='Local '+localHits.length+' · Hub '+hubHits.length;
3728
+ document.getElementById('pagination').innerHTML='';
3729
+ list.innerHTML=''+
3730
+ '<div class="result-section">'+
3731
+ '<div class="result-section-header"><div class="result-section-title">'+t('search.localResults')+'</div><div class="result-section-sub">'+localHits.length+' hit(s)</div></div>'+
3732
+ '<div class="search-hit-list">'+(localHits.length?localHits.map(function(hit,idx){
3733
+ return '<div class="search-hit-card">'+
3734
+ '<div class="summary">'+(idx+1)+'. '+esc(hit.summary||'(no summary)')+'</div>'+
3735
+ '<div class="excerpt">'+esc(hit.excerpt||'')+'</div>'+
3736
+ '<div class="search-hit-meta">'+
3737
+ '<span class="meta-chip">role: '+esc(hit.role||'unknown')+'</span>'+
3738
+ (hit.score!=null?'<span class="meta-chip">score: '+Math.round(hit.score*100)+'%</span>':'')+
3739
+ (hit.taskId?'<span class="meta-chip">task: '+esc(hit.taskId)+'</span>':'')+
3740
+ '</div>'+
3741
+ '</div>';
3742
+ }).join(''):'<div class="search-hit-card"><div class="excerpt">'+t('search.noLocal')+'</div></div>')+'</div>'+
3743
+ '</div>'+
3744
+ '<div class="result-section">'+
3745
+ '<div class="result-section-header"><div class="result-section-title">'+t('search.hubResults')+'</div><div class="result-section-sub">'+hubHits.length+' hit(s)</div></div>'+
3746
+ '<div class="search-hit-list">'+(hubHits.length?hubHits.map(function(hit,idx){
3747
+ return '<div class="hub-hit-card">'+
3748
+ '<div class="summary">'+(idx+1)+'. '+esc(hit.summary||'(no summary)')+'</div>'+
3749
+ '<div class="excerpt">'+esc(hit.excerpt||'')+'</div>'+
3750
+ '<div class="hub-hit-meta">'+
3751
+ '<span class="meta-chip">owner: '+esc(hit.ownerName||'unknown')+'</span>'+
3752
+ (hit.groupName?'<span class="meta-chip">group: '+esc(hit.groupName)+'</span>':'')+
3753
+ '<span class="meta-chip">visibility: '+esc(hit.visibility||'hub')+'</span>'+
3754
+ '</div>'+
3755
+ '<div class="hub-hit-actions">'+
3756
+ '<button class="btn btn-sm" onclick="openSharedMemoryDetail(&quot;'+escAttr(hit.remoteHitId)+'&quot;,&quot;'+escAttr(hit.summary||t('search.sharedMemory'))+'&quot;,&quot;'+escAttr(hit.ownerName||'')+'&quot;,&quot;'+escAttr(hit.groupName||'')+'&quot;)">'+t('search.viewDetail')+'</button>'+
3757
+ '</div>'+
3758
+ '</div>';
3759
+ }).join(''):'<div class="hub-hit-card"><div class="excerpt">'+t('search.noHub')+'</div></div>')+'</div>'+
3760
+ '</div>';
3761
+ }
3762
+
3763
+ async function openSharedMemoryDetail(remoteHitId,title,owner,groupName){
3764
+ currentSharedMemoryHitId=remoteHitId;
3765
+ document.getElementById('sharedMemoryOverlay').classList.add('show');
3766
+ document.getElementById('sharedMemoryTitle').textContent=title||t('search.sharedMemory');
3767
+ document.getElementById('sharedMemoryMeta').innerHTML='<span class="meta-item">Hub</span>'+(owner?'<span class="meta-item">'+t('admin.owner')+esc(owner)+'</span>':'')+(groupName?'<span class="meta-item">'+t('admin.group')+esc(groupName)+'</span>':'');
3768
+ document.getElementById('sharedMemorySummary').textContent=t('sharing.loading');
3769
+ document.getElementById('sharedMemoryContent').textContent='';
3770
+ try{
3771
+ const r=await fetch('/api/sharing/memory-detail',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({remoteHitId:remoteHitId})});
3772
+ const d=await r.json();
3773
+ if(d.error) throw new Error(d.error);
3774
+ document.getElementById('sharedMemorySummary').textContent=d.summary||'';
3775
+ document.getElementById('sharedMemoryContent').textContent=d.content||'';
3776
+ }catch(e){
3777
+ document.getElementById('sharedMemorySummary').textContent=t('search.loadFailed');
3778
+ document.getElementById('sharedMemoryContent').textContent=String(e.message||e);
3779
+ }
3780
+ }
3781
+
3782
+ function closeSharedMemoryDetail(event){
3783
+ if(event && event.target!==document.getElementById('sharedMemoryOverlay')) return;
3784
+ document.getElementById('sharedMemoryOverlay').classList.remove('show');
3785
+ }
3786
+
3787
+ async function openHubMemoryDetail(cacheKey,idx){
3788
+ var arr=cacheKey==='admin'?adminMemoriesCache:hubMemoriesCache;
3789
+ var m=arr[idx];
3790
+ if(!m) return;
3791
+ var overlay=document.getElementById('sharedMemoryOverlay');
3792
+ overlay.classList.add('show');
3793
+ document.getElementById('sharedMemoryTitle').textContent=m.summary||m.content?.slice(0,80)||'(no summary)';
3794
+ var metaHtml='<span class="meta-item">\\u{1F310} Hub</span>'+
3795
+ (m.ownerName?'<span class="meta-item">'+t('admin.owner')+esc(m.ownerName)+'</span>':'')+
3796
+ (m.groupName?'<span class="meta-item">'+t('admin.group')+esc(m.groupName)+'</span>':'')+
3797
+ (m.kind?'<span class="meta-item">Kind: '+esc(m.kind)+'</span>':'')+
3798
+ (m.role?'<span class="meta-item">Role: '+esc(m.role)+'</span>':'')+
3799
+ '<span class="meta-item">visibility: '+esc(m.visibility||'hub')+'</span>'+
3800
+ '<span class="meta-item">'+new Date(m.updatedAt||m.createdAt||0).toLocaleString(dateLoc())+'</span>';
3801
+ document.getElementById('sharedMemoryMeta').innerHTML=metaHtml;
3802
+ document.getElementById('sharedMemorySummary').textContent=m.summary||'';
3803
+ document.getElementById('sharedMemoryContent').textContent=m.content||t('sharing.loading');
3804
+ // try to fetch full content from Hub API
3805
+ var remoteId=m.remoteHitId||m.id;
3806
+ if(remoteId){
3807
+ try{
3808
+ var r=await fetch('/api/sharing/memory-detail',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({remoteHitId:remoteId})});
3809
+ var d=await r.json();
3810
+ if(!d.error&&(d.content||d.summary)){
3811
+ if(d.summary) document.getElementById('sharedMemorySummary').textContent=d.summary;
3812
+ document.getElementById('sharedMemoryContent').textContent=d.content||m.content||'';
3813
+ }
3814
+ }catch(e){}
3815
+ }
3816
+ }
3817
+
3818
+ function openHubTaskDetailFromCache(cacheKey,idx){
3819
+ var arr=cacheKey==='admin'?adminTasksCache:hubTasksCache;
3820
+ var task=arr[idx];
3821
+ if(!task) return;
3822
+ var overlay=document.getElementById('taskDetailOverlay');
3823
+ overlay.classList.add('show');
3824
+ document.getElementById('taskDetailTitle').textContent=task.title||'(no title)';
3825
+ document.getElementById('taskShareActions').innerHTML='';
3826
+ var meta=[
3827
+ '<span class="meta-item">\\u{1F310} Hub</span>',
3828
+ task.status?'<span class="meta-item"><span class="task-status-badge '+task.status+'">'+esc(task.status)+'</span></span>':'',
3829
+ '<span class="meta-item">'+t('admin.owner')+esc(task.ownerName||'unknown')+'</span>',
3830
+ task.groupName?'<span class="meta-item">'+t('admin.group')+esc(task.groupName)+'</span>':'',
3831
+ '<span class="meta-item">visibility: '+esc(task.visibility||'hub')+'</span>',
3832
+ task.chunkCount!=null?'<span class="meta-item">\\u{1F4DD} '+esc(String(task.chunkCount))+' '+t('tasks.chunks.label')+'</span>':'',
3833
+ task.startedAt?'<span class="meta-item">\\u{1F4C5} '+formatTime(task.startedAt)+'</span>':'',
3834
+ task.endedAt?'<span class="meta-item">\\u2192 '+formatTime(task.endedAt)+'</span>':'',
3835
+ (task.updatedAt||task.createdAt)?'<span class="meta-item">'+t('admin.updated')+new Date(task.updatedAt||task.createdAt).toLocaleString(dateLoc())+'</span>':'',
3836
+ task.sourceTaskId?'<div style="width:100%;margin-top:4px"><span class="meta-item" style="width:100%">'+t('tasks.taskid')+'<span class="task-id-full">'+esc(task.sourceTaskId)+'</span></span></div>':'',
3837
+ ].filter(Boolean);
3838
+ document.getElementById('taskDetailMeta').innerHTML=meta.join('');
3839
+ document.getElementById('taskSkillSection').innerHTML='';
3840
+ document.getElementById('taskSkillSection').className='task-skill-section';
3841
+ document.getElementById('taskDetailSummary').innerHTML=task.summary?renderSummaryHtml(task.summary):'<div style="color:var(--text-muted);font-size:13px">'+t('tasks.nochunks')+'</div>';
3842
+ document.getElementById('taskDetailChunks').innerHTML='<div style="color:var(--text-muted);padding:12px;font-size:13px">'+t('tasks.nochunks')+'</div>';
3843
+ }
3844
+
3845
+ function openHubSkillDetailFromCache(cacheKey,idx){
3846
+ var arr=cacheKey==='admin'?adminSkillsCache:hubSkillsCache;
3847
+ var skill=arr[idx];
3848
+ if(!skill) return;
3849
+ var overlay=document.getElementById('skillDetailOverlay');
3850
+ overlay.classList.add('show');
3851
+ document.getElementById('skillDetailTitle').textContent='\\u{1F9E0} '+(skill.name||'(no name)');
3852
+ var qs=skill.qualityScore;
3853
+ var qsBadge=(qs!==null&&qs!==undefined)?'<span class="meta-item"><span class="skill-badge quality '+(qs>=7?'high':qs>=5?'mid':'low')+'">\\u2605 '+(+qs).toFixed(1)+'/10</span></span>':'';
3854
+ var meta=[
3855
+ '<span class="meta-item">\\u{1F310} Hub</span>',
3856
+ skill.version!=null?'<span class="meta-item"><span class="skill-badge version">v'+skill.version+'</span></span>':'',
3857
+ skill.status?'<span class="meta-item"><span class="skill-badge status-'+skill.status+'">'+esc(skill.status)+'</span></span>':'',
3858
+ '<span class="meta-item">visibility: '+esc(skill.visibility||'hub')+'</span>',
3859
+ qsBadge,
3860
+ '<span class="meta-item">'+t('admin.owner')+esc(skill.ownerName||'unknown')+'</span>',
3861
+ skill.groupName?'<span class="meta-item">'+t('admin.group')+esc(skill.groupName)+'</span>':'',
3862
+ (skill.updatedAt||skill.createdAt)?'<span class="meta-item">'+t('admin.updated')+new Date(skill.updatedAt||skill.createdAt).toLocaleString(dateLoc())+'</span>':'',
3863
+ ].filter(Boolean);
3864
+ document.getElementById('skillDetailMeta').innerHTML=meta.join('');
3865
+ document.getElementById('skillDetailDesc').textContent=skill.description||'';
3866
+ document.getElementById('skillFilesList').innerHTML='';
3867
+ document.getElementById('skillDetailContent').innerHTML=skill.content?'<pre>'+esc(skill.content)+'</pre>':'';
3868
+ document.getElementById('skillVersionsList').innerHTML='';
3869
+ document.getElementById('skillRelatedTasks').innerHTML='';
3870
+ var visBtn=document.getElementById('skillVisibilityBtn');
3871
+ if(visBtn) visBtn.style.display='none';
3872
+ var dlBtn=document.getElementById('skillDownloadBtn');
3873
+ if(dlBtn) dlBtn.style.display='none';
3874
+ var shareBtn=document.getElementById('skillShareActions');
3875
+ if(shareBtn) shareBtn.innerHTML='';
3876
+ }
3877
+
3878
+ function escAttr(s){return String(s||'').replace(/&/g,'&amp;').replace(/'/g,'&#39;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
3879
+
3880
+ function renderTaskShareActions(task){
3881
+ currentTaskDetail=task||null;
3882
+ const el=document.getElementById('taskShareActions');
3883
+ if(!el){return;}
3884
+ if(!task||!task.id){el.innerHTML='';return;}
3885
+ const current=(task.sharingVisibility||task.visibility||null);
3886
+ const isShared=!!current;
3887
+ var statusHtml='';
3888
+ if(isShared){
3889
+ var scopeLabel=current==='group'?'Group':'Public';
3890
+ statusHtml='<span style="display:inline-flex;align-items:center;gap:4px;padding:3px 10px;background:#22c55e22;border:1px solid #22c55e44;border-radius:12px;font-size:12px;color:#22c55e">\u2713 '+t('share.alreadyShared')+' ('+scopeLabel+')</span>';
3891
+ }
3892
+ el.innerHTML=statusHtml+
3893
+ '<select id="taskShareScope" class="filter-select" style="min-width:120px">'+
3894
+ '<option value="group"'+(current==='group'?' selected':'')+'>Share to Group</option>'+
3895
+ '<option value="public"'+(current==='public'?' selected':'')+'>Share to Public</option>'+
3896
+ '</select>'+
3897
+ '<button class="btn btn-sm" onclick="shareCurrentTask()">'+(isShared?t('share.updateBtn'):t('share.shareBtn'))+'</button>'+
3898
+ (isShared?'<button class="btn btn-sm btn-ghost" onclick="unshareCurrentTask()">'+t('share.unshareBtn')+'</button>':'');
3899
+ }
3900
+
3901
+ async function shareCurrentTask(){
3902
+ if(!currentTaskDetail) return;
3903
+ const visibility=document.getElementById('taskShareScope').value||'public';
3904
+ try{
3905
+ const r=await fetch('/api/sharing/tasks/share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({taskId:currentTaskDetail.id,visibility:visibility})});
3906
+ const d=await r.json();
3907
+ if(d.ok||d.shared){toast(t('toast.taskShared'),'success');currentTaskDetail.sharingVisibility=visibility;renderTaskShareActions(currentTaskDetail);} else {toast(d.error||t('toast.taskShareFail'),'error');}
3908
+ }catch(e){toast(t('toast.taskShareFail')+': '+e.message,'error');}
3909
+ }
3910
+
3911
+ async function unshareCurrentTask(){
3912
+ if(!currentTaskDetail) return;
3913
+ try{
3914
+ const r=await fetch('/api/sharing/tasks/unshare',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({taskId:currentTaskDetail.id})});
3915
+ const d=await r.json();
3916
+ if(d.ok||d.unshared){toast(t('toast.taskUnshared'),'success');currentTaskDetail.sharingVisibility=null;renderTaskShareActions(currentTaskDetail);} else {toast(d.error||t('toast.taskUnshareFail'),'error');}
3917
+ }catch(e){toast(t('toast.taskUnshareFail')+': '+e.message,'error');}
3918
+ }
3919
+
3920
+ function renderSkillShareActions(skill){
3921
+ const el=document.getElementById('skillShareActions');
3922
+ if(!el){return;}
3923
+ if(!skill||!skill.id){el.innerHTML='';return;}
3924
+ const current=(skill.sharingVisibility||null);
3925
+ const isShared=!!current;
3926
+ var statusHtml='';
3927
+ if(isShared){
3928
+ var scopeLabel=current==='group'?'Group':'Public';
3929
+ statusHtml='<span style="display:inline-flex;align-items:center;gap:4px;padding:3px 10px;background:#22c55e22;border:1px solid #22c55e44;border-radius:12px;font-size:12px;color:#22c55e">\u2713 '+t('share.alreadyShared')+' ('+scopeLabel+')</span>';
3930
+ }
3931
+ el.innerHTML=statusHtml+
3932
+ '<select id="skillShareScope" class="filter-select" style="min-width:120px">'+
3933
+ '<option value="group"'+(current==='group'?' selected':'')+'>Share to Group</option>'+
3934
+ '<option value="public"'+(current==='public'?' selected':'')+'>Share to Public</option>'+
3935
+ '</select>'+
3936
+ '<button class="btn btn-sm" onclick="shareCurrentSkill()">'+(isShared?t('share.updateBtn'):t('share.shareBtn'))+'</button>'+
3937
+ (isShared?'<button class="btn btn-sm btn-ghost" onclick="unshareCurrentSkill()">'+t('share.unshareBtn')+'</button>':'');
3938
+ }
3939
+
3940
+ async function shareCurrentSkill(){
3941
+ if(!currentSkillDetail) return;
3942
+ const visibility=document.getElementById('skillShareScope').value||'public';
3943
+ try{
3944
+ const r=await fetch('/api/sharing/skills/share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({skillId:currentSkillDetail.id,visibility:visibility})});
3945
+ const d=await r.json();
3946
+ if(d.ok){toast(t('toast.skillShared'),'success');currentSkillDetail.sharingVisibility=visibility;renderSkillShareActions(currentSkillDetail);} else {toast(d.error||t('toast.skillShareFail'),'error');}
3947
+ }catch(e){toast(t('toast.skillShareFail')+': '+e.message,'error');}
3948
+ }
3949
+
3950
+ async function unshareCurrentSkill(){
3951
+ if(!currentSkillDetail) return;
3952
+ try{
3953
+ const r=await fetch('/api/sharing/skills/unshare',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({skillId:currentSkillDetail.id})});
3954
+ const d=await r.json();
3955
+ if(d.ok){toast(t('toast.skillUnshared'),'success');currentSkillDetail.sharingVisibility=null;renderSkillShareActions(currentSkillDetail);} else {toast(d.error||t('toast.skillUnshareFail'),'error');}
3956
+ }catch(e){toast(t('toast.skillUnshareFail')+': '+e.message,'error');}
3957
+ }
3958
+
3959
+ async function shareMemoryPrompt(chunkId){
3960
+ try{
3961
+ const r=await fetch('/api/sharing/memories/share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({chunkId:chunkId,visibility:'public'})});
3962
+ const d=await r.json();
3963
+ if(d.ok){toast(t('toast.memoryShared'),'success');loadMemories();} else {toast(d.error||t('toast.memoryShareFail'),'error');}
3964
+ }catch(e){toast(t('toast.memoryShareFail')+': '+e.message,'error');}
3965
+ }
3966
+
3967
+ async function unshareMemory(chunkId){
3968
+ try{
3969
+ const r=await fetch('/api/sharing/memories/unshare',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({chunkId:chunkId})});
3970
+ const d=await r.json();
3971
+ if(d.ok){toast(t('toast.memoryUnshared'),'success');loadMemories();} else {toast(d.error||t('toast.memoryUnshareFail'),'error');}
3972
+ }catch(e){toast(t('toast.memoryUnshareFail')+': '+e.message,'error');}
3973
+ }
3974
+
3975
+ function debounceSkillSearch(){
3976
+ clearTimeout(skillSearchTimer);
3977
+ skillSearchTimer=setTimeout(function(){loadSkills();},300);
3978
+ }
3979
+
3980
+ async function pullHubSkill(skillId){
3981
+ try{
3982
+ const r=await fetch('/api/sharing/skills/pull',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({skillId:skillId})});
3983
+ const d=await r.json();
3984
+ if(d.ok||d.pulled||d.details){toast(t('toast.skillPulled'),'success');loadSkills();} else {toast(d.error||t('toast.skillPullFail'),'error');}
3985
+ }catch(e){toast(t('toast.skillPullFail')+': '+e.message,'error');}
2173
3986
  }
2174
3987
 
2175
3988
  // ─── Logs ───
@@ -2228,7 +4041,7 @@ function renderLogToolFilter(tools,current){
2228
4041
 
2229
4042
  function formatLogTime(ts){
2230
4043
  const d=new Date(ts);
2231
- const time=d.toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit',second:'2-digit',hour12:false});
4044
+ const time=d.toLocaleTimeString(dateLoc(),{hour:'2-digit',minute:'2-digit',second:'2-digit',hour12:false});
2232
4045
  const y=d.getFullYear();
2233
4046
  const m=String(d.getMonth()+1).padStart(2,'0');
2234
4047
  const day=String(d.getDate()).padStart(2,'0');
@@ -2489,6 +4302,7 @@ function setMetricsDays(d){
2489
4302
  }
2490
4303
 
2491
4304
  async function loadMetrics(){
4305
+ try{
2492
4306
  const r=await fetch('/api/metrics?days='+metricsDays);
2493
4307
  const d=await r.json();
2494
4308
  document.getElementById('mTotal').textContent=formatNum(d.totals.memories);
@@ -2497,9 +4311,11 @@ async function loadMetrics(){
2497
4311
  document.getElementById('mEmbeddings').textContent=formatNum(d.totals.embeddings);
2498
4312
  renderChartWrites(d.writesPerDay);
2499
4313
  loadToolMetrics();
4314
+ }catch(e){console.error('loadMetrics',e)}
2500
4315
  }
2501
4316
 
2502
4317
  function formatNum(n){return n>=1e6?(n/1e6).toFixed(1)+'M':n>=1e3?(n/1e3).toFixed(1)+'k':String(n);}
4318
+ function dateLoc(){return curLang==='zh'?'zh-CN':'en-US';}
2503
4319
 
2504
4320
  /* ─── Tasks View Logic ─── */
2505
4321
  let tasksStatusFilter='';
@@ -2515,29 +4331,24 @@ function setTaskStatusFilter(btn,status){
2515
4331
  }
2516
4332
 
2517
4333
  async function loadTasks(){
4334
+ const scope=document.getElementById('taskSearchScope')?document.getElementById('taskSearchScope').value:taskSearchScope;
4335
+ taskSearchScope=scope||'local';
4336
+ if(taskSearchScope!=='local'){ return loadHubTasks(); }
2518
4337
  const list=document.getElementById('tasksList');
2519
4338
  list.innerHTML='<div class="spinner"></div>';
2520
4339
  try{
2521
4340
  const params=new URLSearchParams({limit:String(TASKS_PER_PAGE),offset:String(tasksPage*TASKS_PER_PAGE)});
2522
4341
  if(tasksStatusFilter) params.set('status',tasksStatusFilter);
2523
- const r=await fetch('/api/tasks?'+params);
2524
- const data=await r.json();
2525
-
2526
- // stats
2527
- const allR=await fetch('/api/tasks?limit=1&offset=0');
2528
- const allD=await allR.json();
4342
+ const [data,allD,activeD,compD,skipD]=await Promise.all([
4343
+ fetch('/api/tasks?'+params).then(r=>r.json()),
4344
+ fetch('/api/tasks?limit=1&offset=0').then(r=>r.json()),
4345
+ fetch('/api/tasks?status=active&limit=1&offset=0').then(r=>r.json()),
4346
+ fetch('/api/tasks?status=completed&limit=1&offset=0').then(r=>r.json()),
4347
+ fetch('/api/tasks?status=skipped&limit=1&offset=0').then(r=>r.json())
4348
+ ]);
2529
4349
  document.getElementById('tasksTotalCount').textContent=formatNum(allD.total);
2530
-
2531
- const activeR=await fetch('/api/tasks?status=active&limit=1&offset=0');
2532
- const activeD=await activeR.json();
2533
4350
  document.getElementById('tasksActiveCount').textContent=formatNum(activeD.total);
2534
-
2535
- const compR=await fetch('/api/tasks?status=completed&limit=1&offset=0');
2536
- const compD=await compR.json();
2537
4351
  document.getElementById('tasksCompletedCount').textContent=formatNum(compD.total);
2538
-
2539
- const skipR=await fetch('/api/tasks?status=skipped&limit=1&offset=0');
2540
- const skipD=await skipR.json();
2541
4352
  document.getElementById('tasksSkippedCount').textContent=formatNum(skipD.total);
2542
4353
 
2543
4354
  if(!data.tasks||data.tasks.length===0){
@@ -2603,13 +4414,16 @@ async function openTaskDetail(taskId){
2603
4414
  document.getElementById('taskSkillSection').className='task-skill-section';
2604
4415
  document.getElementById('taskDetailSummary').textContent='';
2605
4416
  document.getElementById('taskDetailChunks').innerHTML='<div class="spinner"></div>';
4417
+ document.getElementById('taskShareActions').innerHTML='';
2606
4418
  document.getElementById('taskDetailActions').innerHTML='';
2607
4419
 
2608
4420
  try{
2609
4421
  const r=await fetch('/api/task/'+taskId);
2610
4422
  const task=await r.json();
4423
+ currentTaskDetail=task;
2611
4424
 
2612
4425
  document.getElementById('taskDetailTitle').textContent=task.title||t('tasks.untitled');
4426
+ renderTaskShareActions(task);
2613
4427
 
2614
4428
  const meta=[
2615
4429
  '<span class="meta-item"><span class="task-status-badge '+task.status+'">'+t('tasks.status.'+task.status)+'</span></span>',
@@ -2778,60 +4592,177 @@ function setSkillStatusFilter(btn,status){
2778
4592
 
2779
4593
  async function loadSkills(){
2780
4594
  const list=document.getElementById('skillsList');
4595
+ const hubList=document.getElementById('hubSkillsList');
2781
4596
  list.innerHTML='<div class="spinner"></div>';
4597
+ var hubSection=document.getElementById('hubSkillsSection');
4598
+ if(hubList){
4599
+ if(skillSearchScope==='local'){
4600
+ if(hubSection) hubSection.style.display='none';
4601
+ }else{
4602
+ if(hubSection) hubSection.style.display='block';
4603
+ hubList.innerHTML='<div class="spinner"></div>';
4604
+ }
4605
+ }
4606
+
4607
+ const query=(document.getElementById('skillSearchInput')?.value||'').trim();
4608
+ const scope=document.getElementById('skillSearchScope') ? document.getElementById('skillSearchScope').value : skillSearchScope;
4609
+ skillSearchScope=scope||'local';
4610
+
2782
4611
  try{
2783
4612
  const params=new URLSearchParams();
2784
4613
  if(skillsStatusFilter) params.set('status',skillsStatusFilter);
2785
4614
  const visFilter=document.getElementById('skillVisibilityFilter')?.value;
2786
4615
  if(visFilter) params.set('visibility',visFilter);
2787
- const r=await fetch('/api/skills?'+params);
2788
- const data=await r.json();
2789
4616
 
2790
- document.getElementById('skillsTotalCount').textContent=formatNum(data.skills.length);
2791
- document.getElementById('skillsActiveCount').textContent=formatNum(data.skills.filter(s=>s.status==='active').length);
2792
- document.getElementById('skillsDraftCount').textContent=formatNum(data.skills.filter(s=>s.status==='draft').length);
2793
- document.getElementById('skillsInstalledCount').textContent=formatNum(data.skills.filter(s=>s.installed).length);
2794
- document.getElementById('skillsPublicCount').textContent=formatNum(data.skills.filter(s=>s.visibility==='public').length);
4617
+ const localRes=await fetch('/api/skills?'+params.toString());
4618
+ const localData=await localRes.json();
4619
+ let localSkills=Array.isArray(localData.skills)?localData.skills:[];
4620
+ if(query){
4621
+ const q=query.toLowerCase();
4622
+ localSkills=localSkills.filter(skill=>{
4623
+ const haystack=[skill.name,skill.description,skill.tags].filter(Boolean).join(' ').toLowerCase();
4624
+ return haystack.includes(q);
4625
+ });
4626
+ }
4627
+
4628
+ const renderLocalCards=function(skills){
4629
+ if(!skills||skills.length===0){
4630
+ return '<div style="text-align:center;padding:48px;color:var(--text-muted);font-size:14px">'+(query?t('skills.search.noresult'):t('skills.empty'))+'</div>';
4631
+ }
4632
+ return skills.map(skill=>{
4633
+ const timeStr=formatTime(skill.createdAt);
4634
+ const tags=parseTags(skill.tags);
4635
+ const installedClass=skill.installed?'installed':'';
4636
+ const statusClass=skill.status==='archived'?'archived':(skill.status==='draft'?'draft':'');
4637
+ const sourceLabel=tags.includes('hub-import')?'hub-import':skill.sourceType;
4638
+ const qs=skill.qualityScore;
4639
+ const qsBadge=qs!==null&&qs!==undefined?'<span class="skill-badge quality '+(qs>=7?'high':qs>=5?'mid':'low')+'">★ '+qs.toFixed(1)+'</span>':'';
4640
+ const visBadge=skill.visibility==='public'?'<span class="skill-badge visibility-public">🌐 '+t('skills.visibility.public')+'</span>':'';
4641
+ return '<div class="skill-card '+installedClass+' '+statusClass+'" onclick="openSkillDetail(&quot;'+escAttr(skill.id)+'&quot;)">'+
4642
+ '<div class="skill-card-top">'+
4643
+ '<div class="skill-card-name">🧠 '+esc(skill.name)+'</div>'+
4644
+ '<div class="skill-card-badges">'+
4645
+ qsBadge+
4646
+ '<span class="skill-badge version">v'+skill.version+'</span>'+
4647
+ visBadge+
4648
+ (skill.installed?'<span class="skill-badge installed">'+t('skills.installed.badge')+'</span>':'')+
4649
+ '<span class="skill-badge status-'+skill.status+'">'+t('skills.status.'+skill.status)+'</span>'+
4650
+ '</div>'+
4651
+ '</div>'+
4652
+ '<div class="skill-card-desc">'+esc(skill.description)+'</div>'+
4653
+ '<div class="skill-card-bottom">'+
4654
+ '<span class="tag"><span class="icon">📅</span> '+timeStr+'</span>'+
4655
+ '<span class="tag"><span class="icon">📦</span> '+sourceLabel+'</span>'+
4656
+ (tags.length>0?'<div class="skill-card-tags">'+tags.map(t=>'<span class="skill-tag">'+esc(t)+'</span>').join('')+'</div>':'')+
4657
+ '</div>'+
4658
+ '</div>';
4659
+ }).join('');
4660
+ };
4661
+
4662
+ list.innerHTML=renderLocalCards(localSkills);
4663
+
4664
+ if(skillSearchScope==='local'){
4665
+ if(hubSection) hubSection.style.display='none';
4666
+ document.getElementById('skillSearchMeta').textContent=query?(t('skills.search.local')+' '+localSkills.length):'';
4667
+ document.getElementById('skillsTotalCount').textContent=formatNum(localSkills.length);
4668
+ document.getElementById('skillsActiveCount').textContent=formatNum(localSkills.filter(s=>s.status==='active').length);
4669
+ document.getElementById('skillsDraftCount').textContent=formatNum(localSkills.filter(s=>s.status==='draft').length);
4670
+ document.getElementById('skillsInstalledCount').textContent=formatNum(localSkills.filter(s=>s.installed).length);
4671
+ document.getElementById('skillsPublicCount').textContent=formatNum(localSkills.filter(s=>s.visibility==='public').length);
4672
+ return;
4673
+ }
2795
4674
 
2796
- if(!data.skills||data.skills.length===0){
2797
- list.innerHTML='<div style="text-align:center;padding:48px;color:var(--text-muted);font-size:14px">'+t('skills.empty')+'</div>';
4675
+ if(!query){
4676
+ if(hubSection) hubSection.style.display='block';
4677
+ if(hubList){ loadHubSkills(hubList); }
4678
+ document.getElementById('skillSearchMeta').textContent=t('skills.search.local')+' '+localSkills.length;
4679
+ document.getElementById('skillsTotalCount').textContent=formatNum(localSkills.length);
4680
+ document.getElementById('skillsActiveCount').textContent=formatNum(localSkills.filter(s=>s.status==='active').length);
4681
+ document.getElementById('skillsDraftCount').textContent=formatNum(localSkills.filter(s=>s.status==='draft').length);
4682
+ document.getElementById('skillsInstalledCount').textContent=formatNum(localSkills.filter(s=>s.installed).length);
4683
+ document.getElementById('skillsPublicCount').textContent=formatNum(localSkills.filter(s=>s.visibility==='public').length);
2798
4684
  return;
2799
4685
  }
2800
4686
 
2801
- list.innerHTML=data.skills.map(skill=>{
2802
- const timeStr=formatTime(skill.createdAt);
2803
- const tags=parseTags(skill.tags);
2804
- const installedClass=skill.installed?'installed':'';
2805
- const statusClass=skill.status==='archived'?'archived':(skill.status==='draft'?'draft':'');
2806
- const qs=skill.qualityScore;
2807
- const qsBadge=qs!==null&&qs!==undefined?'<span class="skill-badge quality '+(qs>=7?'high':qs>=5?'mid':'low')+'">\\u2605 '+qs.toFixed(1)+'</span>':'';
2808
- const visBadge=skill.visibility==='public'?'<span class="skill-badge visibility-public">\\u{1F310} '+t('skills.visibility.public')+'</span>':'';
2809
- return '<div class="skill-card '+installedClass+' '+statusClass+'" onclick="openSkillDetail(\\''+skill.id+'\\')">'+
2810
- '<div class="skill-card-top">'+
2811
- '<div class="skill-card-name">\\u{1F9E0} '+esc(skill.name)+'</div>'+
2812
- '<div class="skill-card-badges">'+
2813
- qsBadge+
2814
- '<span class="skill-badge version">v'+skill.version+'</span>'+
2815
- visBadge+
2816
- (skill.installed?'<span class="skill-badge installed">'+t('skills.installed.badge')+'</span>':'')+
2817
- '<span class="skill-badge status-'+skill.status+'">'+t('skills.status.'+skill.status)+'</span>'+
4687
+ const sharingParams=new URLSearchParams();
4688
+ sharingParams.set('query',query);
4689
+ sharingParams.set('scope',skillSearchScope);
4690
+ sharingParams.set('maxResults','20');
4691
+ const r=await fetch('/api/sharing/search/skills?'+sharingParams.toString());
4692
+ const data=await r.json();
4693
+ const localHits=(data.local&&Array.isArray(data.local.hits))?data.local.hits:[];
4694
+ const hubHits=(data.hub&&Array.isArray(data.hub.hits))?data.hub.hits:[];
4695
+
4696
+ list.innerHTML=localHits.length?localHits.map(function(skill){
4697
+ return '<div class="hub-skill-card" onclick="openSkillDetail(&quot;'+escAttr(skill.skillId)+'&quot;)">'+
4698
+ '<div class="summary">'+esc(skill.name)+'</div>'+
4699
+ '<div class="excerpt">'+esc(skill.description||'')+'</div>'+
4700
+ '<div class="hub-skill-meta"><span class="meta-chip">visibility: '+esc(skill.visibility||'private')+'</span><span class="meta-chip">owner: '+esc(skill.owner||'agent:main')+'</span></div>'+
4701
+ '</div>';
4702
+ }).join(''):'<div style="text-align:center;padding:48px;color:var(--text-muted);font-size:14px">'+t('skills.search.noresult')+'</div>';
4703
+
4704
+ if(hubList){
4705
+ if(hubSection) hubSection.style.display=hubHits.length?'block':'none';
4706
+ hubList.innerHTML=hubHits.length?hubHits.map(function(skill){
4707
+ return '<div class="hub-skill-card">'+
4708
+ '<div class="summary">'+esc(skill.name)+'</div>'+
4709
+ '<div class="excerpt">'+esc(skill.description||'')+'</div>'+
4710
+ '<div class="hub-skill-meta">'+
4711
+ '<span class="meta-chip">owner: '+esc(skill.ownerName||'unknown')+'</span>'+
4712
+ (skill.groupName?'<span class="meta-chip">group: '+esc(skill.groupName)+'</span>':'')+
4713
+ '<span class="meta-chip">visibility: '+esc(skill.visibility||'hub')+'</span>'+
4714
+ (skill.version!=null?'<span class="meta-chip">v'+skill.version+'</span>':'')+
2818
4715
  '</div>'+
4716
+ '<div class="hub-skill-actions"><button class="btn btn-sm" onclick="event.stopPropagation();pullHubSkill(&quot;'+escAttr(skill.skillId)+'&quot;)">Pull to Local</button></div>'+
4717
+ '</div>';
4718
+ }).join(''):'';
4719
+ }
4720
+
4721
+ document.getElementById('skillSearchMeta').textContent=t('skills.search.local')+' '+localHits.length+(hubHits.length?' · Hub '+hubHits.length:'');
4722
+ document.getElementById('skillsTotalCount').textContent=formatNum(localHits.length+hubHits.length);
4723
+ document.getElementById('skillsActiveCount').textContent=formatNum(localHits.length);
4724
+ document.getElementById('skillsDraftCount').textContent='0';
4725
+ document.getElementById('skillsInstalledCount').textContent='-';
4726
+ document.getElementById('skillsPublicCount').textContent=formatNum(hubHits.filter(function(s){return s.visibility==='public';}).length);
4727
+ }catch(e){
4728
+ list.innerHTML='<div style="text-align:center;padding:24px;color:var(--rose)">'+t('skills.load.error')+': '+esc(String(e))+'</div>';
4729
+ if(hubList){
4730
+ hubList.innerHTML='<div style="text-align:center;padding:24px;color:var(--rose)">'+t('skills.load.error')+'</div>';
4731
+ }
4732
+ }
4733
+ }
4734
+
4735
+ async function loadHubSkills(hubList){
4736
+ if(!hubList) hubList=document.getElementById('hubSkillsList');
4737
+ if(!hubList) return;
4738
+ var hubSection=document.getElementById('hubSkillsSection');
4739
+ hubList.innerHTML='<div class="spinner"></div>';
4740
+ try{
4741
+ const r=await fetch('/api/sharing/skills/list?limit=40');
4742
+ const d=await r.json();
4743
+ const skills=Array.isArray(d.skills)?d.skills:[];
4744
+ hubSkillsCache=skills;
4745
+ if(!skills.length){
4746
+ if(hubSection) hubSection.style.display='none';
4747
+ return;
4748
+ }
4749
+ if(hubSection) hubSection.style.display='block';
4750
+ hubList.innerHTML=skills.map(function(skill,idx){
4751
+ return '<div class="hub-skill-card" onclick="openHubSkillDetailFromCache(\\\'hub\\\',' +idx+')" style="cursor:pointer">'+
4752
+ '<div class="summary">'+esc(skill.name)+'</div>'+
4753
+ '<div class="excerpt">'+esc(skill.description||'')+'</div>'+
4754
+ '<div class="hub-skill-meta">'+
4755
+ '<span class="meta-chip">owner: '+esc(skill.ownerName||'unknown')+'</span>'+
4756
+ (skill.groupName?'<span class="meta-chip">group: '+esc(skill.groupName)+'</span>':'')+
4757
+ '<span class="meta-chip">visibility: '+esc(skill.visibility||'hub')+'</span>'+
4758
+ (skill.version!=null?'<span class="meta-chip">v'+skill.version+'</span>':'')+
2819
4759
  '</div>'+
2820
- '<div class="skill-card-desc">'+esc(skill.description)+'</div>'+
2821
- '<div class="skill-card-bottom">'+
2822
- '<span class="tag"><span class="icon">\\u{1F4C5}</span> '+timeStr+'</span>'+
2823
- '<span class="tag"><span class="icon">\\u{1F4E6}</span> '+skill.sourceType+'</span>'+
2824
- (tags.length>0?'<div class="skill-card-tags">'+tags.map(t=>'<span class="skill-tag">'+esc(t)+'</span>').join('')+'</div>':'')+
2825
- '</div>'+
2826
- '<div class="card-actions" onclick="event.stopPropagation()">'+
2827
- '<button class="btn btn-sm btn-ghost" onclick="openSkillDetail(\\''+skill.id+'\\')">'+t('card.expand')+'</button>'+
2828
- (skill.visibility==='public'?'<button class="btn btn-sm btn-ghost" onclick="toggleSkillPublic(\\''+skill.id+'\\',false)">\\u{1F512} '+t('skills.setPrivate')+'</button>':'<button class="btn btn-sm btn-ghost" onclick="toggleSkillPublic(\\''+skill.id+'\\',true)">\\u{1F310} '+t('skills.setPublic')+'</button>')+
2829
- '<button class="btn btn-sm btn-ghost" style="color:var(--accent)" onclick="deleteSkill(\\''+skill.id+'\\')">'+t('skill.delete')+'</button>'+
2830
- '</div>'+
4760
+ '<div class="hub-skill-actions"><button class="btn btn-sm" onclick="event.stopPropagation();pullHubSkill(\\''+escAttr(skill.sourceSkillId)+'\\')">Pull to Local</button></div>'+
2831
4761
  '</div>';
2832
4762
  }).join('');
2833
4763
  }catch(e){
2834
- list.innerHTML='<div style="text-align:center;padding:24px;color:var(--rose)">Failed to load skills: '+esc(String(e))+'</div>';
4764
+ if(hubSection) hubSection.style.display='none';
4765
+ hubList.innerHTML='';
2835
4766
  }
2836
4767
  }
2837
4768
 
@@ -2840,6 +4771,7 @@ function parseTags(tagsStr){
2840
4771
  }
2841
4772
 
2842
4773
  let currentSkillId='';
4774
+ let currentSkillDetail=null;
2843
4775
 
2844
4776
  async function openSkillDetail(skillId){
2845
4777
  currentSkillId=skillId;
@@ -2852,6 +4784,8 @@ async function openSkillDetail(skillId){
2852
4784
  document.getElementById('skillDetailContent').innerHTML='<div class="spinner"></div>';
2853
4785
  document.getElementById('skillVersionsList').innerHTML='<div class="spinner"></div>';
2854
4786
  document.getElementById('skillRelatedTasks').innerHTML='';
4787
+ var vb=document.getElementById('skillVisibilityBtn');if(vb)vb.style.display='';
4788
+ var db=document.getElementById('skillDownloadBtn');if(db)db.style.display='';
2855
4789
  document.getElementById('skillDetailActions').innerHTML='';
2856
4790
 
2857
4791
  try{
@@ -2897,6 +4831,8 @@ async function openSkillDetail(skillId){
2897
4831
  }
2898
4832
 
2899
4833
  document.getElementById('skillDetailDesc').textContent=skill.description;
4834
+ currentSkillDetail=skill;
4835
+ renderSkillShareActions(skill);
2900
4836
 
2901
4837
  if(files.length>0){
2902
4838
  const fileIcons={'skill':'\\u{1F4D6}','script':'\\u{2699}','reference':'\\u{1F4CE}','file':'\\u{1F4C4}'};
@@ -3011,7 +4947,14 @@ function classifyError(msg){
3011
4947
  if(msg.indexOf('ECONNREFUSED')>=0) return 'Connection refused';
3012
4948
  if(msg.indexOf('ENOTFOUND')>=0) return 'DNS resolution failed';
3013
4949
  if(msg.indexOf('403')>=0) return 'Forbidden (403)';
3014
- return msg;
4950
+ if(msg.indexOf('503')>=0||msg.indexOf('upstream connect error')>=0||msg.indexOf('Service Unavailable')>=0) return 'Service unavailable (503)';
4951
+ if(msg.indexOf('502')>=0||msg.indexOf('Bad Gateway')>=0) return 'Bad gateway (502)';
4952
+ if(msg.indexOf('500')>=0||msg.indexOf('Internal Server Error')>=0) return 'Server error (500)';
4953
+ if(msg.indexOf('404')>=0||msg.indexOf('Not Found')>=0) return 'Not found (404)';
4954
+ if(msg.indexOf('fetch failed')>=0||msg.indexOf('ETIMEDOUT')>=0) return 'Network error';
4955
+ if(msg.indexOf('Unknown')>=0&&msg.indexOf('provider')>=0) return 'Unknown provider';
4956
+ var m=msg.match(/\((\d{3})\)/); if(m) return 'HTTP error ('+m[1]+')';
4957
+ return msg.length>80?msg.substring(0,77)+'...':msg;
3015
4958
  }
3016
4959
 
3017
4960
  function shortenModel(s){return s?s.replace('openai_compatible/','').replace('openai/',''):'\u2014';}
@@ -3057,7 +5000,7 @@ async function loadModelHealth(){
3057
5000
  issue+=shortErr;
3058
5001
  if(m.consecutiveErrors>1) issue+=' ('+m.consecutiveErrors+'x)';
3059
5002
  }
3060
- if(issue) h+='<td><span class="mh-err-text" title="'+escapeHtml(m.lastErrorMessage||'')+'">'+escapeHtml(issue)+'</span></td>';
5003
+ if(issue) h+='<td><span class="mh-err-text" data-err="'+escapeHtml(m.lastErrorMessage||'')+'">'+escapeHtml(issue)+'</span></td>';
3061
5004
  else h+='<td><span style="color:var(--text-muted);font-size:11px">\u2014</span></td>';
3062
5005
 
3063
5006
  h+='<td style="text-align:right"><span class="mh-time">'+(ago||'\u2014')+'</span></td>';
@@ -3065,11 +5008,29 @@ async function loadModelHealth(){
3065
5008
  }
3066
5009
  h+='</tbody></table>';
3067
5010
  bar.innerHTML=h;
5011
+ initMhTooltips();
3068
5012
  }catch(e){
3069
5013
  bar.innerHTML='<div class="mh-empty">Failed to load model health</div>';
3070
5014
  }
3071
5015
  }
3072
5016
 
5017
+ function initMhTooltips(){
5018
+ var tip=document.getElementById('mhTooltip');
5019
+ if(!tip){tip=document.createElement('div');tip.id='mhTooltip';document.body.appendChild(tip);}
5020
+ document.querySelectorAll('.mh-err-text[data-err]').forEach(function(el){
5021
+ el.addEventListener('mouseenter',function(e){
5022
+ var msg=el.getAttribute('data-err');
5023
+ if(!msg)return;
5024
+ tip.textContent=msg;
5025
+ tip.style.display='block';
5026
+ var rect=el.getBoundingClientRect();
5027
+ tip.style.left=Math.max(0,Math.min(rect.left,window.innerWidth-490))+'px';
5028
+ tip.style.top=(rect.bottom+6)+'px';
5029
+ });
5030
+ el.addEventListener('mouseleave',function(){tip.style.display='none';});
5031
+ });
5032
+ }
5033
+
3073
5034
  function timeAgo(ts){
3074
5035
  var diff=Date.now()-ts;
3075
5036
  if(diff<60000) return 'just now';
@@ -3079,6 +5040,9 @@ function timeAgo(ts){
3079
5040
  }
3080
5041
 
3081
5042
  /* ─── Settings / Config ─── */
5043
+ function syncHostToggles(){}
5044
+ function onProviderChange(){}
5045
+
3082
5046
  async function loadConfig(){
3083
5047
  try{
3084
5048
  const r=await fetch('/api/config');
@@ -3113,6 +5077,26 @@ async function loadConfig(){
3113
5077
 
3114
5078
  const tel=cfg.telemetry||{};
3115
5079
  document.getElementById('cfgTelemetryEnabled').checked=tel.enabled!==false;
5080
+
5081
+ const sharing=cfg.sharing||{};
5082
+ const caps=sharing.capabilities||{};
5083
+ const embProv=(cfg.embedding||{}).provider;
5084
+ const sumProv=(cfg.summarizer||{}).provider;
5085
+ const skProv=((cfg.skillEvolution||{}).summarizer||{}).provider;
5086
+
5087
+
5088
+ document.getElementById('cfgSharingEnabled').checked=!!sharing.enabled;
5089
+ _sharingRole=sharing.role||'client';
5090
+ var hub=sharing.hub||{};
5091
+ var client=sharing.client||{};
5092
+ document.getElementById('cfgHubPort').value=hub.port||18800;
5093
+ document.getElementById('cfgHubTeamName').value=hub.teamName||'';
5094
+ document.getElementById('cfgHubTeamToken').value=hub.teamToken||'';
5095
+ document.getElementById('cfgClientHubAddress').value=client.hubAddress||'';
5096
+ document.getElementById('cfgClientTeamToken').value=client.teamToken||'';
5097
+ document.getElementById('cfgClientUserToken').value=client.userToken||'';
5098
+ onSharingToggle();
5099
+ updateHubShareInfo();
3116
5100
  }catch(e){
3117
5101
  console.error('loadConfig error',e);
3118
5102
  }
@@ -3194,6 +5178,30 @@ async function saveConfig(){
3194
5178
 
3195
5179
  function done(){saveBtn.disabled=false;saveBtn.textContent=t('settings.save');}
3196
5180
 
5181
+ var sharingEnabled=document.getElementById('cfgSharingEnabled').checked;
5182
+ cfg.sharing={
5183
+ enabled:sharingEnabled,
5184
+ role:_sharingRole,
5185
+ capabilities:{}
5186
+ };
5187
+ if(sharingEnabled&&_sharingRole==='hub'){
5188
+ var hubPort=document.getElementById('cfgHubPort').value.trim();
5189
+ var hubTeamName=document.getElementById('cfgHubTeamName').value.trim();
5190
+ var hubTeamToken=document.getElementById('cfgHubTeamToken').value.trim();
5191
+ cfg.sharing.hub={port:hubPort?Number(hubPort):18800};
5192
+ if(hubTeamName) cfg.sharing.hub.teamName=hubTeamName;
5193
+ if(hubTeamToken) cfg.sharing.hub.teamToken=hubTeamToken;
5194
+ }
5195
+ if(sharingEnabled&&_sharingRole==='client'){
5196
+ var clientAddr=document.getElementById('cfgClientHubAddress').value.trim();
5197
+ var clientTeamToken=document.getElementById('cfgClientTeamToken').value.trim();
5198
+ var clientUserToken=document.getElementById('cfgClientUserToken').value.trim();
5199
+ cfg.sharing.client={};
5200
+ if(clientAddr) cfg.sharing.client.hubAddress=clientAddr;
5201
+ if(clientTeamToken) cfg.sharing.client.teamToken=clientTeamToken;
5202
+ if(clientUserToken) cfg.sharing.client.userToken=clientUserToken;
5203
+ }
5204
+
3197
5205
  // 1) Embedding model is required
3198
5206
  if(!embP||embP===''){done();toast(t('settings.save.emb.required'),'error');return;}
3199
5207
 
@@ -3250,6 +5258,10 @@ async function saveConfig(){
3250
5258
  el.classList.add('show');
3251
5259
  setTimeout(()=>el.classList.remove('show'),2500);
3252
5260
  toast(t('settings.saved'),'success');
5261
+ if(sharingEnabled){
5262
+ updateHubShareInfo();
5263
+ setTimeout(function(){alert(t('settings.hub.restartAlert'));},300);
5264
+ }
3253
5265
  }catch(e){
3254
5266
  toast(t('settings.save.fail')+': '+e.message,'error');
3255
5267
  }finally{done();}
@@ -3353,7 +5365,7 @@ function formatDuration(ms){
3353
5365
 
3354
5366
  function formatTime(ts){
3355
5367
  if(!ts) return '-';
3356
- return new Date(ts).toLocaleString('zh-CN',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'});
5368
+ return new Date(ts).toLocaleString(dateLoc(),{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'});
3357
5369
  }
3358
5370
 
3359
5371
  function fillDays(rows,days){
@@ -3606,16 +5618,18 @@ function renderToolAgg(data){
3606
5618
 
3607
5619
  /* ─── Data loading ─── */
3608
5620
  async function loadAll(){
3609
- await Promise.all([loadStats(),loadMemories()]);
5621
+ await Promise.all([loadStats(),loadMemories(),loadSharingStatus(false)]);
3610
5622
  checkMigrateStatus();
3611
5623
  connectPPSSE();
3612
5624
  checkForUpdate();
3613
5625
  }
3614
5626
 
3615
- async function loadStats(){
5627
+ async function loadStats(ownerFilter){
3616
5628
  let d;
3617
5629
  try{
3618
- const r=await fetch('/api/stats');
5630
+ var statsUrl='/api/stats';
5631
+ if(ownerFilter) statsUrl+='?owner='+encodeURIComponent(ownerFilter);
5632
+ const r=await fetch(statsUrl);
3619
5633
  d=await r.json();
3620
5634
  }catch(e){ d={}; }
3621
5635
  if(!d||typeof d!=='object') d={};
@@ -3671,6 +5685,17 @@ async function loadStats(){
3671
5685
  sl.innerHTML+='<div class="session-item'+(isActive?' active':'')+'" onclick="filterSession(\\''+s.session_key.replace(/'/g,"\\\\'")+'\\')"><span title="'+s.session_key+'">'+name+'</span><span class="count">'+s.count+'</span></div>';
3672
5686
  });
3673
5687
 
5688
+ const fSel=document.getElementById('filterSession');
5689
+ if(fSel){
5690
+ const curVal=activeSession||'';
5691
+ var sessionCount=(d.sessions||[]).length;
5692
+ fSel.innerHTML='<option value="">'+t('filter.allsessions')+' ('+sessionCount+')</option>';
5693
+ (d.sessions||[]).forEach(s=>{
5694
+ const sName=s.session_key.length>30?s.session_key.slice(0,12)+'...'+s.session_key.slice(-10):s.session_key;
5695
+ fSel.innerHTML+='<option value="'+s.session_key.replace(/"/g,'&quot;')+'"'+(s.session_key===curVal?' selected':'')+'>'+sName+' ('+s.count+')</option>';
5696
+ });
5697
+ }
5698
+
3674
5699
  const ownerSel=document.getElementById('filterOwner');
3675
5700
  if(ownerSel && d.owners && d.owners.length>0){
3676
5701
  const curVal=ownerSel.value;
@@ -3682,6 +5707,32 @@ async function loadStats(){
3682
5707
  }
3683
5708
  }
3684
5709
 
5710
+ function onOwnerFilterChange(){
5711
+ var owner=document.getElementById('filterOwner').value;
5712
+ activeSession=null;
5713
+ currentPage=1;
5714
+ refreshSessionDropdown(owner);
5715
+ applyFilters();
5716
+ }
5717
+
5718
+ async function refreshSessionDropdown(ownerFilter){
5719
+ try{
5720
+ var statsUrl='/api/stats';
5721
+ if(ownerFilter) statsUrl+='?owner='+encodeURIComponent(ownerFilter);
5722
+ var r=await fetch(statsUrl);
5723
+ var d=await r.json();
5724
+ var sessions=d.sessions||[];
5725
+ var fSel=document.getElementById('filterSession');
5726
+ if(fSel){
5727
+ fSel.innerHTML='<option value="">'+t('filter.allsessions')+' ('+sessions.length+')</option>';
5728
+ sessions.forEach(function(s){
5729
+ var sName=s.session_key.length>30?s.session_key.slice(0,12)+'...'+s.session_key.slice(-10):s.session_key;
5730
+ fSel.innerHTML+='<option value="'+s.session_key.replace(/"/g,'&quot;')+'">'+sName+' ('+s.count+')</option>';
5731
+ });
5732
+ }
5733
+ }catch(e){}
5734
+ }
5735
+
3685
5736
  function getFilterParams(){
3686
5737
  const p=new URLSearchParams();
3687
5738
  if(activeSession) p.set('session',activeSession);
@@ -3720,22 +5771,16 @@ async function loadMemories(page){
3720
5771
  }
3721
5772
  }
3722
5773
 
3723
- async function doSearch(q){
3724
- if(!q.trim()){currentPage=1;loadMemories();return}
5774
+ async function loadHubMemories(){
3725
5775
  const list=document.getElementById('memoryList');
3726
5776
  list.innerHTML='<div class="spinner"></div>';
3727
5777
  try{
3728
- const p=getFilterParams();
3729
- p.set('q',q);
3730
- const r=await fetch('/api/search?'+p.toString());
5778
+ const r=await fetch('/api/sharing/memories/list?limit='+PAGE_SIZE);
3731
5779
  const d=await r.json();
3732
- const total=d.total||0;
3733
- const meta=[];
3734
- if(d.vectorCount>0) meta.push(d.vectorCount+t('search.meta.semantic'));
3735
- if(d.ftsCount>0) meta.push(d.ftsCount+t('search.meta.text'));
3736
- meta.push(total+t('search.meta.results'));
3737
- document.getElementById('searchMeta').textContent=meta.join(' \\u00B7 ');
3738
- renderMemories(d.results||[]);
5780
+ const items=d.memories||[];
5781
+ document.getElementById('searchMeta').textContent=items.length+t('search.meta.total');
5782
+ document.getElementById('sharingSearchMeta').textContent='';
5783
+ renderMemories(items);
3739
5784
  document.getElementById('pagination').innerHTML='';
3740
5785
  }catch(e){
3741
5786
  document.getElementById('searchMeta').textContent='0'+t('search.meta.results');
@@ -3744,6 +5789,51 @@ async function doSearch(q){
3744
5789
  }
3745
5790
  }
3746
5791
 
5792
+ async function doSearch(query){
5793
+ query=(query||'').trim();
5794
+ if(!query){loadMemories();return;}
5795
+ var scope=document.getElementById('memorySearchScope')?.value||memorySearchScope||'local';
5796
+ var list=document.getElementById('memoryList');
5797
+ list.innerHTML='<div class="spinner"></div>';
5798
+ if(scope!=='local'){
5799
+ try{
5800
+ var r=await fetch('/api/sharing/search/memories',{
5801
+ method:'POST',
5802
+ headers:{'Content-Type':'application/json'},
5803
+ body:JSON.stringify({query:query,scope:scope,maxResults:20,role:activeRole||undefined})
5804
+ });
5805
+ var data=await r.json();
5806
+ renderSharingMemorySearchResults(data,query);
5807
+ }catch(e){
5808
+ document.getElementById('searchMeta').textContent='0'+t('search.meta.results');
5809
+ document.getElementById('sharingSearchMeta').textContent='';
5810
+ renderMemories([]);
5811
+ document.getElementById('pagination').innerHTML='';
5812
+ }
5813
+ } else {
5814
+ try{
5815
+ var p=getFilterParams();
5816
+ p.set('q',query);
5817
+ var r=await fetch('/api/search?'+p.toString());
5818
+ var d=await r.json();
5819
+ var total=d.total||0;
5820
+ var meta=[];
5821
+ if(d.vectorCount>0) meta.push(d.vectorCount+t('search.meta.semantic'));
5822
+ if(d.ftsCount>0) meta.push(d.ftsCount+t('search.meta.text'));
5823
+ meta.push(total+t('search.meta.results'));
5824
+ document.getElementById('searchMeta').textContent=meta.join(' \u00B7 ');
5825
+ document.getElementById('sharingSearchMeta').textContent='';
5826
+ renderMemories(d.results||[]);
5827
+ document.getElementById('pagination').innerHTML='';
5828
+ }catch(e){
5829
+ document.getElementById('searchMeta').textContent='0'+t('search.meta.results');
5830
+ document.getElementById('sharingSearchMeta').textContent='';
5831
+ renderMemories([]);
5832
+ document.getElementById('pagination').innerHTML='';
5833
+ }
5834
+ }
5835
+ }
5836
+
3747
5837
  function debounceSearch(){
3748
5838
  clearTimeout(searchTimer);
3749
5839
  searchTimer=setTimeout(()=>doSearch(document.getElementById('searchInput').value),350);
@@ -3752,6 +5842,12 @@ function debounceSearch(){
3752
5842
  function filterSession(key){
3753
5843
  activeSession=key;
3754
5844
  currentPage=1;
5845
+ var fSel=document.getElementById('filterSession');
5846
+ if(fSel) fSel.value=key||'';
5847
+ document.querySelectorAll('#sessionList .session-item').forEach(function(el,i){
5848
+ if(i===0) el.classList.toggle('active',!key);
5849
+ else el.classList.toggle('active',el.querySelector('span')?.title===key);
5850
+ });
3755
5851
  loadAll();
3756
5852
  }
3757
5853
 
@@ -3787,7 +5883,7 @@ function renderMemories(items){
3787
5883
  }
3788
5884
  items.forEach(m=>{memoryCache[m.id]=m});
3789
5885
  list.innerHTML=items.map(m=>{
3790
- const time=m.created_at?new Date(typeof m.created_at==='number'?m.created_at:m.created_at).toLocaleString('zh-CN'):'';
5886
+ const time=m.created_at?new Date(typeof m.created_at==='number'?m.created_at:m.created_at).toLocaleString(dateLoc()):'';
3791
5887
  const role=m.role||'user';
3792
5888
  const rawSummary=m.summary||'';
3793
5889
  const rawContent=m.content||'';
@@ -3799,7 +5895,7 @@ function renderMemories(items){
3799
5895
  const mc=m.merge_count||0;
3800
5896
  const cardTitle=esc(rawSummary||rawContent||'');
3801
5897
  const mergeBadge=mc>0?'<span class="merge-badge">\\u{1F504} '+t('card.evolved')+' '+mc+t('card.times')+'</span>':'';
3802
- const updatedAt=(m.updated_at&&m.updated_at>m.created_at)?'<span class="card-updated">'+t('card.updated')+' '+new Date(m.updated_at).toLocaleString('zh-CN')+'</span>':'';
5898
+ const updatedAt=(m.updated_at&&m.updated_at>m.created_at)?'<span class="card-updated">'+t('card.updated')+' '+new Date(m.updated_at).toLocaleString(dateLoc())+'</span>':'';
3803
5899
  const ds=m.dedup_status||'active';
3804
5900
  const isInactive=ds==='merged';
3805
5901
  const dedupBadge=ds==='duplicate'?'<span class="dedup-badge duplicate">'+t('card.dedupDuplicate')+'</span>':ds==='merged'?'<span class="dedup-badge merged">'+t('card.dedupMerged')+'</span>':'';
@@ -3808,6 +5904,8 @@ function renderMemories(items){
3808
5904
  const ownerVal=m.owner||'agent:main';
3809
5905
  const isPublicMem=ownerVal==='public';
3810
5906
  const ownerBadge=isPublicMem?'<span class="owner-badge public">\\u{1F310} '+t('filter.public')+'</span>':'<span class="owner-badge agent">\\u{1F512} '+t('filter.private')+'</span>';
5907
+ const memShared=m.sharingVisibility||null;
5908
+ const memShareBadge=memShared?'<span style="display:inline-flex;align-items:center;gap:3px;padding:2px 8px;background:#22c55e22;border:1px solid #22c55e44;border-radius:10px;font-size:11px;color:#22c55e">\\u2713 '+(memShared==='group'?t('share.group'):t('share.public'))+'</span>':'';
3811
5909
  let dedupInfo='';
3812
5910
  if(ds==='duplicate'||ds==='merged'){
3813
5911
  const reason=m.dedup_reason?'<span style="font-size:11px;color:var(--text-muted)">'+t('card.dedupReason')+esc(m.dedup_reason)+'</span>':'';
@@ -3821,7 +5919,7 @@ function renderMemories(items){
3821
5919
  if(hist.length>0){
3822
5920
  historyHtml='<div class="merge-history" id="history-'+id+'" style="display:none"><div style="font-weight:600;margin-bottom:8px;font-size:12px">'+t('card.evolveHistory')+' ('+hist.length+')</div>';
3823
5921
  hist.forEach(function(h){
3824
- const ht=h.at?new Date(h.at).toLocaleString('zh-CN'):'';
5922
+ const ht=h.at?new Date(h.at).toLocaleString(dateLoc()):'';
3825
5923
  historyHtml+='<div class="merge-history-item"><span class="merge-action '+h.action+'">'+h.action+'</span> <span style="color:var(--text-muted)">'+ht+'</span><br>'+esc(h.reason||'');
3826
5924
  if(h.from) historyHtml+='<br><span style="opacity:.6">'+t('card.oldSummary')+':</span> '+esc(h.from);
3827
5925
  if(h.to) historyHtml+='<br><span style="opacity:.6">'+t('card.newSummary')+':</span> '+esc(h.to);
@@ -3857,6 +5955,7 @@ function renderMemories(items){
3857
5955
  (mc>0?'<button class="btn btn-sm btn-ghost" onclick="toggleHistory(\\''+id+'\\')">'+t('card.evolveHistory')+'</button>':'')+
3858
5956
  '<button class="btn btn-sm btn-ghost" onclick="openEditModal(\\''+id+'\\')">'+t('card.edit')+'</button>'+
3859
5957
  (isPublicMem?'<button class="btn btn-sm btn-ghost" onclick="toggleMemoryPublic(\\''+id+'\\',false)">\\u{1F512} '+t('skills.setPrivate')+'</button>':'<button class="btn btn-sm btn-ghost mem-public-btn" onclick="toggleMemoryPublic(\\''+id+'\\',true)">\\u{1F310} '+t('skills.setPublic')+'</button>')+
5958
+ (memShared?'<button class="btn btn-sm btn-ghost" onclick="unshareMemory(\\''+id+'\\')">'+t('share.unshareBtn')+'</button>':'<button class="btn btn-sm btn-ghost" onclick="shareMemoryPrompt(\\''+id+'\\')">'+t('share.shareBtn')+'</button>')+
3860
5959
  '<button class="btn btn-sm btn-ghost" style="color:var(--accent)" onclick="deleteMemory(\\''+id+'\\')">'+t('card.delete')+'</button>'+
3861
5960
  vscore+
3862
5961
  '</div></div>';
@@ -3928,8 +6027,8 @@ async function showMemoryModal(chunkId){
3928
6027
  const role=(m.role||'unknown').toUpperCase();
3929
6028
  const roleCls=(m.role||'').toLowerCase();
3930
6029
  const ds=m.dedup_status||'active';
3931
- const time=new Date(m.created_at).toLocaleString('zh-CN');
3932
- const updated=m.updated_at?new Date(m.updated_at).toLocaleString('zh-CN'):'';
6030
+ const time=new Date(m.created_at).toLocaleString(dateLoc());
6031
+ const updated=m.updated_at?new Date(m.updated_at).toLocaleString(dateLoc()):'';
3933
6032
  let html='<div class="modal-memory-card">';
3934
6033
  html+='<div class="modal-header-row"><span class="role-tag '+roleCls+'">'+role+'</span>';
3935
6034
  if(ds!=='active') html+='<span class="dedup-badge '+(ds==='duplicate'?'duplicate':'merged')+'">'+ds+'</span>';
@@ -4666,35 +6765,35 @@ function waitForGatewayAndReload(maxAttempts,attempt){
4666
6765
  attempt=attempt||0;
4667
6766
  if(attempt>=maxAttempts){window.location.reload();return;}
4668
6767
  setTimeout(function(){
4669
- fetch('/api/update-check').then(function(r){
4670
- if(r.ok) window.location.reload();
4671
- else waitForGatewayAndReload(maxAttempts,attempt+1);
6768
+ fetch('/api/auth/status').then(function(){
6769
+ window.location.reload();
4672
6770
  }).catch(function(){waitForGatewayAndReload(maxAttempts,attempt+1);});
4673
6771
  },3000);
4674
6772
  }
4675
- function doUpdateInstall(packageSpec,btnEl){
6773
+ function doUpdateInstall(packageSpec,btnEl,statusEl){
4676
6774
  btnEl.disabled=true;
4677
6775
  btnEl.textContent=t('update.installing');
6776
+ btnEl.style.cssText='background:rgba(99,102,241,.15);color:var(--pri);border:1px solid rgba(99,102,241,.3);border-radius:6px;padding:4px 14px;font-size:12px;font-weight:600;cursor:wait;white-space:nowrap';
4678
6777
  fetch('/api/update-install',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({packageSpec:packageSpec})})
4679
6778
  .then(function(r){return r.json()})
4680
6779
  .then(function(d){
4681
6780
  if(d.ok){
4682
6781
  btnEl.textContent=t('update.success');
4683
- btnEl.style.background='#22c55e';
4684
- btnEl.style.color='#fff';
4685
- waitForGatewayAndReload(20);
6782
+ btnEl.style.cssText='background:rgba(34,197,94,.15);color:#22c55e;border:1px solid rgba(34,197,94,.3);border-radius:6px;padding:4px 14px;font-size:12px;font-weight:600;cursor:default;white-space:nowrap';
6783
+ if(statusEl)statusEl.textContent=t('update.restarting');
6784
+ waitForGatewayAndReload(40);
4686
6785
  }else{
4687
- btnEl.textContent=t('update.failed')+': '+(d.error||'').slice(0,80);
4688
- btnEl.style.background='#ef4444';
4689
- btnEl.style.color='#fff';
6786
+ btnEl.textContent=t('update.btn');
6787
+ btnEl.style.cssText='background:none;border:1px solid currentColor;border-radius:6px;padding:4px 14px;font-size:12px;font-weight:600;color:inherit;cursor:pointer;white-space:nowrap;opacity:.85';
4690
6788
  btnEl.disabled=false;
4691
- setTimeout(function(){btnEl.textContent=t('update.btn');btnEl.style.background='';btnEl.style.color='';},5000);
6789
+ if(statusEl)statusEl.textContent=t('update.failed')+': '+(d.error||'').slice(0,60);
6790
+ setTimeout(function(){if(statusEl)statusEl.textContent='';},8000);
4692
6791
  }
4693
6792
  })
4694
- .catch(function(e){
4695
- btnEl.textContent=t('update.failed');
6793
+ .catch(function(){
6794
+ btnEl.textContent=t('update.btn');
6795
+ btnEl.style.cssText='background:none;border:1px solid currentColor;border-radius:6px;padding:4px 14px;font-size:12px;font-weight:600;color:inherit;cursor:pointer;white-space:nowrap;opacity:.85';
4696
6796
  btnEl.disabled=false;
4697
- setTimeout(function(){btnEl.textContent=t('update.btn');btnEl.style.background='';btnEl.style.color='';},5000);
4698
6797
  });
4699
6798
  }
4700
6799
  async function checkForUpdate(){
@@ -4703,25 +6802,33 @@ async function checkForUpdate(){
4703
6802
  if(!r.ok)return;
4704
6803
  const d=await r.json();
4705
6804
  if(!d.updateAvailable)return;
4706
- const pkgSpec=d.installCommand?d.installCommand.replace(/^openclaw plugins install\s+/,''):(d.packageName+'@'+d.latest);
4707
- const banner=document.createElement('div');
6805
+ const pkgSpec=d.installCommand?d.installCommand.replace(/^(?:npx\s+)?openclaw\s+plugins\s+install\s+/,''):(d.packageName+'@'+d.latest);
6806
+ var banner=document.createElement('div');
4708
6807
  banner.id='updateBanner';
4709
- banner.style.cssText='position:fixed;top:0;left:0;right:0;z-index:9999;background:linear-gradient(135deg,#f59e0b,#d97706);color:#fff;padding:10px 20px;display:flex;align-items:center;justify-content:space-between;font-size:14px;box-shadow:0 2px 8px rgba(0,0,0,.25)';
4710
- var leftSpan=document.createElement('span');
4711
- leftSpan.innerHTML='🔔 '+t('update.available')+': <b>v'+esc(d.current)+'</b> → <b>v'+esc(d.latest)+'</b>';
6808
+ banner.style.cssText='display:flex;align-items:center;gap:10px;padding:12px 20px;font-size:13px;font-weight:500;border-radius:10px;margin:0 32px;animation:slideIn .3s ease;background:rgba(245,158,11,.1);color:#d97706;border:1px solid rgba(245,158,11,.25)';
6809
+ var textNode=document.createElement('div');
6810
+ textNode.style.cssText='display:flex;align-items:center;gap:8px;flex-shrink:0';
6811
+ textNode.innerHTML='\u{1F4E6} '+t('update.available')+' <b style="margin:0 2px">v'+esc(d.current)+'</b> \u2192 <b style="margin:0 2px">v'+esc(d.latest)+'</b>';
4712
6812
  var btnUpdate=document.createElement('button');
6813
+ btnUpdate.className='emb-banner-btn';
4713
6814
  btnUpdate.textContent=t('update.btn');
4714
- btnUpdate.style.cssText='background:#fff;color:#d97706;border:none;padding:5px 16px;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;margin-left:12px';
4715
- btnUpdate.onclick=function(){doUpdateInstall(pkgSpec,btnUpdate)};
6815
+ var statusDiv=document.createElement('div');
6816
+ statusDiv.style.cssText='font-size:11px;opacity:.8;flex-shrink:0';
6817
+ btnUpdate.onclick=function(){doUpdateInstall(pkgSpec,btnUpdate,statusDiv)};
6818
+ textNode.appendChild(btnUpdate);
6819
+ var spacer=document.createElement('div');
6820
+ spacer.style.cssText='flex:1';
4716
6821
  var btnClose=document.createElement('button');
6822
+ btnClose.className='emb-banner-close';
4717
6823
  btnClose.innerHTML='&times;';
4718
- btnClose.style.cssText='background:none;border:none;color:#fff;font-size:18px;cursor:pointer;padding:0 4px';
4719
- btnClose.onclick=function(){banner.remove();document.body.style.paddingTop='';};
4720
- leftSpan.appendChild(btnUpdate);
4721
- banner.appendChild(leftSpan);
6824
+ btnClose.onclick=function(){banner.remove()};
6825
+ banner.appendChild(textNode);
6826
+ banner.appendChild(statusDiv);
6827
+ banner.appendChild(spacer);
4722
6828
  banner.appendChild(btnClose);
4723
- document.body.prepend(banner);
4724
- document.body.style.paddingTop='48px';
6829
+ var embBanner=document.querySelector('.emb-banner');
6830
+ if(embBanner&&embBanner.parentNode){embBanner.parentNode.insertBefore(banner,embBanner);}
6831
+ else{var ct=document.querySelector('.content-area')||document.querySelector('main')||document.body;if(ct.firstChild)ct.insertBefore(banner,ct.firstChild);else ct.appendChild(banner);}
4725
6832
  }catch(e){}
4726
6833
  }
4727
6834