@memtensor/memos-local-openclaw-plugin 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -2
- package/SKILL.md +2 -2
- package/dist/viewer/html.d.ts +1 -1
- package/dist/viewer/html.d.ts.map +1 -1
- package/dist/viewer/html.js +45 -1
- package/dist/viewer/html.js.map +1 -1
- package/index.ts +4 -3
- package/package.json +1 -1
- package/src/viewer/html.ts +45 -1
- package/www/index.html +28 -2
package/README.md
CHANGED
|
@@ -176,6 +176,38 @@ npx tsx scripts/smoke-test.ts
|
|
|
176
176
|
|
|
177
177
|
The smoke test writes test conversations, searches for them, verifies timeline and get operations, and checks anti-writeback protection. If you installed only from npm, you can skip this step.
|
|
178
178
|
|
|
179
|
+
### Reinstall (when install fails or you need to upgrade)
|
|
180
|
+
|
|
181
|
+
If you see **"plugin already exists"** or **"plugin not found"** after deleting the plugin folder:
|
|
182
|
+
|
|
183
|
+
1. **Option A — Use OpenClaw install (recommended when config does not reference the plugin yet)**
|
|
184
|
+
Delete the extension folder, then run:
|
|
185
|
+
```bash
|
|
186
|
+
rm -rf ~/.openclaw/extensions/memos-local
|
|
187
|
+
openclaw plugins install @memtensor/memos-local-openclaw-plugin
|
|
188
|
+
```
|
|
189
|
+
If your `openclaw.json` already has `plugins.slots.memory: "memos-local"` and `plugins.entries.memos-local`, the config check may fail with "plugin not found" before install runs. In that case use Option B.
|
|
190
|
+
|
|
191
|
+
2. **Option B — Manual install (when config already references memos-local)**
|
|
192
|
+
Delete the folder, then install from npm and extract:
|
|
193
|
+
```bash
|
|
194
|
+
rm -rf ~/.openclaw/extensions/memos-local
|
|
195
|
+
cd /tmp
|
|
196
|
+
npm pack @memtensor/memos-local-openclaw-plugin
|
|
197
|
+
tar -xzf memtensor-memos-local-openclaw-plugin-*.tgz
|
|
198
|
+
mv package ~/.openclaw/extensions/memos-local
|
|
199
|
+
```
|
|
200
|
+
**No need to run `npm install`** — on first load the plugin will install dependencies automatically. Then restart the gateway: `openclaw gateway stop` then `openclaw gateway start`.
|
|
201
|
+
|
|
202
|
+
**Plugin shows as "error" in `openclaw plugins list`?** (e.g. `Cannot find module '@sinclair/typebox'`)
|
|
203
|
+
If auto-install failed (e.g. `npm` not in PATH when the gateway runs), install dependencies manually once:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
cd ~/.openclaw/extensions/memos-local && npm install --omit=dev
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Then restart the gateway.
|
|
210
|
+
|
|
179
211
|
## Agent Tools
|
|
180
212
|
|
|
181
213
|
The plugin registers 4 tools that your agent can use:
|
|
@@ -253,7 +285,7 @@ Query → FTS5 + Vector dual recall → RRF Fusion → MMR Rerank
|
|
|
253
285
|
→ Recency Decay → Score Filter → Top-K Results
|
|
254
286
|
```
|
|
255
287
|
|
|
256
|
-
See the [full documentation](www/docs/) in the repo for detailed architecture and algorithm explanations.
|
|
288
|
+
See the [full documentation](www/docs/index.html) in the repo (or open `www/docs/index.html` locally) for detailed architecture and algorithm explanations.
|
|
257
289
|
|
|
258
290
|
## Data Location
|
|
259
291
|
|
|
@@ -262,7 +294,7 @@ Whether you install from npm or from source, the plugin stores data under your O
|
|
|
262
294
|
| File | Path |
|
|
263
295
|
|---|---|
|
|
264
296
|
| Database | `~/.openclaw/memos-local/memos.db` |
|
|
265
|
-
| Viewer auth | `~/.openclaw/
|
|
297
|
+
| Viewer auth | `~/.openclaw/viewer-auth.json` |
|
|
266
298
|
| Gateway log | `~/.openclaw/logs/gateway.log` |
|
|
267
299
|
| Plugin code (npm install) | `~/.openclaw/extensions/` (managed by OpenClaw) |
|
|
268
300
|
|
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Memory Recall Skill — memos-local
|
|
2
2
|
|
|
3
|
-
You have access to long-term conversation memory through
|
|
4
|
-
`memory_search`, `memory_timeline`, `memory_get
|
|
3
|
+
You have access to long-term conversation memory through four tools:
|
|
4
|
+
`memory_search`, `memory_timeline`, `memory_get`, `memory_viewer` (returns the web dashboard URL).
|
|
5
5
|
|
|
6
6
|
## When to Search
|
|
7
7
|
|
package/dist/viewer/html.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const viewerHTML = "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>OpenClaw Memory - Powered by MemOS</title>\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<style>\n*{margin:0;padding:0;box-sizing:border-box}\n:root{\n --bg:#050510;--bg-card:rgba(255,255,255,.04);--bg-card-hover:rgba(255,255,255,.07);\n --border:rgba(255,255,255,.08);--border-glow:rgba(0,187,238,.25);\n --text:#f0f4f8;--text-sec:#8b95a5;--text-muted:#5a6373;\n --pri:#00bbee;--pri-glow:rgba(0,187,238,.15);--pri-dark:#0088aa;\n --pri-grad:linear-gradient(135deg,#00bbee,#00a0cc);\n --accent:#e63946;--accent-glow:rgba(230,57,70,.15);\n --green:#10b981;--green-bg:rgba(16,185,129,.12);\n --amber:#f59e0b;--amber-bg:rgba(245,158,11,.12);\n --violet:#8b5cf6;--rose:#f43f5e;--rose-bg:rgba(244,63,94,.12);\n --shadow-sm:0 1px 2px rgba(0,0,0,.2);--shadow:0 4px 12px rgba(0,0,0,.25);\n --shadow-lg:0 20px 40px rgba(0,0,0,.35);\n --radius:12px;--radius-lg:14px;--radius-xl:18px;\n}\nbody{font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);line-height:1.6}\nbutton{cursor:pointer;font-family:inherit;font-size:inherit}\ninput,textarea,select{font-family:inherit;font-size:inherit}\n\n/* \u2500\u2500\u2500 Auth (Linkify \u914D\u8272: globals.css .dark + \u84DD\u7D2B\u6E10\u53D8) \u2500\u2500\u2500 */\n.auth-screen{display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px;background:linear-gradient(135deg,rgb(36,0,255) 0%,rgb(0,135,255) 35%,rgb(108,39,157) 70%,rgb(105,30,255) 100%);position:relative;overflow:hidden}\n.auth-card{background:hsl(0 0% 100%);border:none;border-radius:8px;padding:48px 40px;width:100%;max-width:420px;box-shadow:0 25px 50px -12px rgba(0,0,0,.25);text-align:center;position:relative;z-index:1}\n.auth-card .logo{width:56px;height:56px;margin:0 auto 20px;display:flex;align-items:center;justify-content:center;font-size:48px;background:none;border-radius:0}\n.auth-card h1{font-size:22px;font-weight:700;margin-bottom:4px;color:hsl(0 0% 3.9%);letter-spacing:-.02em}\n.auth-card p{color:hsl(0 0% 45.1%);margin-bottom:24px;font-size:14px}\n.auth-card input{width:100%;padding:12px 16px;border:1px solid hsl(0 0% 89.8%);border-radius:8px;font-size:14px;transition:all .2s;margin-bottom:10px;outline:none;background:#fff;color:hsl(0 0% 3.9%)}\n.auth-card input::placeholder{color:hsl(0 0% 45.1%)}\n.auth-card input:focus{border-color:rgb(168,85,247);box-shadow:0 0 0 3px rgba(168,85,247,.2)}\n.auth-card .btn-auth{width:100%;padding:12px;border:none;border-radius:8px;background:hsl(0 0% 9%);color:hsl(0 0% 98%);font-weight:600;font-size:14px;transition:all .2s}\n.auth-card .btn-auth:hover{background:hsl(0 0% 14%);transform:translateY(-1px);box-shadow:0 8px 25px rgba(0,0,0,.2)}\n.auth-card .error-msg{color:hsl(0 84.2% 60.2%);font-size:13px;margin-top:8px;min-height:20px}\n.auth-card .btn-text{color:hsl(0 0% 45.1%)}\n.auth-card .btn-text:hover{color:rgb(168,85,247)}\n\n.reset-guide{text-align:left;margin-bottom:20px}\n.reset-step{display:flex;gap:14px;margin-bottom:16px}\n.step-num{width:28px;height:28px;border-radius:50%;background:hsl(0 0% 9%);color:hsl(0 0% 98%);font-size:12px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0}\n.step-body{flex:1;min-width:0}\n.step-title{font-size:14px;font-weight:600;color:hsl(0 0% 3.9%);margin-bottom:2px}\n.step-desc{font-size:13px;color:hsl(0 0% 45.1%);line-height:1.5}\n.cmd-box{margin-top:8px;background:hsl(0 0% 96.1%);border:1px solid hsl(0 0% 89.8%);border-radius:8px;padding:12px 14px;font-size:12px;font-family:ui-monospace,monospace;cursor:pointer;transition:all .15s;display:flex;align-items:center;justify-content:space-between;gap:8px;word-break:break-all;color:hsl(0 0% 3.9%)}\n.cmd-box:hover{border-color:rgb(168,85,247);background:rgba(168,85,247,.08)}\n.cmd-box code{flex:1}\n.copy-hint{font-size:11px;color:hsl(0 0% 45.1%);white-space:nowrap}\n.cmd-box.copied .copy-hint{color:hsl(142 71% 45%)}\n\n/* \u2500\u2500\u2500 App Layout (dark dashboard, same as www) \u2500\u2500\u2500 */\n.app{display:none;flex-direction:column;min-height:100vh}\n.topbar{background:rgba(5,5,16,.85);border-bottom:1px solid var(--border);padding:0 28px;height:64px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100;backdrop-filter:blur(12px)}\n.topbar .brand{display:flex;align-items:center;gap:12px;font-weight:700;font-size:17px;color:var(--text);letter-spacing:-.02em}\n.topbar .brand .icon{width:38px;height:38px;display:flex;align-items:center;justify-content:center;font-size:26px;background:none;border-radius:0}\n.topbar .actions{display:flex;align-items:center;gap:10px}\n\n.main-content{display:flex;flex:1;max-width:1400px;margin:0 auto;width:100%;padding:28px 32px;gap:28px}\n\n/* \u2500\u2500\u2500 Sidebar \u2500\u2500\u2500 */\n.sidebar{width:260px;flex-shrink:0}\n.sidebar .stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:24px}\n.stat-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:18px;transition:all .2s}\n.stat-card:hover{border-color:var(--border-glow);background:var(--bg-card-hover)}\n.stat-card .stat-value{font-size:22px;font-weight:700;color:var(--text);letter-spacing:-.02em}\n.stat-card .stat-label{font-size:12px;color:var(--text-sec);margin-top:4px;font-weight:500}\n.stat-card.pri .stat-value{color:var(--pri)}\n.stat-card.green .stat-value{color:var(--green)}\n.stat-card.amber .stat-value{color:var(--amber)}\n.stat-card.rose .stat-value{color:var(--rose)}\n\n.sidebar .section-title{font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em;margin:24px 0 12px;padding:0 2px}\n.sidebar .session-list{display:flex;flex-direction:column;gap:6px;max-height:280px;overflow-y:auto}\n.session-item{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:var(--bg-card);border:1px solid var(--border);border-radius:10px;cursor:pointer;transition:all .15s;font-size:13px;color:var(--text)}\n.session-item:hover{border-color:var(--pri);background:var(--pri-glow)}\n.session-item.active{border-color:var(--pri);background:var(--pri-glow);font-weight:600;color:var(--pri)}\n.session-item .count{color:var(--text-sec);font-size:11px;font-weight:600;background:rgba(0,0,0,.2);padding:3px 8px;border-radius:8px}\n\n.provider-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--green-bg);color:var(--green);border-radius:999px;font-size:11px;font-weight:600;margin-top:10px}\n.provider-badge.offline{background:var(--amber-bg);color:var(--amber)}\n\n/* \u2500\u2500\u2500 Feed \u2500\u2500\u2500 */\n.feed{flex:1;min-width:0}\n.search-bar{display:flex;gap:12px;margin-bottom:16px;position:relative}\n.search-bar input{flex:1;padding:12px 16px 12px 44px;border:1px solid var(--border);border-radius:12px;font-size:14px;outline:none;background:var(--bg-card);color:var(--text);transition:all .2s}\n.search-bar input::placeholder{color:var(--text-muted)}\n.search-bar input:focus{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-glow)}\n.search-bar .search-icon{position:absolute;left:16px;top:50%;transform:translateY(-50%);color:var(--text-muted);font-size:15px;pointer-events:none}\n.search-meta{font-size:12px;color:var(--text-sec);margin-bottom:14px;padding:0 2px}\n\n.filter-bar{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}\n.filter-chip{padding:6px 14px;border:1px solid var(--border);border-radius:999px;background:var(--bg-card);color:var(--text-sec);font-size:13px;font-weight:500;transition:all .15s}\n.filter-chip:hover{border-color:var(--pri);color:var(--pri)}\n.filter-chip.active{background:var(--pri);color:#000;border-color:var(--pri)}\n\n.memory-list{display:flex;flex-direction:column;gap:16px}\n.memory-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px 24px;transition:all .2s}\n.memory-card:hover{border-color:var(--border-glow);background:var(--bg-card-hover)}\n.memory-card .card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:wrap;gap:8px}\n.memory-card .meta{display:flex;align-items:center;gap:8px}\n.role-tag{padding:4px 10px;border-radius:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.03em}\n.role-tag.user{background:var(--pri-glow);color:var(--pri);border:1px solid rgba(0,187,238,.2)}\n.role-tag.assistant{background:var(--accent-glow);color:var(--accent);border:1px solid rgba(230,57,70,.2)}\n.role-tag.system{background:var(--amber-bg);color:var(--amber);border:1px solid rgba(245,158,11,.2)}\n.kind-tag{padding:4px 10px;border-radius:8px;font-size:11px;color:var(--text-sec);background:rgba(0,0,0,.2);font-weight:500}\n.card-time{font-size:12px;color:var(--text-sec);display:flex;align-items:center;gap:8px}\n.session-tag{font-size:11px;font-family:ui-monospace,monospace;color:var(--text-muted);background:rgba(0,0,0,.2);padding:3px 8px;border-radius:6px;cursor:default}\n.card-summary{font-size:15px;font-weight:600;color:var(--text);margin-bottom:10px;line-height:1.5;letter-spacing:-.01em}\n.card-content{font-size:13px;color:var(--text-sec);line-height:1.65;max-height:0;overflow:hidden;transition:max-height .3s ease}\n.card-content.show{max-height:600px;overflow-y:auto}\n.card-content pre{white-space:pre-wrap;word-break:break-all;background:rgba(0,0,0,.25);padding:14px;border-radius:10px;font-size:12px;font-family:ui-monospace,monospace;margin-top:10px;border:1px solid var(--border);color:var(--text-sec)}\n.card-actions{display:flex;align-items:center;gap:8px;margin-top:14px}\n.vscore-badge{display:inline-flex;align-items:center;background:linear-gradient(135deg,var(--pri),var(--violet));color:#fff;font-size:10px;font-weight:700;padding:4px 10px;border-radius:8px;margin-left:auto}\n\n/* \u2500\u2500\u2500 Buttons \u2500\u2500\u2500 */\n.btn{padding:8px 16px;border-radius:10px;border:1px solid var(--border);background:var(--bg-card);color:var(--text);font-size:13px;font-weight:500;transition:all .15s;display:inline-flex;align-items:center;gap:6px}\n.btn:hover{border-color:var(--pri);color:var(--pri)}\n.btn-primary{background:var(--pri);color:#000;border:none}\n.btn-primary:hover{background:#4dd9ff;transform:translateY(-1px);box-shadow:0 6px 20px rgba(0,187,238,.25)}\n.btn-danger{color:var(--accent);border-color:var(--accent)}\n.btn-danger:hover{background:var(--accent);color:#fff;border-color:var(--accent)}\n.btn-sm{padding:6px 12px;font-size:12px}\n.btn-icon{padding:6px 8px;font-size:14px}\n.btn-text{border:none;background:none;color:var(--text-sec);font-size:13px;padding:4px 8px}\n.btn-text:hover{color:var(--pri)}\n\n/* \u2500\u2500\u2500 Modal \u2500\u2500\u2500 */\n.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:500;align-items:center;justify-content:center;backdrop-filter:blur(8px)}\n.modal-overlay.show{display:flex}\n.modal{background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-xl);padding:32px;width:100%;max-width:520px;box-shadow:var(--shadow-lg);max-height:85vh;overflow-y:auto}\n.modal h2{font-size:20px;font-weight:700;margin-bottom:24px;color:var(--text);letter-spacing:-.02em}\n.form-group{margin-bottom:18px}\n.form-group label{display:block;font-size:13px;font-weight:600;color:var(--text-sec);margin-bottom:6px}\n.form-group input,.form-group textarea,.form-group select{width:100%;padding:10px 14px;border:1px solid var(--border);border-radius:10px;font-size:14px;outline:none;transition:all .2s;background:var(--bg-card);color:var(--text)}\n.form-group input::placeholder,.form-group textarea::placeholder{color:var(--text-muted)}\n.form-group input:focus,.form-group textarea:focus,.form-group select:focus{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-glow)}\n.form-group textarea{min-height:100px;resize:vertical}\n.modal-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:28px}\n\n/* \u2500\u2500\u2500 Toast \u2500\u2500\u2500 */\n.toast-container{position:fixed;top:80px;right:24px;z-index:1000;display:flex;flex-direction:column;gap:8px}\n.toast{padding:14px 20px;border-radius:10px;font-size:13px;font-weight:500;box-shadow:var(--shadow-lg);animation:slideIn .3s ease;display:flex;align-items:center;gap:10px;max-width:360px;border:1px solid}\n.toast.success{background:var(--green-bg);color:var(--green);border-color:rgba(16,185,129,.3)}\n.toast.error{background:var(--rose-bg);color:var(--rose);border-color:rgba(244,63,94,.3)}\n.toast.info{background:var(--pri-glow);color:var(--pri);border-color:rgba(0,187,238,.3)}\n@keyframes slideIn{from{transform:translateX(100px);opacity:0}to{transform:translateX(0);opacity:1}}\n\n.empty{text-align:center;padding:64px 20px;color:var(--text-sec)}\n.empty .icon{font-size:52px;margin-bottom:16px;opacity:.5}\n.empty p{font-size:15px;font-weight:500}\n\n.spinner{width:40px;height:40px;border:3px solid var(--border);border-top-color:var(--pri);border-radius:50%;animation:spin .8s linear infinite;margin:48px auto}\n@keyframes spin{to{transform:rotate(360deg)}}\n\n::-webkit-scrollbar{width:6px;height:6px}\n::-webkit-scrollbar-track{background:transparent}\n::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15);border-radius:3px}\n::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.25)}\n\n.filter-sep{width:1px;height:20px;background:var(--border);margin:0 4px}\n.filter-select{padding:6px 12px;border:1px solid var(--border);border-radius:999px;background:var(--bg-card);color:var(--text-sec);font-size:13px;outline:none;cursor:pointer}\n.filter-select:focus{border-color:var(--pri)}\n.date-filter{display:flex;align-items:center;gap:10px;margin-bottom:18px;font-size:13px;color:var(--text-sec)}\n.date-filter input[type=\"datetime-local\"]{padding:6px 10px;border:1px solid var(--border);border-radius:8px;font-size:12px;outline:none;background:var(--bg-card);color:var(--text)}\n.date-filter input[type=\"datetime-local\"]:focus{border-color:var(--pri)}\n.date-filter label{font-weight:500}\n\n.pagination{display:flex;align-items:center;justify-content:center;gap:6px;padding:28px 0;flex-wrap:wrap}\n.pagination .pg-btn{min-width:38px;height:38px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:10px;background:var(--bg-card);color:var(--text-sec);font-size:13px;font-weight:500;cursor:pointer;transition:all .15s}\n.pagination .pg-btn:hover{border-color:var(--pri);color:var(--pri)}\n.pagination .pg-btn.active{background:var(--pri);color:#000;border-color:var(--pri)}\n.pagination .pg-btn.disabled{opacity:.4;pointer-events:none}\n.pagination .pg-info{font-size:12px;color:var(--text-sec);padding:0 12px}\n\n@media(max-width:900px){.main-content{flex-direction:column;padding:20px}.sidebar{width:100%}.sidebar .stats-grid{grid-template-columns:repeat(4,1fr)}}\n</style>\n</head>\n<body>\n\n<!-- \u2500\u2500\u2500 Auth: Setup Password \u2500\u2500\u2500 -->\n<div id=\"setupScreen\" class=\"auth-screen\" style=\"display:none\">\n <div class=\"auth-card\">\n <div class=\"logo\">\uD83E\uDD9E</div>\n <h1>OpenClaw Memory</h1>\n <p style=\"font-size:12px;color:var(--text-sec);margin-bottom:6px\">Powered by MemOS</p>\n <p>Set a password to protect your memories</p>\n <input type=\"password\" id=\"setupPw\" placeholder=\"Enter a password (4+ characters)\" autofocus>\n <input type=\"password\" id=\"setupPw2\" placeholder=\"Confirm password\">\n <button class=\"btn-auth\" onclick=\"doSetup()\">Set Password & Enter</button>\n <div class=\"error-msg\" id=\"setupErr\"></div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Auth: Login \u2500\u2500\u2500 -->\n<div id=\"loginScreen\" class=\"auth-screen\" style=\"display:none\">\n <div class=\"auth-card\">\n <div class=\"logo\">\uD83E\uDD9E</div>\n <h1>OpenClaw Memory</h1>\n <p style=\"font-size:12px;color:var(--text-sec);margin-bottom:6px\">Powered by MemOS</p>\n <p>Enter your password to access memories</p>\n <div id=\"loginForm\">\n <input type=\"password\" id=\"loginPw\" placeholder=\"Password\" autofocus>\n <button class=\"btn-auth\" onclick=\"doLogin()\">Unlock</button>\n <div class=\"error-msg\" id=\"loginErr\"></div>\n <button class=\"btn-text\" style=\"margin-top:12px;font-size:13px;color:var(--text-sec)\" onclick=\"showResetForm()\">Forgot password?</button>\n </div>\n <div id=\"resetForm\" style=\"display:none\">\n <div class=\"reset-guide\">\n <div class=\"reset-step\">\n <div class=\"step-num\">1</div>\n <div class=\"step-body\">\n <div class=\"step-title\">Open Terminal</div>\n <div class=\"step-desc\">Run the following command to get your reset token (use the pattern below so you get the line that contains the token):</div>\n <div class=\"cmd-box\" onclick=\"copyCmd(this)\">\n <code>grep \"password reset token:\" /tmp/openclaw/openclaw-*.log ~/.openclaw/logs/gateway.log 2>/dev/null | tail -1</code>\n <span class=\"copy-hint\">Click to copy</span>\n </div>\n </div>\n </div>\n <div class=\"reset-step\">\n <div class=\"step-num\">2</div>\n <div class=\"step-body\">\n <div class=\"step-title\">Find the token</div>\n <div class=\"step-desc\">In the output, find <span style=\"font-family:monospace;font-size:12px;color:var(--pri)\">password reset token: <strong>a1b2c3d4e5f6...</strong></span> (plain line or inside JSON). Copy the 32-character hex string after the colon.</div>\n </div>\n </div>\n <div class=\"reset-step\">\n <div class=\"step-num\">3</div>\n <div class=\"step-body\">\n <div class=\"step-title\">Paste & reset</div>\n <div class=\"step-desc\">Paste the token below and set your new password.</div>\n </div>\n </div>\n </div>\n <input type=\"text\" id=\"resetToken\" placeholder=\"Paste reset token here\" style=\"margin-bottom:8px;font-family:monospace\">\n <input type=\"password\" id=\"resetNewPw\" placeholder=\"New password (4+ characters)\">\n <input type=\"password\" id=\"resetNewPw2\" placeholder=\"Confirm new password\">\n <button class=\"btn-auth\" onclick=\"doReset()\">Reset Password</button>\n <div class=\"error-msg\" id=\"resetErr\"></div>\n <button class=\"btn-text\" style=\"margin-top:12px;font-size:13px;color:var(--text-sec)\" onclick=\"showLoginForm()\">\u2190 Back to login</button>\n </div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Main App \u2500\u2500\u2500 -->\n<div class=\"app\" id=\"app\">\n <div class=\"topbar\">\n <div class=\"brand\">\n <div class=\"icon\">\uD83E\uDD9E</div>\n <span>OpenClaw Memory <span style=\"font-weight:400;color:var(--text-sec);font-size:12px\">by MemOS</span></span>\n </div>\n <div class=\"actions\">\n <button class=\"btn btn-primary\" onclick=\"openCreateModal()\">+ New Memory</button>\n <button class=\"btn\" onclick=\"loadAll()\">Refresh</button>\n <button class=\"btn btn-danger\" onclick=\"clearAll()\">Clear All</button>\n <button class=\"btn btn-text\" onclick=\"doLogout()\">Logout</button>\n </div>\n </div>\n\n <div class=\"main-content\">\n <div class=\"sidebar\" id=\"sidebar\">\n <div class=\"stats-grid\" id=\"statsGrid\">\n <div class=\"stat-card pri\"><div class=\"stat-value\" id=\"statTotal\">-</div><div class=\"stat-label\">Memories</div></div>\n <div class=\"stat-card green\"><div class=\"stat-value\" id=\"statSessions\">-</div><div class=\"stat-label\">Sessions</div></div>\n <div class=\"stat-card amber\"><div class=\"stat-value\" id=\"statEmbeddings\">-</div><div class=\"stat-label\">Embeddings</div></div>\n <div class=\"stat-card rose\"><div class=\"stat-value\" id=\"statTimeSpan\">-</div><div class=\"stat-label\">Days</div></div>\n </div>\n <div id=\"embeddingStatus\"></div>\n <div class=\"section-title\">Sessions</div>\n <div class=\"session-list\" id=\"sessionList\"></div>\n </div>\n\n <div class=\"feed\">\n <div class=\"search-bar\">\n <span class=\"search-icon\">\uD83D\uDD0D</span>\n <input type=\"text\" id=\"searchInput\" placeholder=\"Search memories (supports semantic search)...\" oninput=\"debounceSearch()\">\n </div>\n <div class=\"search-meta\" id=\"searchMeta\"></div>\n <div class=\"filter-bar\" id=\"filterBar\">\n <button class=\"filter-chip active\" data-role=\"\" onclick=\"setRoleFilter(this,'')\">All</button>\n <button class=\"filter-chip\" data-role=\"user\" onclick=\"setRoleFilter(this,'user')\">User</button>\n <button class=\"filter-chip\" data-role=\"assistant\" onclick=\"setRoleFilter(this,'assistant')\">Assistant</button>\n <button class=\"filter-chip\" data-role=\"system\" onclick=\"setRoleFilter(this,'system')\">System</button>\n <span class=\"filter-sep\"></span>\n <select id=\"filterKind\" class=\"filter-select\" onchange=\"applyFilters()\">\n <option value=\"\">All kinds</option>\n <option value=\"paragraph\">Paragraph</option>\n <option value=\"code_block\">Code</option>\n <option value=\"dialog\">Dialog</option>\n <option value=\"list\">List</option>\n <option value=\"error_stack\">Error</option>\n <option value=\"command\">Command</option>\n </select>\n <select id=\"filterSort\" class=\"filter-select\" onchange=\"applyFilters()\">\n <option value=\"newest\">Newest first</option>\n <option value=\"oldest\">Oldest first</option>\n </select>\n </div>\n <div class=\"date-filter\">\n <label>From</label><input type=\"datetime-local\" id=\"dateFrom\" step=\"1\" onchange=\"applyFilters()\">\n <label>To</label><input type=\"datetime-local\" id=\"dateTo\" step=\"1\" onchange=\"applyFilters()\">\n <button class=\"btn btn-sm btn-text\" onclick=\"clearDateFilter()\">Clear</button>\n </div>\n <div class=\"memory-list\" id=\"memoryList\"><div class=\"spinner\"></div></div>\n <div class=\"pagination\" id=\"pagination\"></div>\n </div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Memory Modal \u2500\u2500\u2500 -->\n<div class=\"modal-overlay\" id=\"modalOverlay\">\n <div class=\"modal\">\n <h2 id=\"modalTitle\">New Memory</h2>\n <div class=\"form-group\"><label>Role</label><select id=\"mRole\"><option value=\"user\">User</option><option value=\"assistant\">Assistant</option><option value=\"system\">System</option></select></div>\n <div class=\"form-group\"><label>Content</label><textarea id=\"mContent\" rows=\"4\" placeholder=\"Memory content...\"></textarea></div>\n <div class=\"form-group\"><label>Summary</label><input type=\"text\" id=\"mSummary\" placeholder=\"Brief summary (optional)\"></div>\n <div class=\"form-group\"><label>Kind</label><select id=\"mKind\"><option value=\"paragraph\">Paragraph</option><option value=\"code\">Code</option><option value=\"dialog\">Dialog</option></select></div>\n <div class=\"modal-actions\">\n <button class=\"btn\" onclick=\"closeModal()\">Cancel</button>\n <button class=\"btn btn-primary\" id=\"modalSubmit\" onclick=\"submitModal()\">Create</button>\n </div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Toast \u2500\u2500\u2500 -->\n<div class=\"toast-container\" id=\"toasts\"></div>\n\n<script>\nlet activeSession=null,activeRole='',editingId=null,searchTimer=null,memoryCache={},currentPage=1,totalPages=1,totalCount=0,PAGE_SIZE=30;\n\n/* \u2500\u2500\u2500 Auth flow \u2500\u2500\u2500 */\nasync function checkAuth(){\n const r=await fetch('/api/auth/status');\n const d=await r.json();\n if(d.needsSetup){\n document.getElementById('setupScreen').style.display='flex';\n document.getElementById('setupPw').addEventListener('keydown',e=>{if(e.key==='Enter')document.getElementById('setupPw2').focus()});\n document.getElementById('setupPw2').addEventListener('keydown',e=>{if(e.key==='Enter')doSetup()});\n } else if(!d.loggedIn){\n document.getElementById('loginScreen').style.display='flex';\n document.getElementById('loginPw').addEventListener('keydown',e=>{if(e.key==='Enter')doLogin()});\n } else {\n enterApp();\n }\n}\n\nasync function doSetup(){\n const pw=document.getElementById('setupPw').value;\n const pw2=document.getElementById('setupPw2').value;\n const err=document.getElementById('setupErr');\n if(pw.length<4){err.textContent='Password must be at least 4 characters';return}\n if(pw!==pw2){err.textContent='Passwords do not match';return}\n const r=await fetch('/api/auth/setup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pw})});\n const d=await r.json();\n if(d.ok){document.getElementById('setupScreen').style.display='none';enterApp();}\n else{err.textContent=d.error||'Setup failed'}\n}\n\nasync function doLogin(){\n const pw=document.getElementById('loginPw').value;\n const err=document.getElementById('loginErr');\n const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pw})});\n const d=await r.json();\n if(d.ok){document.getElementById('loginScreen').style.display='none';enterApp();}\n else{err.textContent='Incorrect password';document.getElementById('loginPw').value='';document.getElementById('loginPw').focus();}\n}\n\nasync function doLogout(){\n await fetch('/api/auth/logout',{method:'POST'});\n location.reload();\n}\n\nfunction showResetForm(){\n document.getElementById('loginForm').style.display='none';\n document.getElementById('resetForm').style.display='block';\n document.getElementById('resetToken').focus();\n}\n\nfunction showLoginForm(){\n document.getElementById('resetForm').style.display='none';\n document.getElementById('loginForm').style.display='block';\n document.getElementById('loginPw').focus();\n}\n\nfunction copyCmd(el){\n const code=el.querySelector('code').textContent;\n navigator.clipboard.writeText(code).then(()=>{\n el.classList.add('copied');\n el.querySelector('.copy-hint').textContent='Copied!';\n setTimeout(()=>{el.classList.remove('copied');el.querySelector('.copy-hint').textContent='Click to copy'},2000);\n });\n}\n\nasync function doReset(){\n const token=document.getElementById('resetToken').value.trim();\n const pw=document.getElementById('resetNewPw').value;\n const pw2=document.getElementById('resetNewPw2').value;\n const err=document.getElementById('resetErr');\n if(!token){err.textContent='Please enter the reset token';return}\n if(pw.length<4){err.textContent='Password must be at least 4 characters';return}\n if(pw!==pw2){err.textContent='Passwords do not match';return}\n const r=await fetch('/api/auth/reset',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,newPassword:pw})});\n const d=await r.json();\n if(d.ok){document.getElementById('loginScreen').style.display='none';enterApp();}\n else{err.textContent=d.error||'Reset failed'}\n}\n\nfunction enterApp(){\n document.getElementById('app').style.display='flex';\n loadAll();\n}\n\n/* \u2500\u2500\u2500 Data loading \u2500\u2500\u2500 */\nasync function loadAll(){\n await Promise.all([loadStats(),loadMemories()]);\n}\n\nasync function loadStats(){\n const r=await fetch('/api/stats');\n const d=await r.json();\n document.getElementById('statTotal').textContent=d.totalMemories;\n document.getElementById('statSessions').textContent=d.totalSessions;\n document.getElementById('statEmbeddings').textContent=d.totalEmbeddings;\n const days=d.timeRange.earliest?Math.max(1,Math.round((new Date(d.timeRange.latest)-new Date(d.timeRange.earliest))/(86400000))):0;\n document.getElementById('statTimeSpan').textContent=days;\n\n const provEl=document.getElementById('embeddingStatus');\n if(d.embeddingProvider && d.embeddingProvider!=='none'){\n provEl.innerHTML='<div class=\"provider-badge\"><span>\\u2713</span> Embedding: '+d.embeddingProvider+'</div>';\n } else {\n provEl.innerHTML='<div class=\"provider-badge offline\"><span>\\u26A0</span> No embedding model</div>';\n }\n\n const sl=document.getElementById('sessionList');\n sl.innerHTML='<div class=\"session-item'+(activeSession===null?' active':'')+'\" onclick=\"filterSession(null)\"><span>All Sessions</span><span class=\"count\">'+d.totalMemories+'</span></div>';\n (d.sessions||[]).forEach(s=>{\n const isActive=activeSession===s.session_key;\n const name=s.session_key.length>20?s.session_key.slice(0,8)+'...'+s.session_key.slice(-8):s.session_key;\n 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>';\n });\n}\n\nfunction getFilterParams(){\n const p=new URLSearchParams();\n if(activeSession) p.set('session',activeSession);\n if(activeRole) p.set('role',activeRole);\n const kind=document.getElementById('filterKind').value;\n if(kind) p.set('kind',kind);\n const df=document.getElementById('dateFrom').value;\n if(df) p.set('dateFrom',df);\n const dt=document.getElementById('dateTo').value;\n if(dt) p.set('dateTo',dt);\n const sort=document.getElementById('filterSort').value;\n if(sort==='oldest') p.set('sort','oldest');\n return p;\n}\n\nasync function loadMemories(page){\n if(page) currentPage=page;\n const list=document.getElementById('memoryList');\n list.innerHTML='<div class=\"spinner\"></div>';\n const p=getFilterParams();\n p.set('limit',PAGE_SIZE);\n p.set('page',currentPage);\n const r=await fetch('/api/memories?'+p.toString());\n const d=await r.json();\n totalPages=d.totalPages||1;\n totalCount=d.total||0;\n document.getElementById('searchMeta').textContent=totalCount+' memories total';\n renderMemories(d.memories||[]);\n renderPagination();\n}\n\nasync function doSearch(q){\n if(!q.trim()){currentPage=1;loadMemories();return}\n const list=document.getElementById('memoryList');\n list.innerHTML='<div class=\"spinner\"></div>';\n const p=getFilterParams();\n p.set('q',q);\n const r=await fetch('/api/search?'+p.toString());\n const d=await r.json();\n const meta=[];\n if(d.vectorCount>0) meta.push(d.vectorCount+' semantic');\n if(d.ftsCount>0) meta.push(d.ftsCount+' text');\n meta.push(d.total+' results');\n document.getElementById('searchMeta').textContent=meta.join(' \\u00B7 ');\n renderMemories(d.results||[]);\n document.getElementById('pagination').innerHTML='';\n}\n\nfunction debounceSearch(){\n clearTimeout(searchTimer);\n searchTimer=setTimeout(()=>doSearch(document.getElementById('searchInput').value),350);\n}\n\nfunction filterSession(key){\n activeSession=key;\n currentPage=1;\n loadAll();\n}\n\nfunction setRoleFilter(btn,role){\n activeRole=role;\n currentPage=1;\n document.querySelectorAll('.filter-chip').forEach(c=>c.classList.remove('active'));\n btn.classList.add('active');\n applyFilters();\n}\n\nfunction applyFilters(){\n currentPage=1;\n if(document.getElementById('searchInput').value.trim()){\n doSearch(document.getElementById('searchInput').value);\n } else {\n loadMemories();\n }\n}\n\nfunction clearDateFilter(){\n document.getElementById('dateFrom').value='';\n document.getElementById('dateTo').value='';\n applyFilters();\n}\n\n/* \u2500\u2500\u2500 Rendering \u2500\u2500\u2500 */\nfunction renderMemories(items){\n const list=document.getElementById('memoryList');\n if(!items.length){\n list.innerHTML='<div class=\"empty\"><div class=\"icon\">\\u{1F4ED}</div><p>No memories found</p></div>';\n return;\n }\n items.forEach(m=>{memoryCache[m.id]=m});\n list.innerHTML=items.map(m=>{\n const time=m.created_at?new Date(typeof m.created_at==='number'?m.created_at:m.created_at).toLocaleString('zh-CN'):'';\n const role=m.role||'user';\n const kind=m.kind||'paragraph';\n const summary=esc(m.summary||m.content?.slice(0,120)||'');\n const content=esc(m.content||'');\n const id=m.id;\n const vscore=m._vscore?'<span class=\"vscore-badge\">'+Math.round(m._vscore*100)+'%</span>':'';\n const sid=m.session_key||'';\n const sidShort=sid.length>18?sid.slice(0,6)+'..'+sid.slice(-6):sid;\n return '<div class=\"memory-card\">'+\n '<div class=\"card-header\"><div class=\"meta\"><span class=\"role-tag '+role+'\">'+role+'</span><span class=\"kind-tag\">'+kind+'</span></div><span class=\"card-time\"><span class=\"session-tag\" title=\"'+esc(sid)+'\">'+esc(sidShort)+'</span> '+time+'</span></div>'+\n '<div class=\"card-summary\">'+summary+'</div>'+\n '<div class=\"card-content\" id=\"content-'+id+'\"><pre>'+content+'</pre></div>'+\n '<div class=\"card-actions\">'+\n '<button class=\"btn btn-sm btn-text\" onclick=\"toggleContent(\\''+id+'\\')\">Expand</button>'+\n '<button class=\"btn btn-sm\" onclick=\"openEditModal(\\''+id+'\\')\">Edit</button>'+\n '<button class=\"btn btn-sm btn-danger\" onclick=\"deleteMemory(\\''+id+'\\')\">Delete</button>'+\n vscore+\n '</div></div>';\n }).join('');\n}\n\nfunction renderPagination(){\n const el=document.getElementById('pagination');\n if(totalPages<=1){el.innerHTML='';return}\n let h='';\n h+='<button class=\"pg-btn'+(currentPage<=1?' disabled':'')+'\" onclick=\"goPage('+(currentPage-1)+')\">\u2039</button>';\n const range=[];\n range.push(1);\n for(let i=Math.max(2,currentPage-2);i<=Math.min(totalPages-1,currentPage+2);i++) range.push(i);\n if(totalPages>1) range.push(totalPages);\n const unique=[...new Set(range)].sort((a,b)=>a-b);\n let prev=0;\n for(const p of unique){\n if(p-prev>1) h+='<span class=\"pg-info\">...</span>';\n h+='<button class=\"pg-btn'+(p===currentPage?' active':'')+'\" onclick=\"goPage('+p+')\">'+p+'</button>';\n prev=p;\n }\n h+='<button class=\"pg-btn'+(currentPage>=totalPages?' disabled':'')+'\" onclick=\"goPage('+(currentPage+1)+')\">\u203A</button>';\n h+='<span class=\"pg-info\">'+totalCount+' total</span>';\n el.innerHTML=h;\n}\n\nfunction goPage(p){\n if(p<1||p>totalPages||p===currentPage) return;\n currentPage=p;\n loadMemories();\n document.getElementById('memoryList').scrollIntoView({behavior:'smooth',block:'start'});\n}\n\nfunction toggleContent(id){\n const el=document.getElementById('content-'+id);\n el.classList.toggle('show');\n}\n\nfunction esc(s){\n if(!s)return'';\n return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\"/g,'"');\n}\n\n/* \u2500\u2500\u2500 CRUD \u2500\u2500\u2500 */\nfunction openCreateModal(){\n editingId=null;\n document.getElementById('modalTitle').textContent='New Memory';\n document.getElementById('modalSubmit').textContent='Create';\n document.getElementById('mRole').value='user';\n document.getElementById('mContent').value='';\n document.getElementById('mSummary').value='';\n document.getElementById('mKind').value='paragraph';\n document.getElementById('modalOverlay').classList.add('show');\n}\n\nfunction openEditModal(id){\n const m=memoryCache[id];\n if(!m){toast('Memory not found in cache','error');return}\n editingId=id;\n document.getElementById('modalTitle').textContent='Edit Memory';\n document.getElementById('modalSubmit').textContent='Save';\n document.getElementById('mRole').value=m.role||'user';\n document.getElementById('mContent').value=m.content||'';\n document.getElementById('mSummary').value=m.summary||'';\n document.getElementById('mKind').value=m.kind||'paragraph';\n document.getElementById('modalOverlay').classList.add('show');\n}\n\nfunction closeModal(){\n document.getElementById('modalOverlay').classList.remove('show');\n}\n\nasync function submitModal(){\n const data={\n role:document.getElementById('mRole').value,\n content:document.getElementById('mContent').value,\n summary:document.getElementById('mSummary').value,\n kind:document.getElementById('mKind').value,\n };\n if(!data.content.trim()){toast('Please enter content','error');return}\n let r;\n if(editingId){\n r=await fetch('/api/memory/'+editingId,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});\n } else {\n r=await fetch('/api/memory',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});\n }\n const d=await r.json();\n if(d.ok){toast(editingId?'Memory updated':'Memory created','success');closeModal();loadAll();}\n else{toast(d.error||'Operation failed','error')}\n}\n\nasync function deleteMemory(id){\n if(!confirm('Delete this memory?'))return;\n const r=await fetch('/api/memory/'+id,{method:'DELETE'});\n const d=await r.json();\n if(d.ok){toast('Memory deleted','success');loadAll();}\n else{toast('Delete failed','error')}\n}\n\nasync function clearAll(){\n if(!confirm('Delete ALL memories? This cannot be undone.'))return;\n if(!confirm('Are you absolutely sure?'))return;\n const r=await fetch('/api/memories',{method:'DELETE'});\n const d=await r.json();\n if(d.ok){toast('All memories cleared','success');loadAll();}\n else{toast('Clear failed','error')}\n}\n\n/* \u2500\u2500\u2500 Toast \u2500\u2500\u2500 */\nfunction toast(msg,type='info'){\n const c=document.getElementById('toasts');\n const t=document.createElement('div');\n t.className='toast '+type;\n const icons={success:'\\u2705',error:'\\u274C',info:'\\u2139\\uFE0F'};\n t.innerHTML=(icons[type]||'')+' '+esc(msg);\n c.appendChild(t);\n setTimeout(()=>t.remove(),3500);\n}\n\n/* \u2500\u2500\u2500 Init \u2500\u2500\u2500 */\ndocument.getElementById('modalOverlay').addEventListener('click',e=>{if(e.target.id==='modalOverlay')closeModal()});\ndocument.getElementById('searchInput').addEventListener('keydown',e=>{if(e.key==='Escape'){e.target.value='';loadMemories()}});\ncheckAuth();\n</script>\n</body>\n</html>";
|
|
1
|
+
export declare const viewerHTML = "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>OpenClaw Memory - Powered by MemOS</title>\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\n<style>\n*{margin:0;padding:0;box-sizing:border-box}\n:root{\n --bg:#050510;--bg-card:rgba(255,255,255,.04);--bg-card-hover:rgba(255,255,255,.07);\n --border:rgba(255,255,255,.08);--border-glow:rgba(0,187,238,.25);\n --text:#f0f4f8;--text-sec:#8b95a5;--text-muted:#5a6373;\n --pri:#00bbee;--pri-glow:rgba(0,187,238,.15);--pri-dark:#0088aa;\n --pri-grad:linear-gradient(135deg,#00bbee,#00a0cc);\n --accent:#e63946;--accent-glow:rgba(230,57,70,.15);\n --green:#10b981;--green-bg:rgba(16,185,129,.12);\n --amber:#f59e0b;--amber-bg:rgba(245,158,11,.12);\n --violet:#8b5cf6;--rose:#f43f5e;--rose-bg:rgba(244,63,94,.12);\n --shadow-sm:0 1px 2px rgba(0,0,0,.2);--shadow:0 4px 12px rgba(0,0,0,.25);\n --shadow-lg:0 20px 40px rgba(0,0,0,.35);\n --radius:12px;--radius-lg:14px;--radius-xl:18px;\n}\n[data-theme=\"light\"]{\n --bg:#f1f5f9;--bg-card:#fff;--bg-card-hover:#f8fafc;\n --border:rgba(0,0,0,.08);--border-glow:rgba(0,187,238,.4);\n --text:#0f172a;--text-sec:#475569;--text-muted:#64748b;\n --pri:#0891b2;--pri-glow:rgba(8,145,178,.12);--pri-dark:#0e7490;\n --pri-grad:linear-gradient(135deg,#0891b2,#0e7490);\n --accent:#dc2626;--accent-glow:rgba(220,38,38,.1);\n --green:#059669;--green-bg:rgba(5,150,105,.1);\n --amber:#d97706;--amber-bg:rgba(217,119,6,.1);\n --violet:#7c3aed;--rose:#e11d48;--rose-bg:rgba(225,29,72,.1);\n --shadow-sm:0 1px 2px rgba(0,0,0,.06);--shadow:0 4px 12px rgba(0,0,0,.08);\n --shadow-lg:0 20px 40px rgba(0,0,0,.12);\n}\n[data-theme=\"light\"] .auth-screen{background:linear-gradient(135deg,#e0f2fe 0%,#f0f9ff 50%,#e0e7ff 100%)}\n[data-theme=\"light\"] .auth-card{box-shadow:0 25px 50px -12px rgba(0,0,0,.12)}\n[data-theme=\"light\"] .topbar{background:rgba(255,255,255,.9);border-bottom-color:var(--border)}\n[data-theme=\"light\"] .session-item .count,[data-theme=\"light\"] .kind-tag,[data-theme=\"light\"] .session-tag{background:rgba(0,0,0,.06)}\n[data-theme=\"light\"] .card-content pre{background:rgba(0,0,0,.05);border-color:var(--border)}\n[data-theme=\"light\"] .vscore-badge{background:linear-gradient(135deg,var(--pri),var(--violet))}\n[data-theme=\"light\"] ::-webkit-scrollbar-thumb{background:rgba(0,0,0,.2)}\nbody{font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;transition:background .2s,color .2s}\nbutton{cursor:pointer;font-family:inherit;font-size:inherit}\ninput,textarea,select{font-family:inherit;font-size:inherit}\n\n/* \u2500\u2500\u2500 Auth (Linkify \u914D\u8272: globals.css .dark + \u84DD\u7D2B\u6E10\u53D8) \u2500\u2500\u2500 */\n.auth-screen{display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px;background:linear-gradient(135deg,rgb(36,0,255) 0%,rgb(0,135,255) 35%,rgb(108,39,157) 70%,rgb(105,30,255) 100%);position:relative;overflow:hidden}\n.auth-card{background:hsl(0 0% 100%);border:none;border-radius:8px;padding:48px 40px;width:100%;max-width:420px;box-shadow:0 25px 50px -12px rgba(0,0,0,.25);text-align:center;position:relative;z-index:1}\n.auth-card .logo{width:56px;height:56px;margin:0 auto 20px;display:flex;align-items:center;justify-content:center;font-size:48px;background:none;border-radius:0}\n.auth-card h1{font-size:22px;font-weight:700;margin-bottom:4px;color:hsl(0 0% 3.9%);letter-spacing:-.02em}\n.auth-card p{color:hsl(0 0% 45.1%);margin-bottom:24px;font-size:14px}\n.auth-card input{width:100%;padding:12px 16px;border:1px solid hsl(0 0% 89.8%);border-radius:8px;font-size:14px;transition:all .2s;margin-bottom:10px;outline:none;background:#fff;color:hsl(0 0% 3.9%)}\n.auth-card input::placeholder{color:hsl(0 0% 45.1%)}\n.auth-card input:focus{border-color:rgb(168,85,247);box-shadow:0 0 0 3px rgba(168,85,247,.2)}\n.auth-card .btn-auth{width:100%;padding:12px;border:none;border-radius:8px;background:hsl(0 0% 9%);color:hsl(0 0% 98%);font-weight:600;font-size:14px;transition:all .2s}\n.auth-card .btn-auth:hover{background:hsl(0 0% 14%);transform:translateY(-1px);box-shadow:0 8px 25px rgba(0,0,0,.2)}\n.auth-card .error-msg{color:hsl(0 84.2% 60.2%);font-size:13px;margin-top:8px;min-height:20px}\n.auth-card .btn-text{color:hsl(0 0% 45.1%)}\n.auth-card .btn-text:hover{color:rgb(168,85,247)}\n\n.reset-guide{text-align:left;margin-bottom:20px}\n.reset-step{display:flex;gap:14px;margin-bottom:16px}\n.step-num{width:28px;height:28px;border-radius:50%;background:hsl(0 0% 9%);color:hsl(0 0% 98%);font-size:12px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0}\n.step-body{flex:1;min-width:0}\n.step-title{font-size:14px;font-weight:600;color:hsl(0 0% 3.9%);margin-bottom:2px}\n.step-desc{font-size:13px;color:hsl(0 0% 45.1%);line-height:1.5}\n.cmd-box{margin-top:8px;background:hsl(0 0% 96.1%);border:1px solid hsl(0 0% 89.8%);border-radius:8px;padding:12px 14px;font-size:12px;font-family:ui-monospace,monospace;cursor:pointer;transition:all .15s;display:flex;align-items:center;justify-content:space-between;gap:8px;word-break:break-all;color:hsl(0 0% 3.9%)}\n.cmd-box:hover{border-color:rgb(168,85,247);background:rgba(168,85,247,.08)}\n.cmd-box code{flex:1}\n.copy-hint{font-size:11px;color:hsl(0 0% 45.1%);white-space:nowrap}\n.cmd-box.copied .copy-hint{color:hsl(142 71% 45%)}\n\n/* \u2500\u2500\u2500 App Layout (dark dashboard, same as www) \u2500\u2500\u2500 */\n.app{display:none;flex-direction:column;min-height:100vh}\n.topbar{background:rgba(5,5,16,.85);border-bottom:1px solid var(--border);padding:0 28px;height:64px;display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:100;backdrop-filter:blur(12px)}\n.topbar .brand{display:flex;align-items:center;gap:12px;font-weight:700;font-size:17px;color:var(--text);letter-spacing:-.02em}\n.topbar .brand .icon{width:38px;height:38px;display:flex;align-items:center;justify-content:center;font-size:26px;background:none;border-radius:0}\n.topbar .actions{display:flex;align-items:center;gap:10px}\n\n.main-content{display:flex;flex:1;max-width:1400px;margin:0 auto;width:100%;padding:28px 32px;gap:28px}\n\n/* \u2500\u2500\u2500 Sidebar \u2500\u2500\u2500 */\n.sidebar{width:260px;flex-shrink:0}\n.sidebar .stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:24px}\n.stat-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:18px;transition:all .2s}\n.stat-card:hover{border-color:var(--border-glow);background:var(--bg-card-hover)}\n.stat-card .stat-value{font-size:22px;font-weight:700;color:var(--text);letter-spacing:-.02em}\n.stat-card .stat-label{font-size:12px;color:var(--text-sec);margin-top:4px;font-weight:500}\n.stat-card.pri .stat-value{color:var(--pri)}\n.stat-card.green .stat-value{color:var(--green)}\n.stat-card.amber .stat-value{color:var(--amber)}\n.stat-card.rose .stat-value{color:var(--rose)}\n\n.sidebar .section-title{font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em;margin:24px 0 12px;padding:0 2px}\n.sidebar .session-list{display:flex;flex-direction:column;gap:6px;max-height:280px;overflow-y:auto}\n.session-item{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:var(--bg-card);border:1px solid var(--border);border-radius:10px;cursor:pointer;transition:all .15s;font-size:13px;color:var(--text)}\n.session-item:hover{border-color:var(--pri);background:var(--pri-glow)}\n.session-item.active{border-color:var(--pri);background:var(--pri-glow);font-weight:600;color:var(--pri)}\n.session-item .count{color:var(--text-sec);font-size:11px;font-weight:600;background:rgba(0,0,0,.2);padding:3px 8px;border-radius:8px}\n\n.provider-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--green-bg);color:var(--green);border-radius:999px;font-size:11px;font-weight:600;margin-top:10px}\n.provider-badge.offline{background:var(--amber-bg);color:var(--amber)}\n\n/* \u2500\u2500\u2500 Feed \u2500\u2500\u2500 */\n.feed{flex:1;min-width:0}\n.search-bar{display:flex;gap:12px;margin-bottom:16px;position:relative}\n.search-bar input{flex:1;padding:12px 16px 12px 44px;border:1px solid var(--border);border-radius:12px;font-size:14px;outline:none;background:var(--bg-card);color:var(--text);transition:all .2s}\n.search-bar input::placeholder{color:var(--text-muted)}\n.search-bar input:focus{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-glow)}\n.search-bar .search-icon{position:absolute;left:16px;top:50%;transform:translateY(-50%);color:var(--text-muted);font-size:15px;pointer-events:none}\n.search-meta{font-size:12px;color:var(--text-sec);margin-bottom:14px;padding:0 2px}\n\n.filter-bar{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap}\n.filter-chip{padding:6px 14px;border:1px solid var(--border);border-radius:999px;background:var(--bg-card);color:var(--text-sec);font-size:13px;font-weight:500;transition:all .15s}\n.filter-chip:hover{border-color:var(--pri);color:var(--pri)}\n.filter-chip.active{background:var(--pri);color:#000;border-color:var(--pri)}\n\n.memory-list{display:flex;flex-direction:column;gap:16px}\n.memory-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:20px 24px;transition:all .2s}\n.memory-card:hover{border-color:var(--border-glow);background:var(--bg-card-hover)}\n.memory-card .card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;flex-wrap:wrap;gap:8px}\n.memory-card .meta{display:flex;align-items:center;gap:8px}\n.role-tag{padding:4px 10px;border-radius:8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.03em}\n.role-tag.user{background:var(--pri-glow);color:var(--pri);border:1px solid rgba(0,187,238,.2)}\n.role-tag.assistant{background:var(--accent-glow);color:var(--accent);border:1px solid rgba(230,57,70,.2)}\n.role-tag.system{background:var(--amber-bg);color:var(--amber);border:1px solid rgba(245,158,11,.2)}\n.kind-tag{padding:4px 10px;border-radius:8px;font-size:11px;color:var(--text-sec);background:rgba(0,0,0,.2);font-weight:500}\n.card-time{font-size:12px;color:var(--text-sec);display:flex;align-items:center;gap:8px}\n.session-tag{font-size:11px;font-family:ui-monospace,monospace;color:var(--text-muted);background:rgba(0,0,0,.2);padding:3px 8px;border-radius:6px;cursor:default}\n.card-summary{font-size:15px;font-weight:600;color:var(--text);margin-bottom:10px;line-height:1.5;letter-spacing:-.01em}\n.card-content{font-size:13px;color:var(--text-sec);line-height:1.65;max-height:0;overflow:hidden;transition:max-height .3s ease}\n.card-content.show{max-height:600px;overflow-y:auto}\n.card-content pre{white-space:pre-wrap;word-break:break-all;background:rgba(0,0,0,.25);padding:14px;border-radius:10px;font-size:12px;font-family:ui-monospace,monospace;margin-top:10px;border:1px solid var(--border);color:var(--text-sec)}\n.card-actions{display:flex;align-items:center;gap:8px;margin-top:14px}\n.vscore-badge{display:inline-flex;align-items:center;background:linear-gradient(135deg,var(--pri),var(--violet));color:#fff;font-size:10px;font-weight:700;padding:4px 10px;border-radius:8px;margin-left:auto}\n\n/* \u2500\u2500\u2500 Buttons \u2500\u2500\u2500 */\n.btn{padding:8px 16px;border-radius:10px;border:1px solid var(--border);background:var(--bg-card);color:var(--text);font-size:13px;font-weight:500;transition:all .15s;display:inline-flex;align-items:center;gap:6px}\n.btn:hover{border-color:var(--pri);color:var(--pri)}\n.btn-primary{background:var(--pri);color:#000;border:none}\n.btn-primary:hover{background:#4dd9ff;transform:translateY(-1px);box-shadow:0 6px 20px rgba(0,187,238,.25)}\n.btn-danger{color:var(--accent);border-color:var(--accent)}\n.btn-danger:hover{background:var(--accent);color:#fff;border-color:var(--accent)}\n.btn-sm{padding:6px 12px;font-size:12px}\n.btn-icon{padding:6px 8px;font-size:14px}\n.btn-text{border:none;background:none;color:var(--text-sec);font-size:13px;padding:4px 8px}\n.btn-text:hover{color:var(--pri)}\n\n/* \u2500\u2500\u2500 Modal \u2500\u2500\u2500 */\n.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:500;align-items:center;justify-content:center;backdrop-filter:blur(8px)}\n.modal-overlay.show{display:flex}\n.modal{background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-xl);padding:32px;width:100%;max-width:520px;box-shadow:var(--shadow-lg);max-height:85vh;overflow-y:auto}\n.modal h2{font-size:20px;font-weight:700;margin-bottom:24px;color:var(--text);letter-spacing:-.02em}\n.form-group{margin-bottom:18px}\n.form-group label{display:block;font-size:13px;font-weight:600;color:var(--text-sec);margin-bottom:6px}\n.form-group input,.form-group textarea,.form-group select{width:100%;padding:10px 14px;border:1px solid var(--border);border-radius:10px;font-size:14px;outline:none;transition:all .2s;background:var(--bg-card);color:var(--text)}\n.form-group input::placeholder,.form-group textarea::placeholder{color:var(--text-muted)}\n.form-group input:focus,.form-group textarea:focus,.form-group select:focus{border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-glow)}\n.form-group textarea{min-height:100px;resize:vertical}\n.modal-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:28px}\n\n/* \u2500\u2500\u2500 Toast \u2500\u2500\u2500 */\n.toast-container{position:fixed;top:80px;right:24px;z-index:1000;display:flex;flex-direction:column;gap:8px}\n.toast{padding:14px 20px;border-radius:10px;font-size:13px;font-weight:500;box-shadow:var(--shadow-lg);animation:slideIn .3s ease;display:flex;align-items:center;gap:10px;max-width:360px;border:1px solid}\n.toast.success{background:var(--green-bg);color:var(--green);border-color:rgba(16,185,129,.3)}\n.toast.error{background:var(--rose-bg);color:var(--rose);border-color:rgba(244,63,94,.3)}\n.toast.info{background:var(--pri-glow);color:var(--pri);border-color:rgba(0,187,238,.3)}\n@keyframes slideIn{from{transform:translateX(100px);opacity:0}to{transform:translateX(0);opacity:1}}\n\n.empty{text-align:center;padding:64px 20px;color:var(--text-sec)}\n.empty .icon{font-size:52px;margin-bottom:16px;opacity:.5}\n.empty p{font-size:15px;font-weight:500}\n\n.spinner{width:40px;height:40px;border:3px solid var(--border);border-top-color:var(--pri);border-radius:50%;animation:spin .8s linear infinite;margin:48px auto}\n@keyframes spin{to{transform:rotate(360deg)}}\n\n::-webkit-scrollbar{width:6px;height:6px}\n::-webkit-scrollbar-track{background:transparent}\n::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15);border-radius:3px}\n::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.25)}\n\n.filter-sep{width:1px;height:20px;background:var(--border);margin:0 4px}\n.filter-select{padding:6px 12px;border:1px solid var(--border);border-radius:999px;background:var(--bg-card);color:var(--text-sec);font-size:13px;outline:none;cursor:pointer}\n.filter-select:focus{border-color:var(--pri)}\n.date-filter{display:flex;align-items:center;gap:10px;margin-bottom:18px;font-size:13px;color:var(--text-sec)}\n.date-filter input[type=\"datetime-local\"]{padding:6px 10px;border:1px solid var(--border);border-radius:8px;font-size:12px;outline:none;background:var(--bg-card);color:var(--text)}\n.date-filter input[type=\"datetime-local\"]:focus{border-color:var(--pri)}\n.date-filter label{font-weight:500}\n\n.pagination{display:flex;align-items:center;justify-content:center;gap:6px;padding:28px 0;flex-wrap:wrap}\n.pagination .pg-btn{min-width:38px;height:38px;display:flex;align-items:center;justify-content:center;border:1px solid var(--border);border-radius:10px;background:var(--bg-card);color:var(--text-sec);font-size:13px;font-weight:500;cursor:pointer;transition:all .15s}\n.pagination .pg-btn:hover{border-color:var(--pri);color:var(--pri)}\n.pagination .pg-btn.active{background:var(--pri);color:#000;border-color:var(--pri)}\n.pagination .pg-btn.disabled{opacity:.4;pointer-events:none}\n.pagination .pg-info{font-size:12px;color:var(--text-sec);padding:0 12px}\n\n.theme-toggle{position:relative;width:40px;height:40px;padding:0;display:flex;align-items:center;justify-content:center;font-size:18px}\n.theme-toggle .theme-icon-light{display:none}\n.theme-toggle .theme-icon-dark{display:inline}\n[data-theme=\"light\"] .theme-toggle .theme-icon-light{display:inline}\n[data-theme=\"light\"] .theme-toggle .theme-icon-dark{display:none}\n\n.auth-theme-toggle{position:absolute;top:16px;right:16px;z-index:10;width:40px;height:40px;border:none;border-radius:10px;background:rgba(255,255,255,.25);backdrop-filter:blur(8px);color:inherit;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px;transition:background .2s}\n.auth-theme-toggle:hover{background:rgba(255,255,255,.4)}\n.auth-theme-toggle .theme-icon-light{display:none}\n.auth-theme-toggle .theme-icon-dark{display:inline}\n[data-theme=\"light\"] .auth-theme-toggle{background:rgba(0,0,0,.08);color:#0f172a}\n[data-theme=\"light\"] .auth-theme-toggle:hover{background:rgba(0,0,0,.12)}\n[data-theme=\"light\"] .auth-theme-toggle .theme-icon-light{display:inline}\n[data-theme=\"light\"] .auth-theme-toggle .theme-icon-dark{display:none}\n\n@media(max-width:900px){.main-content{flex-direction:column;padding:20px}.sidebar{width:100%}.sidebar .stats-grid{grid-template-columns:repeat(4,1fr)}}\n</style>\n</head>\n<body>\n\n<!-- \u2500\u2500\u2500 Auth: Setup Password \u2500\u2500\u2500 -->\n<div id=\"setupScreen\" class=\"auth-screen\" style=\"display:none\">\n <button class=\"auth-theme-toggle\" onclick=\"toggleViewerTheme()\" title=\"Toggle light/dark\" aria-label=\"Toggle theme\"><span class=\"theme-icon-dark\">\uD83C\uDF19</span><span class=\"theme-icon-light\">\u2600</span></button>\n <div class=\"auth-card\">\n <div class=\"logo\">\uD83E\uDD9E</div>\n <h1>OpenClaw Memory</h1>\n <p style=\"font-size:12px;color:var(--text-sec);margin-bottom:6px\">Powered by MemOS</p>\n <p>Set a password to protect your memories</p>\n <input type=\"password\" id=\"setupPw\" placeholder=\"Enter a password (4+ characters)\" autofocus>\n <input type=\"password\" id=\"setupPw2\" placeholder=\"Confirm password\">\n <button class=\"btn-auth\" onclick=\"doSetup()\">Set Password & Enter</button>\n <div class=\"error-msg\" id=\"setupErr\"></div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Auth: Login \u2500\u2500\u2500 -->\n<div id=\"loginScreen\" class=\"auth-screen\" style=\"display:none\">\n <button class=\"auth-theme-toggle\" onclick=\"toggleViewerTheme()\" title=\"Toggle light/dark\" aria-label=\"Toggle theme\"><span class=\"theme-icon-dark\">\uD83C\uDF19</span><span class=\"theme-icon-light\">\u2600</span></button>\n <div class=\"auth-card\">\n <div class=\"logo\">\uD83E\uDD9E</div>\n <h1>OpenClaw Memory</h1>\n <p style=\"font-size:12px;color:var(--text-sec);margin-bottom:6px\">Powered by MemOS</p>\n <p>Enter your password to access memories</p>\n <div id=\"loginForm\">\n <input type=\"password\" id=\"loginPw\" placeholder=\"Password\" autofocus>\n <button class=\"btn-auth\" onclick=\"doLogin()\">Unlock</button>\n <div class=\"error-msg\" id=\"loginErr\"></div>\n <button class=\"btn-text\" style=\"margin-top:12px;font-size:13px;color:var(--text-sec)\" onclick=\"showResetForm()\">Forgot password?</button>\n </div>\n <div id=\"resetForm\" style=\"display:none\">\n <div class=\"reset-guide\">\n <div class=\"reset-step\">\n <div class=\"step-num\">1</div>\n <div class=\"step-body\">\n <div class=\"step-title\">Open Terminal</div>\n <div class=\"step-desc\">Run the following command to get your reset token (use the pattern below so you get the line that contains the token):</div>\n <div class=\"cmd-box\" onclick=\"copyCmd(this)\">\n <code>grep \"password reset token:\" /tmp/openclaw/openclaw-*.log ~/.openclaw/logs/gateway.log 2>/dev/null | tail -1</code>\n <span class=\"copy-hint\">Click to copy</span>\n </div>\n </div>\n </div>\n <div class=\"reset-step\">\n <div class=\"step-num\">2</div>\n <div class=\"step-body\">\n <div class=\"step-title\">Find the token</div>\n <div class=\"step-desc\">In the output, find <span style=\"font-family:monospace;font-size:12px;color:var(--pri)\">password reset token: <strong>a1b2c3d4e5f6...</strong></span> (plain line or inside JSON). Copy the 32-character hex string after the colon.</div>\n </div>\n </div>\n <div class=\"reset-step\">\n <div class=\"step-num\">3</div>\n <div class=\"step-body\">\n <div class=\"step-title\">Paste & reset</div>\n <div class=\"step-desc\">Paste the token below and set your new password.</div>\n </div>\n </div>\n </div>\n <input type=\"text\" id=\"resetToken\" placeholder=\"Paste reset token here\" style=\"margin-bottom:8px;font-family:monospace\">\n <input type=\"password\" id=\"resetNewPw\" placeholder=\"New password (4+ characters)\">\n <input type=\"password\" id=\"resetNewPw2\" placeholder=\"Confirm new password\">\n <button class=\"btn-auth\" onclick=\"doReset()\">Reset Password</button>\n <div class=\"error-msg\" id=\"resetErr\"></div>\n <button class=\"btn-text\" style=\"margin-top:12px;font-size:13px;color:var(--text-sec)\" onclick=\"showLoginForm()\">\u2190 Back to login</button>\n </div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Main App \u2500\u2500\u2500 -->\n<div class=\"app\" id=\"app\">\n <div class=\"topbar\">\n <div class=\"brand\">\n <div class=\"icon\">\uD83E\uDD9E</div>\n <span>OpenClaw Memory <span style=\"font-weight:400;color:var(--text-sec);font-size:12px\">by MemOS</span></span>\n </div>\n <div class=\"actions\">\n <button class=\"btn btn-icon theme-toggle\" onclick=\"toggleViewerTheme()\" title=\"Toggle light/dark\" aria-label=\"Toggle theme\"><span class=\"theme-icon-dark\">\uD83C\uDF19</span><span class=\"theme-icon-light\">\u2600</span></button>\n <button class=\"btn btn-primary\" onclick=\"openCreateModal()\">+ New Memory</button>\n <button class=\"btn\" onclick=\"loadAll()\">Refresh</button>\n <button class=\"btn btn-danger\" onclick=\"clearAll()\">Clear All</button>\n <button class=\"btn btn-text\" onclick=\"doLogout()\">Logout</button>\n </div>\n </div>\n\n <div class=\"main-content\">\n <div class=\"sidebar\" id=\"sidebar\">\n <div class=\"stats-grid\" id=\"statsGrid\">\n <div class=\"stat-card pri\"><div class=\"stat-value\" id=\"statTotal\">-</div><div class=\"stat-label\">Memories</div></div>\n <div class=\"stat-card green\"><div class=\"stat-value\" id=\"statSessions\">-</div><div class=\"stat-label\">Sessions</div></div>\n <div class=\"stat-card amber\"><div class=\"stat-value\" id=\"statEmbeddings\">-</div><div class=\"stat-label\">Embeddings</div></div>\n <div class=\"stat-card rose\"><div class=\"stat-value\" id=\"statTimeSpan\">-</div><div class=\"stat-label\">Days</div></div>\n </div>\n <div id=\"embeddingStatus\"></div>\n <div class=\"section-title\">Sessions</div>\n <div class=\"session-list\" id=\"sessionList\"></div>\n </div>\n\n <div class=\"feed\">\n <div class=\"search-bar\">\n <span class=\"search-icon\">\uD83D\uDD0D</span>\n <input type=\"text\" id=\"searchInput\" placeholder=\"Search memories (supports semantic search)...\" oninput=\"debounceSearch()\">\n </div>\n <div class=\"search-meta\" id=\"searchMeta\"></div>\n <div class=\"filter-bar\" id=\"filterBar\">\n <button class=\"filter-chip active\" data-role=\"\" onclick=\"setRoleFilter(this,'')\">All</button>\n <button class=\"filter-chip\" data-role=\"user\" onclick=\"setRoleFilter(this,'user')\">User</button>\n <button class=\"filter-chip\" data-role=\"assistant\" onclick=\"setRoleFilter(this,'assistant')\">Assistant</button>\n <button class=\"filter-chip\" data-role=\"system\" onclick=\"setRoleFilter(this,'system')\">System</button>\n <span class=\"filter-sep\"></span>\n <select id=\"filterKind\" class=\"filter-select\" onchange=\"applyFilters()\">\n <option value=\"\">All kinds</option>\n <option value=\"paragraph\">Paragraph</option>\n <option value=\"code_block\">Code</option>\n <option value=\"dialog\">Dialog</option>\n <option value=\"list\">List</option>\n <option value=\"error_stack\">Error</option>\n <option value=\"command\">Command</option>\n </select>\n <select id=\"filterSort\" class=\"filter-select\" onchange=\"applyFilters()\">\n <option value=\"newest\">Newest first</option>\n <option value=\"oldest\">Oldest first</option>\n </select>\n </div>\n <div class=\"date-filter\">\n <label>From</label><input type=\"datetime-local\" id=\"dateFrom\" step=\"1\" onchange=\"applyFilters()\">\n <label>To</label><input type=\"datetime-local\" id=\"dateTo\" step=\"1\" onchange=\"applyFilters()\">\n <button class=\"btn btn-sm btn-text\" onclick=\"clearDateFilter()\">Clear</button>\n </div>\n <div class=\"memory-list\" id=\"memoryList\"><div class=\"spinner\"></div></div>\n <div class=\"pagination\" id=\"pagination\"></div>\n </div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Memory Modal \u2500\u2500\u2500 -->\n<div class=\"modal-overlay\" id=\"modalOverlay\">\n <div class=\"modal\">\n <h2 id=\"modalTitle\">New Memory</h2>\n <div class=\"form-group\"><label>Role</label><select id=\"mRole\"><option value=\"user\">User</option><option value=\"assistant\">Assistant</option><option value=\"system\">System</option></select></div>\n <div class=\"form-group\"><label>Content</label><textarea id=\"mContent\" rows=\"4\" placeholder=\"Memory content...\"></textarea></div>\n <div class=\"form-group\"><label>Summary</label><input type=\"text\" id=\"mSummary\" placeholder=\"Brief summary (optional)\"></div>\n <div class=\"form-group\"><label>Kind</label><select id=\"mKind\"><option value=\"paragraph\">Paragraph</option><option value=\"code\">Code</option><option value=\"dialog\">Dialog</option></select></div>\n <div class=\"modal-actions\">\n <button class=\"btn\" onclick=\"closeModal()\">Cancel</button>\n <button class=\"btn btn-primary\" id=\"modalSubmit\" onclick=\"submitModal()\">Create</button>\n </div>\n </div>\n</div>\n\n<!-- \u2500\u2500\u2500 Toast \u2500\u2500\u2500 -->\n<div class=\"toast-container\" id=\"toasts\"></div>\n\n<script>\nlet activeSession=null,activeRole='',editingId=null,searchTimer=null,memoryCache={},currentPage=1,totalPages=1,totalCount=0,PAGE_SIZE=30;\n\n/* \u2500\u2500\u2500 Auth flow \u2500\u2500\u2500 */\nasync function checkAuth(){\n const r=await fetch('/api/auth/status');\n const d=await r.json();\n if(d.needsSetup){\n document.getElementById('setupScreen').style.display='flex';\n document.getElementById('setupPw').addEventListener('keydown',e=>{if(e.key==='Enter')document.getElementById('setupPw2').focus()});\n document.getElementById('setupPw2').addEventListener('keydown',e=>{if(e.key==='Enter')doSetup()});\n } else if(!d.loggedIn){\n document.getElementById('loginScreen').style.display='flex';\n document.getElementById('loginPw').addEventListener('keydown',e=>{if(e.key==='Enter')doLogin()});\n } else {\n enterApp();\n }\n}\n\nasync function doSetup(){\n const pw=document.getElementById('setupPw').value;\n const pw2=document.getElementById('setupPw2').value;\n const err=document.getElementById('setupErr');\n if(pw.length<4){err.textContent='Password must be at least 4 characters';return}\n if(pw!==pw2){err.textContent='Passwords do not match';return}\n const r=await fetch('/api/auth/setup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pw})});\n const d=await r.json();\n if(d.ok){document.getElementById('setupScreen').style.display='none';enterApp();}\n else{err.textContent=d.error||'Setup failed'}\n}\n\nasync function doLogin(){\n const pw=document.getElementById('loginPw').value;\n const err=document.getElementById('loginErr');\n const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pw})});\n const d=await r.json();\n if(d.ok){document.getElementById('loginScreen').style.display='none';enterApp();}\n else{err.textContent='Incorrect password';document.getElementById('loginPw').value='';document.getElementById('loginPw').focus();}\n}\n\nasync function doLogout(){\n await fetch('/api/auth/logout',{method:'POST'});\n location.reload();\n}\n\nfunction showResetForm(){\n document.getElementById('loginForm').style.display='none';\n document.getElementById('resetForm').style.display='block';\n document.getElementById('resetToken').focus();\n}\n\nfunction showLoginForm(){\n document.getElementById('resetForm').style.display='none';\n document.getElementById('loginForm').style.display='block';\n document.getElementById('loginPw').focus();\n}\n\nfunction copyCmd(el){\n const code=el.querySelector('code').textContent;\n navigator.clipboard.writeText(code).then(()=>{\n el.classList.add('copied');\n el.querySelector('.copy-hint').textContent='Copied!';\n setTimeout(()=>{el.classList.remove('copied');el.querySelector('.copy-hint').textContent='Click to copy'},2000);\n });\n}\n\nasync function doReset(){\n const token=document.getElementById('resetToken').value.trim();\n const pw=document.getElementById('resetNewPw').value;\n const pw2=document.getElementById('resetNewPw2').value;\n const err=document.getElementById('resetErr');\n if(!token){err.textContent='Please enter the reset token';return}\n if(pw.length<4){err.textContent='Password must be at least 4 characters';return}\n if(pw!==pw2){err.textContent='Passwords do not match';return}\n const r=await fetch('/api/auth/reset',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token,newPassword:pw})});\n const d=await r.json();\n if(d.ok){document.getElementById('loginScreen').style.display='none';enterApp();}\n else{err.textContent=d.error||'Reset failed'}\n}\n\nfunction enterApp(){\n document.getElementById('app').style.display='flex';\n loadAll();\n}\n\n/* \u2500\u2500\u2500 Data loading \u2500\u2500\u2500 */\nasync function loadAll(){\n await Promise.all([loadStats(),loadMemories()]);\n}\n\nasync function loadStats(){\n const r=await fetch('/api/stats');\n const d=await r.json();\n document.getElementById('statTotal').textContent=d.totalMemories;\n document.getElementById('statSessions').textContent=d.totalSessions;\n document.getElementById('statEmbeddings').textContent=d.totalEmbeddings;\n const days=d.timeRange.earliest?Math.max(1,Math.round((new Date(d.timeRange.latest)-new Date(d.timeRange.earliest))/(86400000))):0;\n document.getElementById('statTimeSpan').textContent=days;\n\n const provEl=document.getElementById('embeddingStatus');\n if(d.embeddingProvider && d.embeddingProvider!=='none'){\n provEl.innerHTML='<div class=\"provider-badge\"><span>\\u2713</span> Embedding: '+d.embeddingProvider+'</div>';\n } else {\n provEl.innerHTML='<div class=\"provider-badge offline\"><span>\\u26A0</span> No embedding model</div>';\n }\n\n const sl=document.getElementById('sessionList');\n sl.innerHTML='<div class=\"session-item'+(activeSession===null?' active':'')+'\" onclick=\"filterSession(null)\"><span>All Sessions</span><span class=\"count\">'+d.totalMemories+'</span></div>';\n (d.sessions||[]).forEach(s=>{\n const isActive=activeSession===s.session_key;\n const name=s.session_key.length>20?s.session_key.slice(0,8)+'...'+s.session_key.slice(-8):s.session_key;\n 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>';\n });\n}\n\nfunction getFilterParams(){\n const p=new URLSearchParams();\n if(activeSession) p.set('session',activeSession);\n if(activeRole) p.set('role',activeRole);\n const kind=document.getElementById('filterKind').value;\n if(kind) p.set('kind',kind);\n const df=document.getElementById('dateFrom').value;\n if(df) p.set('dateFrom',df);\n const dt=document.getElementById('dateTo').value;\n if(dt) p.set('dateTo',dt);\n const sort=document.getElementById('filterSort').value;\n if(sort==='oldest') p.set('sort','oldest');\n return p;\n}\n\nasync function loadMemories(page){\n if(page) currentPage=page;\n const list=document.getElementById('memoryList');\n list.innerHTML='<div class=\"spinner\"></div>';\n const p=getFilterParams();\n p.set('limit',PAGE_SIZE);\n p.set('page',currentPage);\n const r=await fetch('/api/memories?'+p.toString());\n const d=await r.json();\n totalPages=d.totalPages||1;\n totalCount=d.total||0;\n document.getElementById('searchMeta').textContent=totalCount+' memories total';\n renderMemories(d.memories||[]);\n renderPagination();\n}\n\nasync function doSearch(q){\n if(!q.trim()){currentPage=1;loadMemories();return}\n const list=document.getElementById('memoryList');\n list.innerHTML='<div class=\"spinner\"></div>';\n const p=getFilterParams();\n p.set('q',q);\n const r=await fetch('/api/search?'+p.toString());\n const d=await r.json();\n const meta=[];\n if(d.vectorCount>0) meta.push(d.vectorCount+' semantic');\n if(d.ftsCount>0) meta.push(d.ftsCount+' text');\n meta.push(d.total+' results');\n document.getElementById('searchMeta').textContent=meta.join(' \\u00B7 ');\n renderMemories(d.results||[]);\n document.getElementById('pagination').innerHTML='';\n}\n\nfunction debounceSearch(){\n clearTimeout(searchTimer);\n searchTimer=setTimeout(()=>doSearch(document.getElementById('searchInput').value),350);\n}\n\nfunction filterSession(key){\n activeSession=key;\n currentPage=1;\n loadAll();\n}\n\nfunction setRoleFilter(btn,role){\n activeRole=role;\n currentPage=1;\n document.querySelectorAll('.filter-chip').forEach(c=>c.classList.remove('active'));\n btn.classList.add('active');\n applyFilters();\n}\n\nfunction applyFilters(){\n currentPage=1;\n if(document.getElementById('searchInput').value.trim()){\n doSearch(document.getElementById('searchInput').value);\n } else {\n loadMemories();\n }\n}\n\nfunction clearDateFilter(){\n document.getElementById('dateFrom').value='';\n document.getElementById('dateTo').value='';\n applyFilters();\n}\n\n/* \u2500\u2500\u2500 Rendering \u2500\u2500\u2500 */\nfunction renderMemories(items){\n const list=document.getElementById('memoryList');\n if(!items.length){\n list.innerHTML='<div class=\"empty\"><div class=\"icon\">\\u{1F4ED}</div><p>No memories found</p></div>';\n return;\n }\n items.forEach(m=>{memoryCache[m.id]=m});\n list.innerHTML=items.map(m=>{\n const time=m.created_at?new Date(typeof m.created_at==='number'?m.created_at:m.created_at).toLocaleString('zh-CN'):'';\n const role=m.role||'user';\n const kind=m.kind||'paragraph';\n const summary=esc(m.summary||m.content?.slice(0,120)||'');\n const content=esc(m.content||'');\n const id=m.id;\n const vscore=m._vscore?'<span class=\"vscore-badge\">'+Math.round(m._vscore*100)+'%</span>':'';\n const sid=m.session_key||'';\n const sidShort=sid.length>18?sid.slice(0,6)+'..'+sid.slice(-6):sid;\n return '<div class=\"memory-card\">'+\n '<div class=\"card-header\"><div class=\"meta\"><span class=\"role-tag '+role+'\">'+role+'</span><span class=\"kind-tag\">'+kind+'</span></div><span class=\"card-time\"><span class=\"session-tag\" title=\"'+esc(sid)+'\">'+esc(sidShort)+'</span> '+time+'</span></div>'+\n '<div class=\"card-summary\">'+summary+'</div>'+\n '<div class=\"card-content\" id=\"content-'+id+'\"><pre>'+content+'</pre></div>'+\n '<div class=\"card-actions\">'+\n '<button class=\"btn btn-sm btn-text\" onclick=\"toggleContent(\\''+id+'\\')\">Expand</button>'+\n '<button class=\"btn btn-sm\" onclick=\"openEditModal(\\''+id+'\\')\">Edit</button>'+\n '<button class=\"btn btn-sm btn-danger\" onclick=\"deleteMemory(\\''+id+'\\')\">Delete</button>'+\n vscore+\n '</div></div>';\n }).join('');\n}\n\nfunction renderPagination(){\n const el=document.getElementById('pagination');\n if(totalPages<=1){el.innerHTML='';return}\n let h='';\n h+='<button class=\"pg-btn'+(currentPage<=1?' disabled':'')+'\" onclick=\"goPage('+(currentPage-1)+')\">\u2039</button>';\n const range=[];\n range.push(1);\n for(let i=Math.max(2,currentPage-2);i<=Math.min(totalPages-1,currentPage+2);i++) range.push(i);\n if(totalPages>1) range.push(totalPages);\n const unique=[...new Set(range)].sort((a,b)=>a-b);\n let prev=0;\n for(const p of unique){\n if(p-prev>1) h+='<span class=\"pg-info\">...</span>';\n h+='<button class=\"pg-btn'+(p===currentPage?' active':'')+'\" onclick=\"goPage('+p+')\">'+p+'</button>';\n prev=p;\n }\n h+='<button class=\"pg-btn'+(currentPage>=totalPages?' disabled':'')+'\" onclick=\"goPage('+(currentPage+1)+')\">\u203A</button>';\n h+='<span class=\"pg-info\">'+totalCount+' total</span>';\n el.innerHTML=h;\n}\n\nfunction goPage(p){\n if(p<1||p>totalPages||p===currentPage) return;\n currentPage=p;\n loadMemories();\n document.getElementById('memoryList').scrollIntoView({behavior:'smooth',block:'start'});\n}\n\nfunction toggleContent(id){\n const el=document.getElementById('content-'+id);\n el.classList.toggle('show');\n}\n\nfunction esc(s){\n if(!s)return'';\n return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\"/g,'"');\n}\n\n/* \u2500\u2500\u2500 CRUD \u2500\u2500\u2500 */\nfunction openCreateModal(){\n editingId=null;\n document.getElementById('modalTitle').textContent='New Memory';\n document.getElementById('modalSubmit').textContent='Create';\n document.getElementById('mRole').value='user';\n document.getElementById('mContent').value='';\n document.getElementById('mSummary').value='';\n document.getElementById('mKind').value='paragraph';\n document.getElementById('modalOverlay').classList.add('show');\n}\n\nfunction openEditModal(id){\n const m=memoryCache[id];\n if(!m){toast('Memory not found in cache','error');return}\n editingId=id;\n document.getElementById('modalTitle').textContent='Edit Memory';\n document.getElementById('modalSubmit').textContent='Save';\n document.getElementById('mRole').value=m.role||'user';\n document.getElementById('mContent').value=m.content||'';\n document.getElementById('mSummary').value=m.summary||'';\n document.getElementById('mKind').value=m.kind||'paragraph';\n document.getElementById('modalOverlay').classList.add('show');\n}\n\nfunction closeModal(){\n document.getElementById('modalOverlay').classList.remove('show');\n}\n\nasync function submitModal(){\n const data={\n role:document.getElementById('mRole').value,\n content:document.getElementById('mContent').value,\n summary:document.getElementById('mSummary').value,\n kind:document.getElementById('mKind').value,\n };\n if(!data.content.trim()){toast('Please enter content','error');return}\n let r;\n if(editingId){\n r=await fetch('/api/memory/'+editingId,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});\n } else {\n r=await fetch('/api/memory',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});\n }\n const d=await r.json();\n if(d.ok){toast(editingId?'Memory updated':'Memory created','success');closeModal();loadAll();}\n else{toast(d.error||'Operation failed','error')}\n}\n\nasync function deleteMemory(id){\n if(!confirm('Delete this memory?'))return;\n const r=await fetch('/api/memory/'+id,{method:'DELETE'});\n const d=await r.json();\n if(d.ok){toast('Memory deleted','success');loadAll();}\n else{toast('Delete failed','error')}\n}\n\nasync function clearAll(){\n if(!confirm('Delete ALL memories? This cannot be undone.'))return;\n if(!confirm('Are you absolutely sure?'))return;\n const r=await fetch('/api/memories',{method:'DELETE'});\n const d=await r.json();\n if(d.ok){toast('All memories cleared','success');loadAll();}\n else{toast('Clear failed','error')}\n}\n\n/* \u2500\u2500\u2500 Toast \u2500\u2500\u2500 */\nfunction toast(msg,type='info'){\n const c=document.getElementById('toasts');\n const t=document.createElement('div');\n t.className='toast '+type;\n const icons={success:'\\u2705',error:'\\u274C',info:'\\u2139\\uFE0F'};\n t.innerHTML=(icons[type]||'')+' '+esc(msg);\n c.appendChild(t);\n setTimeout(()=>t.remove(),3500);\n}\n\n/* \u2500\u2500\u2500 Theme \u2500\u2500\u2500 */\nconst VIEWER_THEME_KEY='memos-viewer-theme';\nfunction initViewerTheme(){const s=localStorage.getItem(VIEWER_THEME_KEY);const theme=(s==='light'||s==='dark')?s:'dark';document.documentElement.setAttribute('data-theme',theme);}\nfunction toggleViewerTheme(){const el=document.documentElement;const cur=el.getAttribute('data-theme')||'dark';const next=cur==='dark'?'light':'dark';el.setAttribute('data-theme',next);localStorage.setItem(VIEWER_THEME_KEY,next);}\ninitViewerTheme();\n\n/* \u2500\u2500\u2500 Init \u2500\u2500\u2500 */\ndocument.getElementById('modalOverlay').addEventListener('click',e=>{if(e.target.id==='modalOverlay')closeModal()});\ndocument.getElementById('searchInput').addEventListener('keydown',e=>{if(e.key==='Escape'){e.target.value='';loadMemories()}});\ncheckAuth();\n</script>\n</body>\n</html>";
|
|
2
2
|
//# sourceMappingURL=html.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/viewer/html.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,
|
|
1
|
+
{"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/viewer/html.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,4yyCAqtBf,CAAC"}
|
package/dist/viewer/html.js
CHANGED
|
@@ -26,7 +26,27 @@ exports.viewerHTML = `<!DOCTYPE html>
|
|
|
26
26
|
--shadow-lg:0 20px 40px rgba(0,0,0,.35);
|
|
27
27
|
--radius:12px;--radius-lg:14px;--radius-xl:18px;
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
[data-theme="light"]{
|
|
30
|
+
--bg:#f1f5f9;--bg-card:#fff;--bg-card-hover:#f8fafc;
|
|
31
|
+
--border:rgba(0,0,0,.08);--border-glow:rgba(0,187,238,.4);
|
|
32
|
+
--text:#0f172a;--text-sec:#475569;--text-muted:#64748b;
|
|
33
|
+
--pri:#0891b2;--pri-glow:rgba(8,145,178,.12);--pri-dark:#0e7490;
|
|
34
|
+
--pri-grad:linear-gradient(135deg,#0891b2,#0e7490);
|
|
35
|
+
--accent:#dc2626;--accent-glow:rgba(220,38,38,.1);
|
|
36
|
+
--green:#059669;--green-bg:rgba(5,150,105,.1);
|
|
37
|
+
--amber:#d97706;--amber-bg:rgba(217,119,6,.1);
|
|
38
|
+
--violet:#7c3aed;--rose:#e11d48;--rose-bg:rgba(225,29,72,.1);
|
|
39
|
+
--shadow-sm:0 1px 2px rgba(0,0,0,.06);--shadow:0 4px 12px rgba(0,0,0,.08);
|
|
40
|
+
--shadow-lg:0 20px 40px rgba(0,0,0,.12);
|
|
41
|
+
}
|
|
42
|
+
[data-theme="light"] .auth-screen{background:linear-gradient(135deg,#e0f2fe 0%,#f0f9ff 50%,#e0e7ff 100%)}
|
|
43
|
+
[data-theme="light"] .auth-card{box-shadow:0 25px 50px -12px rgba(0,0,0,.12)}
|
|
44
|
+
[data-theme="light"] .topbar{background:rgba(255,255,255,.9);border-bottom-color:var(--border)}
|
|
45
|
+
[data-theme="light"] .session-item .count,[data-theme="light"] .kind-tag,[data-theme="light"] .session-tag{background:rgba(0,0,0,.06)}
|
|
46
|
+
[data-theme="light"] .card-content pre{background:rgba(0,0,0,.05);border-color:var(--border)}
|
|
47
|
+
[data-theme="light"] .vscore-badge{background:linear-gradient(135deg,var(--pri),var(--violet))}
|
|
48
|
+
[data-theme="light"] ::-webkit-scrollbar-thumb{background:rgba(0,0,0,.2)}
|
|
49
|
+
body{font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;transition:background .2s,color .2s}
|
|
30
50
|
button{cursor:pointer;font-family:inherit;font-size:inherit}
|
|
31
51
|
input,textarea,select{font-family:inherit;font-size:inherit}
|
|
32
52
|
|
|
@@ -181,6 +201,21 @@ input,textarea,select{font-family:inherit;font-size:inherit}
|
|
|
181
201
|
.pagination .pg-btn.disabled{opacity:.4;pointer-events:none}
|
|
182
202
|
.pagination .pg-info{font-size:12px;color:var(--text-sec);padding:0 12px}
|
|
183
203
|
|
|
204
|
+
.theme-toggle{position:relative;width:40px;height:40px;padding:0;display:flex;align-items:center;justify-content:center;font-size:18px}
|
|
205
|
+
.theme-toggle .theme-icon-light{display:none}
|
|
206
|
+
.theme-toggle .theme-icon-dark{display:inline}
|
|
207
|
+
[data-theme="light"] .theme-toggle .theme-icon-light{display:inline}
|
|
208
|
+
[data-theme="light"] .theme-toggle .theme-icon-dark{display:none}
|
|
209
|
+
|
|
210
|
+
.auth-theme-toggle{position:absolute;top:16px;right:16px;z-index:10;width:40px;height:40px;border:none;border-radius:10px;background:rgba(255,255,255,.25);backdrop-filter:blur(8px);color:inherit;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px;transition:background .2s}
|
|
211
|
+
.auth-theme-toggle:hover{background:rgba(255,255,255,.4)}
|
|
212
|
+
.auth-theme-toggle .theme-icon-light{display:none}
|
|
213
|
+
.auth-theme-toggle .theme-icon-dark{display:inline}
|
|
214
|
+
[data-theme="light"] .auth-theme-toggle{background:rgba(0,0,0,.08);color:#0f172a}
|
|
215
|
+
[data-theme="light"] .auth-theme-toggle:hover{background:rgba(0,0,0,.12)}
|
|
216
|
+
[data-theme="light"] .auth-theme-toggle .theme-icon-light{display:inline}
|
|
217
|
+
[data-theme="light"] .auth-theme-toggle .theme-icon-dark{display:none}
|
|
218
|
+
|
|
184
219
|
@media(max-width:900px){.main-content{flex-direction:column;padding:20px}.sidebar{width:100%}.sidebar .stats-grid{grid-template-columns:repeat(4,1fr)}}
|
|
185
220
|
</style>
|
|
186
221
|
</head>
|
|
@@ -188,6 +223,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
|
|
|
188
223
|
|
|
189
224
|
<!-- ─── Auth: Setup Password ─── -->
|
|
190
225
|
<div id="setupScreen" class="auth-screen" style="display:none">
|
|
226
|
+
<button class="auth-theme-toggle" onclick="toggleViewerTheme()" title="Toggle light/dark" aria-label="Toggle theme"><span class="theme-icon-dark">\u{1F319}</span><span class="theme-icon-light">\u2600</span></button>
|
|
191
227
|
<div class="auth-card">
|
|
192
228
|
<div class="logo">\u{1F99E}</div>
|
|
193
229
|
<h1>OpenClaw Memory</h1>
|
|
@@ -202,6 +238,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
|
|
|
202
238
|
|
|
203
239
|
<!-- ─── Auth: Login ─── -->
|
|
204
240
|
<div id="loginScreen" class="auth-screen" style="display:none">
|
|
241
|
+
<button class="auth-theme-toggle" onclick="toggleViewerTheme()" title="Toggle light/dark" aria-label="Toggle theme"><span class="theme-icon-dark">\u{1F319}</span><span class="theme-icon-light">\u2600</span></button>
|
|
205
242
|
<div class="auth-card">
|
|
206
243
|
<div class="logo">\u{1F99E}</div>
|
|
207
244
|
<h1>OpenClaw Memory</h1>
|
|
@@ -259,6 +296,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
|
|
|
259
296
|
<span>OpenClaw Memory <span style="font-weight:400;color:var(--text-sec);font-size:12px">by MemOS</span></span>
|
|
260
297
|
</div>
|
|
261
298
|
<div class="actions">
|
|
299
|
+
<button class="btn btn-icon theme-toggle" onclick="toggleViewerTheme()" title="Toggle light/dark" aria-label="Toggle theme"><span class="theme-icon-dark">\u{1F319}</span><span class="theme-icon-light">\u2600</span></button>
|
|
262
300
|
<button class="btn btn-primary" onclick="openCreateModal()">+ New Memory</button>
|
|
263
301
|
<button class="btn" onclick="loadAll()">Refresh</button>
|
|
264
302
|
<button class="btn btn-danger" onclick="clearAll()">Clear All</button>
|
|
@@ -676,6 +714,12 @@ function toast(msg,type='info'){
|
|
|
676
714
|
setTimeout(()=>t.remove(),3500);
|
|
677
715
|
}
|
|
678
716
|
|
|
717
|
+
/* ─── Theme ─── */
|
|
718
|
+
const VIEWER_THEME_KEY='memos-viewer-theme';
|
|
719
|
+
function initViewerTheme(){const s=localStorage.getItem(VIEWER_THEME_KEY);const theme=(s==='light'||s==='dark')?s:'dark';document.documentElement.setAttribute('data-theme',theme);}
|
|
720
|
+
function toggleViewerTheme(){const el=document.documentElement;const cur=el.getAttribute('data-theme')||'dark';const next=cur==='dark'?'light':'dark';el.setAttribute('data-theme',next);localStorage.setItem(VIEWER_THEME_KEY,next);}
|
|
721
|
+
initViewerTheme();
|
|
722
|
+
|
|
679
723
|
/* ─── Init ─── */
|
|
680
724
|
document.getElementById('modalOverlay').addEventListener('click',e=>{if(e.target.id==='modalOverlay')closeModal()});
|
|
681
725
|
document.getElementById('searchInput').addEventListener('keydown',e=>{if(e.key==='Escape'){e.target.value='';loadMemories()}});
|
package/dist/viewer/html.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html.js","sourceRoot":"","sources":["../../src/viewer/html.ts"],"names":[],"mappings":";;;AAAa,QAAA,UAAU,GAAG
|
|
1
|
+
{"version":3,"file":"html.js","sourceRoot":"","sources":["../../src/viewer/html.ts"],"names":[],"mappings":";;;AAAa,QAAA,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAqtBlB,CAAC"}
|
package/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenClaw Plugin Entry
|
|
2
|
+
* OpenClaw Plugin Entry — memos-local
|
|
3
3
|
*
|
|
4
4
|
* Full-write local memory with hybrid retrieval (RRF + MMR + recency).
|
|
5
|
-
* Provides: memory_search, memory_timeline, memory_get
|
|
5
|
+
* Provides: memory_search, memory_timeline, memory_get, memory_viewer
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
@@ -235,6 +235,8 @@ const memosLocalPlugin = {
|
|
|
235
235
|
|
|
236
236
|
// ─── Tool: memory_viewer ───
|
|
237
237
|
|
|
238
|
+
const viewerPort = (pluginCfg as any).viewerPort ?? 18799;
|
|
239
|
+
|
|
238
240
|
api.registerTool(
|
|
239
241
|
{
|
|
240
242
|
name: "memory_viewer",
|
|
@@ -316,7 +318,6 @@ const memosLocalPlugin = {
|
|
|
316
318
|
|
|
317
319
|
// ─── Memory Viewer (web UI) ───
|
|
318
320
|
|
|
319
|
-
const viewerPort = (pluginCfg as any).viewerPort ?? 18799;
|
|
320
321
|
const viewer = new ViewerServer({
|
|
321
322
|
store,
|
|
322
323
|
embedder,
|
package/package.json
CHANGED
package/src/viewer/html.ts
CHANGED
|
@@ -23,7 +23,27 @@ export const viewerHTML = `<!DOCTYPE html>
|
|
|
23
23
|
--shadow-lg:0 20px 40px rgba(0,0,0,.35);
|
|
24
24
|
--radius:12px;--radius-lg:14px;--radius-xl:18px;
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
[data-theme="light"]{
|
|
27
|
+
--bg:#f1f5f9;--bg-card:#fff;--bg-card-hover:#f8fafc;
|
|
28
|
+
--border:rgba(0,0,0,.08);--border-glow:rgba(0,187,238,.4);
|
|
29
|
+
--text:#0f172a;--text-sec:#475569;--text-muted:#64748b;
|
|
30
|
+
--pri:#0891b2;--pri-glow:rgba(8,145,178,.12);--pri-dark:#0e7490;
|
|
31
|
+
--pri-grad:linear-gradient(135deg,#0891b2,#0e7490);
|
|
32
|
+
--accent:#dc2626;--accent-glow:rgba(220,38,38,.1);
|
|
33
|
+
--green:#059669;--green-bg:rgba(5,150,105,.1);
|
|
34
|
+
--amber:#d97706;--amber-bg:rgba(217,119,6,.1);
|
|
35
|
+
--violet:#7c3aed;--rose:#e11d48;--rose-bg:rgba(225,29,72,.1);
|
|
36
|
+
--shadow-sm:0 1px 2px rgba(0,0,0,.06);--shadow:0 4px 12px rgba(0,0,0,.08);
|
|
37
|
+
--shadow-lg:0 20px 40px rgba(0,0,0,.12);
|
|
38
|
+
}
|
|
39
|
+
[data-theme="light"] .auth-screen{background:linear-gradient(135deg,#e0f2fe 0%,#f0f9ff 50%,#e0e7ff 100%)}
|
|
40
|
+
[data-theme="light"] .auth-card{box-shadow:0 25px 50px -12px rgba(0,0,0,.12)}
|
|
41
|
+
[data-theme="light"] .topbar{background:rgba(255,255,255,.9);border-bottom-color:var(--border)}
|
|
42
|
+
[data-theme="light"] .session-item .count,[data-theme="light"] .kind-tag,[data-theme="light"] .session-tag{background:rgba(0,0,0,.06)}
|
|
43
|
+
[data-theme="light"] .card-content pre{background:rgba(0,0,0,.05);border-color:var(--border)}
|
|
44
|
+
[data-theme="light"] .vscore-badge{background:linear-gradient(135deg,var(--pri),var(--violet))}
|
|
45
|
+
[data-theme="light"] ::-webkit-scrollbar-thumb{background:rgba(0,0,0,.2)}
|
|
46
|
+
body{font-family:'Inter',-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;transition:background .2s,color .2s}
|
|
27
47
|
button{cursor:pointer;font-family:inherit;font-size:inherit}
|
|
28
48
|
input,textarea,select{font-family:inherit;font-size:inherit}
|
|
29
49
|
|
|
@@ -178,6 +198,21 @@ input,textarea,select{font-family:inherit;font-size:inherit}
|
|
|
178
198
|
.pagination .pg-btn.disabled{opacity:.4;pointer-events:none}
|
|
179
199
|
.pagination .pg-info{font-size:12px;color:var(--text-sec);padding:0 12px}
|
|
180
200
|
|
|
201
|
+
.theme-toggle{position:relative;width:40px;height:40px;padding:0;display:flex;align-items:center;justify-content:center;font-size:18px}
|
|
202
|
+
.theme-toggle .theme-icon-light{display:none}
|
|
203
|
+
.theme-toggle .theme-icon-dark{display:inline}
|
|
204
|
+
[data-theme="light"] .theme-toggle .theme-icon-light{display:inline}
|
|
205
|
+
[data-theme="light"] .theme-toggle .theme-icon-dark{display:none}
|
|
206
|
+
|
|
207
|
+
.auth-theme-toggle{position:absolute;top:16px;right:16px;z-index:10;width:40px;height:40px;border:none;border-radius:10px;background:rgba(255,255,255,.25);backdrop-filter:blur(8px);color:inherit;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px;transition:background .2s}
|
|
208
|
+
.auth-theme-toggle:hover{background:rgba(255,255,255,.4)}
|
|
209
|
+
.auth-theme-toggle .theme-icon-light{display:none}
|
|
210
|
+
.auth-theme-toggle .theme-icon-dark{display:inline}
|
|
211
|
+
[data-theme="light"] .auth-theme-toggle{background:rgba(0,0,0,.08);color:#0f172a}
|
|
212
|
+
[data-theme="light"] .auth-theme-toggle:hover{background:rgba(0,0,0,.12)}
|
|
213
|
+
[data-theme="light"] .auth-theme-toggle .theme-icon-light{display:inline}
|
|
214
|
+
[data-theme="light"] .auth-theme-toggle .theme-icon-dark{display:none}
|
|
215
|
+
|
|
181
216
|
@media(max-width:900px){.main-content{flex-direction:column;padding:20px}.sidebar{width:100%}.sidebar .stats-grid{grid-template-columns:repeat(4,1fr)}}
|
|
182
217
|
</style>
|
|
183
218
|
</head>
|
|
@@ -185,6 +220,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
|
|
|
185
220
|
|
|
186
221
|
<!-- ─── Auth: Setup Password ─── -->
|
|
187
222
|
<div id="setupScreen" class="auth-screen" style="display:none">
|
|
223
|
+
<button class="auth-theme-toggle" onclick="toggleViewerTheme()" title="Toggle light/dark" aria-label="Toggle theme"><span class="theme-icon-dark">\u{1F319}</span><span class="theme-icon-light">\u2600</span></button>
|
|
188
224
|
<div class="auth-card">
|
|
189
225
|
<div class="logo">\u{1F99E}</div>
|
|
190
226
|
<h1>OpenClaw Memory</h1>
|
|
@@ -199,6 +235,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
|
|
|
199
235
|
|
|
200
236
|
<!-- ─── Auth: Login ─── -->
|
|
201
237
|
<div id="loginScreen" class="auth-screen" style="display:none">
|
|
238
|
+
<button class="auth-theme-toggle" onclick="toggleViewerTheme()" title="Toggle light/dark" aria-label="Toggle theme"><span class="theme-icon-dark">\u{1F319}</span><span class="theme-icon-light">\u2600</span></button>
|
|
202
239
|
<div class="auth-card">
|
|
203
240
|
<div class="logo">\u{1F99E}</div>
|
|
204
241
|
<h1>OpenClaw Memory</h1>
|
|
@@ -256,6 +293,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
|
|
|
256
293
|
<span>OpenClaw Memory <span style="font-weight:400;color:var(--text-sec);font-size:12px">by MemOS</span></span>
|
|
257
294
|
</div>
|
|
258
295
|
<div class="actions">
|
|
296
|
+
<button class="btn btn-icon theme-toggle" onclick="toggleViewerTheme()" title="Toggle light/dark" aria-label="Toggle theme"><span class="theme-icon-dark">\u{1F319}</span><span class="theme-icon-light">\u2600</span></button>
|
|
259
297
|
<button class="btn btn-primary" onclick="openCreateModal()">+ New Memory</button>
|
|
260
298
|
<button class="btn" onclick="loadAll()">Refresh</button>
|
|
261
299
|
<button class="btn btn-danger" onclick="clearAll()">Clear All</button>
|
|
@@ -673,6 +711,12 @@ function toast(msg,type='info'){
|
|
|
673
711
|
setTimeout(()=>t.remove(),3500);
|
|
674
712
|
}
|
|
675
713
|
|
|
714
|
+
/* ─── Theme ─── */
|
|
715
|
+
const VIEWER_THEME_KEY='memos-viewer-theme';
|
|
716
|
+
function initViewerTheme(){const s=localStorage.getItem(VIEWER_THEME_KEY);const theme=(s==='light'||s==='dark')?s:'dark';document.documentElement.setAttribute('data-theme',theme);}
|
|
717
|
+
function toggleViewerTheme(){const el=document.documentElement;const cur=el.getAttribute('data-theme')||'dark';const next=cur==='dark'?'light':'dark';el.setAttribute('data-theme',next);localStorage.setItem(VIEWER_THEME_KEY,next);}
|
|
718
|
+
initViewerTheme();
|
|
719
|
+
|
|
676
720
|
/* ─── Init ─── */
|
|
677
721
|
document.getElementById('modalOverlay').addEventListener('click',e=>{if(e.target.id==='modalOverlay')closeModal()});
|
|
678
722
|
document.getElementById('searchInput').addEventListener('keydown',e=>{if(e.key==='Escape'){e.target.value='';loadMemories()}});
|
package/www/index.html
CHANGED
|
@@ -21,8 +21,22 @@
|
|
|
21
21
|
--font:'Inter',system-ui,-apple-system,sans-serif;
|
|
22
22
|
--mono:'SF Mono','Fira Code','JetBrains Mono',monospace;
|
|
23
23
|
}
|
|
24
|
+
[data-theme="light"]{
|
|
25
|
+
--bg:#f8fafc;--bg-card:#fff;--bg-card-hover:#f1f5f9;
|
|
26
|
+
--border:rgba(0,0,0,.08);--border-glow:rgba(0,187,238,.35);
|
|
27
|
+
--text:#0f172a;--text-sec:#475569;--text-thr:#64748b;
|
|
28
|
+
--pri:#0891b2;--pri-glow:rgba(8,145,178,.12);--pri-dark:#0e7490;
|
|
29
|
+
--accent:#dc2626;--accent-glow:rgba(220,38,38,.1);
|
|
30
|
+
--green:#059669;--violet:#7c3aed;--amber:#d97706;
|
|
31
|
+
}
|
|
32
|
+
[data-theme="light"] .page-bg::before{background:radial-gradient(ellipse 80% 60% at 50% -10%,rgba(8,145,178,.08),transparent 60%),radial-gradient(ellipse 50% 40% at 80% 20%,rgba(124,58,237,.06),transparent 50%)}
|
|
33
|
+
[data-theme="light"] nav{background:rgba(255,255,255,.9);border-bottom-color:var(--border)}
|
|
34
|
+
[data-theme="light"] .terminal{background:rgba(248,250,252,.95);border-color:var(--border)}
|
|
35
|
+
[data-theme="light"] .terminal-body{color:#334155}
|
|
36
|
+
[data-theme="light"] .code-block{background:rgba(241,245,249,.9);border-color:var(--border)}
|
|
37
|
+
[data-theme="light"] .code-block pre{color:#0f172a}
|
|
24
38
|
html{scroll-behavior:smooth}
|
|
25
|
-
body{font-family:var(--font);color:var(--text);background:var(--bg);line-height:1.7;overflow-x:hidden}
|
|
39
|
+
body{font-family:var(--font);color:var(--text);background:var(--bg);line-height:1.7;overflow-x:hidden;transition:background .2s,color .2s}
|
|
26
40
|
a{color:var(--pri);text-decoration:none;transition:color .2s}
|
|
27
41
|
a:hover{color:#4dd9ff}
|
|
28
42
|
|
|
@@ -178,6 +192,13 @@ body.lang-zh .lang-en{display:none !important}
|
|
|
178
192
|
.lang-switch .lang-btn.active{background:var(--pri);color:#000;box-shadow:0 1px 3px rgba(0,0,0,.2)}
|
|
179
193
|
.lang-switch .lang-btn.active:hover{background:#4dd9ff;color:#000}
|
|
180
194
|
|
|
195
|
+
.theme-toggle-btn{width:36px;height:36px;padding:0;border:1px solid var(--border);border-radius:50%;background:var(--bg-card);color:var(--text-sec);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px;transition:all .2s;flex-shrink:0}
|
|
196
|
+
.theme-toggle-btn:hover{border-color:var(--pri);color:var(--pri)}
|
|
197
|
+
.theme-toggle-btn .icon-sun{display:none}
|
|
198
|
+
.theme-toggle-btn .icon-moon{display:inline}
|
|
199
|
+
[data-theme="light"] .theme-toggle-btn .icon-sun{display:inline}
|
|
200
|
+
[data-theme="light"] .theme-toggle-btn .icon-moon{display:none}
|
|
201
|
+
|
|
181
202
|
/* ── Security/Local theme ── */
|
|
182
203
|
.hero-tagline{font-size:14px;color:var(--green);margin-top:12px;display:flex;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap}
|
|
183
204
|
.hero-tagline span{display:inline-flex;align-items:center;gap:6px}
|
|
@@ -206,6 +227,7 @@ body.lang-zh .lang-en{display:none !important}
|
|
|
206
227
|
<a href="#providers" class="lang-en">Providers</a>
|
|
207
228
|
<a href="./docs/index.html" class="btn-nav lang-zh">文档 →</a>
|
|
208
229
|
<a href="./docs/index.html" class="btn-nav lang-en">Docs →</a>
|
|
230
|
+
<button type="button" class="theme-toggle-btn" onclick="toggleLandingTheme()" title="Toggle light/dark" aria-label="Toggle theme"><span class="icon-moon">🌙</span><span class="icon-sun">☀</span></button>
|
|
209
231
|
<span class="lang-switch" aria-label="Language">
|
|
210
232
|
<button type="button" class="lang-btn active" data-lang="zh" title="中文">中</button>
|
|
211
233
|
<button type="button" class="lang-btn" data-lang="en" title="English">EN</button>
|
|
@@ -396,7 +418,7 @@ body.lang-zh .lang-en{display:none !important}
|
|
|
396
418
|
<div class="code-section" id="quickstart">
|
|
397
419
|
<div class="code-text">
|
|
398
420
|
<h3><span class="lang-zh">从 npm 安装并配置</span><span class="lang-en">Install from npm & Configure</span></h3>
|
|
399
|
-
<p><span class="lang-zh">从 npm 安装,无需克隆或构建。在 openclaw.json 中加上插件配置(embedding/summarizer 可选),然后<strong>启动或重启 gateway</strong>。Memory Viewer 只有在网关运行时才会在 http://127.0.0.1:18799
|
|
421
|
+
<p><span class="lang-zh">从 npm 安装,无需克隆或构建。在 openclaw.json 中加上插件配置(embedding/summarizer 可选),然后<strong>启动或重启 gateway</strong>。Memory Viewer 只有在网关运行时才会在 http://127.0.0.1:18799 提供。手动解压安装时,插件会在首次加载时自动安装依赖,无需再执行 <code style="background:rgba(0,187,238,.1);color:var(--pri);padding:2px 6px;border-radius:4px;font-size:12px">npm install</code>。</span><span class="lang-en">Install from npm — no clone or build. Add OpenClaw config (embedding/summarizer optional), then <strong>start or restart the gateway</strong>. The Memory Viewer is only available at http://127.0.0.1:18799 when the gateway is running. When you install by manual extract, the plugin installs dependencies automatically on first load — no <code style="background:rgba(0,187,238,.1);color:var(--pri);padding:2px 6px;border-radius:4px;font-size:12px">npm install</code> needed.</span></p>
|
|
400
422
|
<div>
|
|
401
423
|
<span class="tag t-blue">npm</span>
|
|
402
424
|
<span class="tag t-violet">OpenClaw</span>
|
|
@@ -581,6 +603,10 @@ Found <span class="num">3</span> memories (minScore=<span class="num">0.45</span
|
|
|
581
603
|
});
|
|
582
604
|
});
|
|
583
605
|
})();
|
|
606
|
+
var MEMOS_THEME_KEY='memos-theme';
|
|
607
|
+
function initLandingTheme(){var s=localStorage.getItem(MEMOS_THEME_KEY);var theme=(s==='light'||s==='dark')?s:'dark';document.documentElement.setAttribute('data-theme',theme);}
|
|
608
|
+
function toggleLandingTheme(){var el=document.documentElement;var cur=el.getAttribute('data-theme')||'dark';var next=cur==='dark'?'light':'dark';el.setAttribute('data-theme',next);localStorage.setItem(MEMOS_THEME_KEY,next);}
|
|
609
|
+
initLandingTheme();
|
|
584
610
|
</script>
|
|
585
611
|
<!-- ── Scroll animations ── -->
|
|
586
612
|
<script>
|