openclaw-memory-alibaba-local 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2121 @@
1
+ /**
2
+ * Single-page admin UI for /plugins/memory (inline HTML/CSS/JS).
3
+ */
4
+
5
+ export function getMemoryPanelHtml(): string {
6
+ const css = `
7
+ /* 与 OpenClaw Observability 面板一致的浅色卡片 + 红色主色 */
8
+ :root {
9
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
10
+ color: #212121;
11
+ background: #f5f5f5;
12
+ --oc-accent: #d32f2f;
13
+ --oc-accent-hover: #b71c1c;
14
+ --oc-secondary: #7e57c2;
15
+ --oc-border: #e0e0e0;
16
+ --oc-muted: #757575;
17
+ --oc-card: #ffffff;
18
+ --oc-page: #f5f5f5;
19
+ }
20
+ * { box-sizing: border-box; }
21
+ input[type="checkbox"] { accent-color: var(--oc-accent); }
22
+ body {
23
+ margin: 0;
24
+ padding: 20px 20px 32px;
25
+ max-width: 1400px;
26
+ margin-inline: auto;
27
+ min-height: 100vh;
28
+ background: var(--oc-page);
29
+ }
30
+ .page-header {
31
+ margin-bottom: 16px;
32
+ }
33
+ h1 {
34
+ font-size: 1.35rem;
35
+ margin: 0;
36
+ font-weight: 600;
37
+ color: #212121;
38
+ letter-spacing: -0.02em;
39
+ }
40
+ .panel-card {
41
+ background: var(--oc-card);
42
+ border-radius: 10px;
43
+ border: 1px solid var(--oc-border);
44
+ box-shadow: 0 1px 3px rgba(0,0,0,0.06);
45
+ padding: 16px 18px;
46
+ margin-bottom: 14px;
47
+ min-width: 0;
48
+ max-width: 100%;
49
+ }
50
+ .panel-card--flush {
51
+ padding: 12px 0 0;
52
+ overflow: hidden;
53
+ }
54
+ .panel-card--flush .tabs {
55
+ padding: 0 18px 12px;
56
+ margin-bottom: 0;
57
+ }
58
+ .panel-card--flush #tableWrap {
59
+ padding: 0 18px 16px;
60
+ }
61
+ .toolbar {
62
+ display: flex;
63
+ flex-direction: column;
64
+ align-items: stretch;
65
+ gap: 12px;
66
+ }
67
+ .toolbar-row {
68
+ display: flex;
69
+ flex-wrap: wrap;
70
+ gap: 10px 14px;
71
+ align-items: center;
72
+ }
73
+ .toolbar-row--filters {
74
+ padding-top: 4px;
75
+ border-top: 1px solid #eeeeee;
76
+ }
77
+ .toolbar-actions {
78
+ display: inline-flex;
79
+ flex-wrap: wrap;
80
+ gap: 8px;
81
+ align-items: center;
82
+ margin-left: auto;
83
+ }
84
+ .toolbar-row--filters select#sessionId {
85
+ min-width: 12rem;
86
+ }
87
+ .toolbar label { font-size: 0.8125rem; color: var(--oc-muted); font-weight: 500; }
88
+ .toolbar input, .toolbar select, .toolbar button {
89
+ background: #fff;
90
+ border: 1px solid var(--oc-border);
91
+ color: #212121;
92
+ border-radius: 8px;
93
+ padding: 7px 11px;
94
+ font-size: 0.875rem;
95
+ }
96
+ .toolbar button { cursor: pointer; font-weight: 500; }
97
+ .toolbar button:hover { background: #fafafa; border-color: #bdbdbd; }
98
+ .toolbar button:disabled { opacity: 0.45; cursor: not-allowed; }
99
+ #btnRefresh, #btnDeleteSelected, #btnInsertMemory {
100
+ min-height: 36px;
101
+ }
102
+ #btnInsertMemory {
103
+ background: var(--oc-accent);
104
+ border-color: var(--oc-accent);
105
+ color: #fff;
106
+ }
107
+ #btnInsertMemory:hover {
108
+ background: var(--oc-accent-hover);
109
+ border-color: var(--oc-accent-hover);
110
+ }
111
+ .modal-backdrop {
112
+ display: none;
113
+ position: fixed;
114
+ inset: 0;
115
+ z-index: 1000;
116
+ background: rgba(0, 0, 0, 0.45);
117
+ align-items: center;
118
+ justify-content: center;
119
+ padding: 16px;
120
+ }
121
+ .modal-backdrop.open { display: flex; }
122
+ .modal-dialog {
123
+ background: var(--oc-card);
124
+ border: 1px solid var(--oc-border);
125
+ border-radius: 12px;
126
+ padding: 20px 22px;
127
+ max-width: 520px;
128
+ width: 100%;
129
+ max-height: 90vh;
130
+ overflow-y: auto;
131
+ box-shadow: 0 12px 40px rgba(0,0,0,0.15);
132
+ }
133
+ .modal-dialog h2 { font-size: 1.05rem; margin: 0 0 10px; color: #212121; font-weight: 600; }
134
+ .modal-note { font-size: 0.8125rem; color: var(--oc-muted); margin: 0 0 14px; line-height: 1.5; }
135
+ .modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 16px; flex-wrap: wrap; }
136
+ .add-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-start; margin-bottom: 10px; }
137
+ .add-row label { min-width: 88px; padding-top: 8px; font-size: 0.8125rem; color: var(--oc-muted); font-weight: 500; }
138
+ .add-row input[type="text"], .add-row select, .add-row textarea {
139
+ flex: 1;
140
+ min-width: 200px;
141
+ background: #fff;
142
+ border: 1px solid var(--oc-border);
143
+ color: #212121;
144
+ border-radius: 8px;
145
+ padding: 8px 11px;
146
+ font-size: 0.875rem;
147
+ }
148
+ .add-row textarea { min-height: 100px; resize: vertical; font-family: inherit; }
149
+ .modal-actions button {
150
+ background: #fff;
151
+ border: 1px solid var(--oc-border);
152
+ color: #424242;
153
+ border-radius: 8px;
154
+ padding: 8px 16px;
155
+ font-size: 0.875rem;
156
+ cursor: pointer;
157
+ font-weight: 500;
158
+ }
159
+ .modal-actions button:hover { background: #fafafa; }
160
+ .modal-actions button.primary {
161
+ background: var(--oc-accent);
162
+ border-color: var(--oc-accent);
163
+ color: #fff;
164
+ }
165
+ .modal-actions button.primary:hover {
166
+ background: var(--oc-accent-hover);
167
+ border-color: var(--oc-accent-hover);
168
+ }
169
+ .tabs { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
170
+ .tab {
171
+ padding: 8px 16px;
172
+ border-radius: 8px;
173
+ cursor: pointer;
174
+ color: #616161;
175
+ border: none;
176
+ background: transparent;
177
+ font-size: 0.9rem;
178
+ font-weight: 500;
179
+ }
180
+ .tab:hover { background: #f5f5f5; color: #212121; }
181
+ .tab.active {
182
+ background: var(--oc-accent);
183
+ color: #fff;
184
+ }
185
+ .tab.active:hover { background: var(--oc-accent-hover); color: #fff; }
186
+ .tab:disabled { opacity: 0.4; cursor: not-allowed; }
187
+ .banner { padding: 10px 14px; border-radius: 8px; margin-bottom: 14px; font-size: 0.875rem; display: none; }
188
+ .banner.err {
189
+ display: block;
190
+ background: #ffebee;
191
+ border: 1px solid #ffcdd2;
192
+ color: #c62828;
193
+ }
194
+ .banner.info {
195
+ display: block;
196
+ background: #e3f2fd;
197
+ border: 1px solid #90caf9;
198
+ color: #1565c0;
199
+ }
200
+ table { width: 100%; border-collapse: collapse; font-size: 0.8125rem; }
201
+ th, td {
202
+ text-align: left;
203
+ padding: 12px 14px;
204
+ border-bottom: 1px solid #eeeeee;
205
+ vertical-align: top;
206
+ }
207
+ th {
208
+ color: #9e9e9e;
209
+ font-weight: 600;
210
+ font-size: 0.7rem;
211
+ letter-spacing: 0.06em;
212
+ text-transform: uppercase;
213
+ }
214
+ tr:hover td { background: #fafafa; }
215
+ .text-cell { max-width: 560px; }
216
+ .text-preview {
217
+ max-width: 420px;
218
+ max-height: 4.5em;
219
+ overflow: hidden;
220
+ word-break: break-word;
221
+ border-radius: 6px;
222
+ padding: 2px 4px;
223
+ margin: -2px -4px;
224
+ color: #424242;
225
+ }
226
+ .text-preview:not(.text-preview-short) { cursor: pointer; outline: none; }
227
+ .text-preview:not(.text-preview-short):hover { background: #f5f5f5; }
228
+ .text-preview.text-preview-short { max-height: none; overflow: visible; max-width: 100%; }
229
+ .text-preview.expanded { max-height: none; overflow: visible; }
230
+ .text-hint {
231
+ font-size: 0.72rem;
232
+ color: var(--oc-muted);
233
+ margin-top: 6px;
234
+ user-select: none;
235
+ cursor: pointer;
236
+ }
237
+ .batch-group {
238
+ margin-bottom: 14px;
239
+ border: 1px solid var(--oc-border);
240
+ border-radius: 10px;
241
+ padding: 12px 14px;
242
+ background: #fafafa;
243
+ box-shadow: none;
244
+ }
245
+ .batch-group:last-child { margin-bottom: 0; }
246
+ .batch-summary {
247
+ cursor: pointer;
248
+ font-size: 0.875rem;
249
+ font-weight: 600;
250
+ color: #424242;
251
+ padding: 4px 2px;
252
+ list-style-position: outside;
253
+ }
254
+ .batch-group table { margin-top: 12px; }
255
+ /* 用户/自进化/全文列表共用固定列宽,避免各 Tab 错位 */
256
+ table.mem-admin-table {
257
+ table-layout: fixed;
258
+ width: 100%;
259
+ }
260
+ table.mem-admin-table .fm-session-cell,
261
+ table.mem-admin-table .fm-cat-cell {
262
+ overflow: hidden;
263
+ text-overflow: ellipsis;
264
+ white-space: nowrap;
265
+ }
266
+ table.mem-admin-table .fm-time-cell,
267
+ table.mem-admin-table .fm-imp-cell,
268
+ table.mem-admin-table th.fm-th-cat,
269
+ table.mem-admin-table th.fm-th-time,
270
+ table.mem-admin-table th.fm-th-imp {
271
+ white-space: nowrap;
272
+ }
273
+ table.mem-admin-table .fm-time-cell,
274
+ table.mem-admin-table .fm-imp-cell {
275
+ overflow: hidden;
276
+ text-overflow: ellipsis;
277
+ }
278
+ table.mem-admin-table .text-cell {
279
+ max-width: none;
280
+ }
281
+ table.mem-admin-table .text-preview {
282
+ max-width: 100%;
283
+ }
284
+ .fm-col-chk { width: 2.5rem; }
285
+ .fm-col-agent { width: 4.25rem; }
286
+ .fm-col-session { width: 18%; }
287
+ /* 用户/自进化:类型列收窄,时间/权重列加宽,配合 nowrap 避免表头与单元格断行 */
288
+ .fm-col-cat { width: 6.75rem; }
289
+ /* 全文记忆「类型」:约 7 个汉字可视宽度(含间隔号等,略大于 7em) */
290
+ .fm-col-cat-full { width: 11rem; min-width: 11rem; }
291
+ .fm-col-time { width: 13rem; }
292
+ .fm-col-imp { width: 6.5rem; }
293
+ .fm-col-seq { width: 2.5rem; }
294
+ .fm-col-text { width: auto; }
295
+ .pager {
296
+ display: flex;
297
+ gap: 10px;
298
+ align-items: center;
299
+ margin-top: 0;
300
+ padding: 14px 18px 16px;
301
+ flex-wrap: wrap;
302
+ font-size: 0.875rem;
303
+ color: var(--oc-muted);
304
+ }
305
+ .pager button {
306
+ background: #fff;
307
+ border: 1px solid var(--oc-border);
308
+ border-radius: 8px;
309
+ padding: 6px 14px;
310
+ font-size: 0.8125rem;
311
+ cursor: pointer;
312
+ font-weight: 500;
313
+ color: #424242;
314
+ }
315
+ .pager button:hover:not(:disabled) { background: #fafafa; }
316
+ .pager button:disabled { opacity: 0.4; cursor: not-allowed; }
317
+ .pager-jump {
318
+ display: inline-flex;
319
+ align-items: center;
320
+ gap: 6px;
321
+ margin-left: 4px;
322
+ flex-wrap: wrap;
323
+ }
324
+ .pager-jump label {
325
+ font-size: 0.8125rem;
326
+ color: var(--oc-muted);
327
+ font-weight: 500;
328
+ }
329
+ .pager-jump input[type="number"] {
330
+ width: 4.25rem;
331
+ padding: 5px 8px;
332
+ border: 1px solid var(--oc-border);
333
+ border-radius: 8px;
334
+ font-size: 0.8125rem;
335
+ text-align: center;
336
+ }
337
+ .pager-jump-suffix { font-size: 0.8125rem; color: var(--oc-muted); }
338
+ .toolbar .time-quick-btns {
339
+ display: inline-flex;
340
+ flex-wrap: wrap;
341
+ gap: 6px;
342
+ align-items: center;
343
+ }
344
+ .toolbar button.btn-quick-time {
345
+ padding: 5px 10px;
346
+ font-size: 0.78rem;
347
+ font-weight: 500;
348
+ background: #fafafa;
349
+ border: 1px solid var(--oc-border);
350
+ color: #424242;
351
+ }
352
+ .toolbar button.btn-quick-time:hover {
353
+ background: #f0f0f0;
354
+ border-color: #bdbdbd;
355
+ }
356
+ .toolbar button.btn-quick-time.time-range-source-active {
357
+ border-color: var(--oc-accent);
358
+ background: #ffebee;
359
+ color: var(--oc-accent);
360
+ box-shadow: 0 0 0 1px rgba(211, 47, 47, 0.2);
361
+ }
362
+ .toolbar button.btn-quick-time.time-range-source-active:hover {
363
+ background: #ffcdd2;
364
+ border-color: var(--oc-accent-hover);
365
+ }
366
+ .toolbar input[type="datetime-local"].time-range-source-active {
367
+ border-color: var(--oc-accent);
368
+ color: #b71c1c;
369
+ box-shadow: 0 0 0 1px rgba(211, 47, 47, 0.2);
370
+ }
371
+ .toolbar input[type="datetime-local"] {
372
+ min-width: 11.5rem;
373
+ padding: 6px 10px;
374
+ }
375
+ .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.78rem; color: #424242; }
376
+ .empty { color: var(--oc-muted); padding: 32px; text-align: center; font-size: 0.9rem; }
377
+ .inj-plain { display: block; margin: 4px 0; }
378
+ .inj-block { margin: 8px 0; }
379
+ .inj-open-modal {
380
+ display: inline-flex;
381
+ align-items: center;
382
+ justify-content: center;
383
+ border-radius: 8px;
384
+ padding: 6px 14px;
385
+ font-size: 0.8125rem;
386
+ font-weight: 600;
387
+ cursor: pointer;
388
+ border: none;
389
+ font-family: inherit;
390
+ }
391
+ .inj-open-modal--primary {
392
+ background: var(--oc-accent);
393
+ color: #fff;
394
+ }
395
+ .inj-open-modal--primary:hover { background: var(--oc-accent-hover); }
396
+ .inj-open-modal--secondary {
397
+ background: #fff;
398
+ color: var(--oc-secondary);
399
+ border: 1px solid #d1c4e9;
400
+ }
401
+ .inj-open-modal--secondary:hover { background: #f3e5f5; }
402
+ .inj-open-modal--neutral {
403
+ background: #fff;
404
+ color: #616161;
405
+ border: 1px solid var(--oc-border);
406
+ }
407
+ .inj-open-modal--neutral:hover { background: #fafafa; }
408
+ .inj-fold-pre--hidden {
409
+ position: absolute;
410
+ width: 1px;
411
+ height: 1px;
412
+ padding: 0;
413
+ margin: -1px;
414
+ overflow: hidden;
415
+ clip: rect(0, 0, 0, 0);
416
+ white-space: nowrap;
417
+ border: 0;
418
+ }
419
+ .modal-dialog-wide { max-width: min(920px, 96vw); }
420
+ .text-peek-pre {
421
+ margin: 0 0 12px;
422
+ max-height: min(72vh, 640px);
423
+ overflow: auto;
424
+ white-space: pre-wrap;
425
+ word-break: break-word;
426
+ font-size: 0.8125rem;
427
+ color: #212121;
428
+ background: #fafafa;
429
+ border: 1px solid var(--oc-border);
430
+ border-radius: 8px;
431
+ padding: 14px;
432
+ line-height: 1.5;
433
+ }
434
+ /* —— 记忆大盘(对齐 Observability 浅色卡片 + 红/紫点缀) */
435
+ .dash-wrap {
436
+ padding: 0 18px 16px;
437
+ }
438
+ .dash-kpi-grid {
439
+ display: grid;
440
+ grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
441
+ gap: 12px;
442
+ margin-bottom: 14px;
443
+ }
444
+ /* KPI 第一行:6 格横向等分铺满 */
445
+ .dash-kpi-grid--strip {
446
+ grid-template-columns: repeat(6, minmax(0, 1fr));
447
+ }
448
+ @media (max-width: 1100px) {
449
+ .dash-kpi-grid--strip {
450
+ grid-template-columns: repeat(3, minmax(0, 1fr));
451
+ }
452
+ }
453
+ @media (max-width: 560px) {
454
+ .dash-kpi-grid--strip {
455
+ grid-template-columns: repeat(2, minmax(0, 1fr));
456
+ }
457
+ }
458
+ .dash-kpi-card {
459
+ background: #fff;
460
+ border: 1px solid var(--oc-border);
461
+ border-radius: 10px;
462
+ padding: 14px 16px;
463
+ box-shadow: 0 1px 2px rgba(0,0,0,0.04);
464
+ }
465
+ .dash-kpi-label {
466
+ font-size: 0.65rem;
467
+ color: #9e9e9e;
468
+ font-weight: 600;
469
+ letter-spacing: 0.06em;
470
+ text-transform: uppercase;
471
+ }
472
+ .dash-kpi-value {
473
+ font-size: 1.45rem;
474
+ font-weight: 700;
475
+ color: #212121;
476
+ margin-top: 8px;
477
+ line-height: 1.15;
478
+ }
479
+ .dash-kpi-sub {
480
+ font-size: 0.75rem;
481
+ color: var(--oc-muted);
482
+ margin-top: 6px;
483
+ }
484
+ .dash-imp-bar {
485
+ display: flex;
486
+ height: 6px;
487
+ border-radius: 4px;
488
+ overflow: hidden;
489
+ margin-top: 10px;
490
+ }
491
+ .dash-imp-seg-low { background: #7e57c2; flex: 0 0 auto; min-width: 2px; }
492
+ .dash-imp-seg-mid { background: #ff9800; flex: 0 0 auto; min-width: 2px; }
493
+ .dash-imp-seg-high { background: #d32f2f; flex: 0 0 auto; min-width: 2px; }
494
+ .dash-row {
495
+ display: grid;
496
+ grid-template-columns: 1fr 1fr;
497
+ gap: 14px;
498
+ margin-bottom: 14px;
499
+ }
500
+ .dash-row--trend {
501
+ grid-template-columns: 1fr;
502
+ }
503
+ .dash-row--trend .dash-card {
504
+ min-width: 0;
505
+ max-width: 100%;
506
+ overflow: hidden;
507
+ }
508
+ .dash-row--dist {
509
+ grid-template-columns: repeat(3, minmax(0, 1fr));
510
+ }
511
+ @media (max-width: 960px) {
512
+ .dash-row { grid-template-columns: 1fr; }
513
+ .dash-row--dist { grid-template-columns: 1fr; }
514
+ }
515
+ .dash-card {
516
+ background: #fff;
517
+ border: 1px solid var(--oc-border);
518
+ border-radius: 10px;
519
+ padding: 16px 18px;
520
+ box-shadow: 0 1px 2px rgba(0,0,0,0.04);
521
+ }
522
+ .dash-card-title {
523
+ font-size: 0.95rem;
524
+ font-weight: 600;
525
+ margin: 0 0 14px;
526
+ color: #424242;
527
+ }
528
+ /* 写入趋势:固定绘图高度 + 像素柱高,避免 % 高度与 X 轴标签抢空间导致“突出”错位 */
529
+ .dash-chart {
530
+ display: flex;
531
+ flex-direction: row;
532
+ align-items: stretch;
533
+ gap: 4px;
534
+ margin-top: 4px;
535
+ max-width: 100%;
536
+ min-width: 0;
537
+ }
538
+ .dash-chart-y {
539
+ display: flex;
540
+ flex-direction: column;
541
+ justify-content: space-between;
542
+ align-items: flex-end;
543
+ flex-shrink: 0;
544
+ width: 2.5rem;
545
+ padding-right: 8px;
546
+ border-right: 1px solid #e0e0e0;
547
+ font-size: 0.68rem;
548
+ color: #9e9e9e;
549
+ line-height: 1;
550
+ box-sizing: border-box;
551
+ }
552
+ .dash-chart-y-tick { font-variant-numeric: tabular-nums; }
553
+ .dash-chart-bars-wrap {
554
+ flex: 1;
555
+ min-width: 0;
556
+ max-width: 100%;
557
+ overflow-x: auto;
558
+ overflow-y: visible;
559
+ padding-bottom: 2px;
560
+ -webkit-overflow-scrolling: touch;
561
+ }
562
+ /* 列多时仍保持最小列宽,由 .dash-chart-bars 变宽 + wrap 横向滚动看全图 */
563
+ .dash-chart-bars-wrap--dense .dash-chart-col {
564
+ min-width: 26px;
565
+ }
566
+ .dash-chart-bars-wrap--dense .dash-chart-x-label {
567
+ font-size: 0.5rem;
568
+ }
569
+ .dash-chart-bars-wrap--dense .dash-x-l1,
570
+ .dash-chart-bars-wrap--dense .dash-x-l2 {
571
+ font-size: 0.5rem;
572
+ }
573
+ .dash-chart-bars {
574
+ display: flex;
575
+ flex-direction: row;
576
+ align-items: flex-end;
577
+ gap: 4px;
578
+ min-height: 0;
579
+ border-bottom: 2px solid #e0e0e0;
580
+ padding: 0 4px 0;
581
+ box-sizing: border-box;
582
+ /* 列少:至少占满可视区,列均分拉满横轴;列多:按列 min-width 变宽,外层 wrap 可横向滚动 */
583
+ width: fit-content;
584
+ min-width: 100%;
585
+ }
586
+ .dash-chart-col {
587
+ flex: 1 1 0;
588
+ min-width: 44px;
589
+ display: flex;
590
+ flex-direction: column;
591
+ align-items: center;
592
+ }
593
+ .dash-chart-bar-track {
594
+ display: flex;
595
+ align-items: flex-end;
596
+ justify-content: center;
597
+ width: 100%;
598
+ box-sizing: border-box;
599
+ }
600
+ .dash-chart-bar {
601
+ width: min(100%, 20px);
602
+ min-height: 0;
603
+ background: var(--oc-accent);
604
+ border-radius: 4px 4px 0 0;
605
+ cursor: default;
606
+ transition: opacity 0.12s, filter 0.12s;
607
+ flex-shrink: 0;
608
+ }
609
+ .dash-chart-bar:hover {
610
+ opacity: 0.92;
611
+ filter: brightness(1.05);
612
+ }
613
+ .dash-chart-x-label {
614
+ font-size: 0.58rem;
615
+ color: #757575;
616
+ margin-top: 8px;
617
+ text-align: center;
618
+ line-height: 1.2;
619
+ max-width: 100%;
620
+ white-space: normal;
621
+ word-break: keep-all;
622
+ }
623
+ .dash-chart-x-label .dash-x-l1 { display: block; color: #616161; font-weight: 500; }
624
+ .dash-chart-x-label .dash-x-l2 { display: block; color: #9e9e9e; margin-top: 2px; }
625
+ .dash-dist-inner {
626
+ display: flex;
627
+ flex-direction: column;
628
+ align-items: center;
629
+ justify-content: center;
630
+ min-height: 188px;
631
+ gap: 14px;
632
+ padding: 8px 4px 4px;
633
+ }
634
+ .dash-legend--below {
635
+ width: 100%;
636
+ max-width: 220px;
637
+ margin: 0 auto;
638
+ }
639
+ .dash-donut-row {
640
+ display: flex;
641
+ align-items: center;
642
+ gap: 20px;
643
+ flex-wrap: wrap;
644
+ }
645
+ .dash-donut {
646
+ width: 132px;
647
+ height: 132px;
648
+ border-radius: 50%;
649
+ flex-shrink: 0;
650
+ position: relative;
651
+ }
652
+ .dash-donut-center {
653
+ position: absolute;
654
+ inset: 34px;
655
+ background: #fff;
656
+ border-radius: 50%;
657
+ display: flex;
658
+ flex-direction: column;
659
+ align-items: center;
660
+ justify-content: center;
661
+ box-shadow: 0 0 0 1px #f5f5f5 inset;
662
+ }
663
+ .dash-donut-total { font-size: 1.25rem; font-weight: 700; color: #212121; }
664
+ .dash-donut-cap { font-size: 0.65rem; color: #9e9e9e; margin-top: 2px; }
665
+ .dash-legend { display: flex; flex-direction: column; gap: 8px; font-size: 0.8125rem; color: #424242; }
666
+ .dash-legend-item { display: flex; align-items: center; gap: 8px; }
667
+ .dash-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
668
+ .dash-table-wrap { overflow-x: auto; }
669
+ table.dash-top-table {
670
+ width: 100%;
671
+ border-collapse: collapse;
672
+ font-size: 0.8125rem;
673
+ }
674
+ table.dash-top-table th,
675
+ table.dash-top-table td {
676
+ padding: 10px 12px;
677
+ border-bottom: 1px solid #eeeeee;
678
+ text-align: left;
679
+ }
680
+ table.dash-top-table th {
681
+ font-size: 0.68rem;
682
+ color: #9e9e9e;
683
+ font-weight: 600;
684
+ letter-spacing: 0.05em;
685
+ text-transform: uppercase;
686
+ }
687
+ .dash-empty {
688
+ color: var(--oc-muted);
689
+ font-size: 0.875rem;
690
+ padding: 28px;
691
+ text-align: center;
692
+ }
693
+ `;
694
+
695
+ const js = buildClientScript();
696
+
697
+ return `<!DOCTYPE html>
698
+ <html lang="zh-CN">
699
+ <head>
700
+ <meta charset="utf-8"/>
701
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
702
+ <title>RDSClaw 记忆管理</title>
703
+ <style>${css}</style>
704
+ </head>
705
+ <body>
706
+ <div class="page-header">
707
+ <h1>RDSClaw 记忆管理</h1>
708
+ </div>
709
+ <div id="banner" class="banner"></div>
710
+ <div class="panel-card panel-card--flush">
711
+ <div class="tabs" id="tabs">
712
+ <button type="button" class="tab active" data-tab="dash">记忆大盘</button>
713
+ <button type="button" class="tab" data-tab="user">用户记忆</button>
714
+ <button type="button" class="tab" data-tab="self">自进化记忆</button>
715
+ <button type="button" class="tab" data-tab="full">全文记忆</button>
716
+ </div>
717
+ </div>
718
+ <div class="panel-card">
719
+ <div class="toolbar">
720
+ <div class="toolbar-row toolbar-row--time">
721
+ <label for="timeFromInput">开始时间</label>
722
+ <input type="datetime-local" id="timeFromInput" step="1" autocomplete="off"/>
723
+ <label for="timeToInput">结束时间</label>
724
+ <input type="datetime-local" id="timeToInput" step="1" autocomplete="off"/>
725
+ <span class="time-quick-btns" aria-label="快捷时间范围">
726
+ <button type="button" class="btn-quick-time" id="btnTime24h" title="结束为当前时刻,开始为 24 小时前">最近24小时</button>
727
+ <button type="button" class="btn-quick-time" id="btnTime7d" title="结束为当前时刻,开始为 7 天前">最近7天</button>
728
+ <button type="button" class="btn-quick-time" id="btnTime30d" title="结束为当前时刻,开始为 30 天前">最近30天</button>
729
+ </span>
730
+ </div>
731
+ <div class="toolbar-row toolbar-row--filters">
732
+ <label for="agentId">Agent</label>
733
+ <select id="agentId"><option value="">(请选择)</option></select>
734
+ <label for="sessionId">会话</label>
735
+ <select id="sessionId"><option value="">全部</option></select>
736
+ <span id="memoryTypeFilterWrap">
737
+ <label for="memoryTypeFilter">记忆类型</label>
738
+ <select id="memoryTypeFilter"><option value="">全部</option></select>
739
+ </span>
740
+ <span id="toolbarSortWrap">
741
+ <label for="sortOrder">排序</label>
742
+ <select id="sortOrder">
743
+ <option value="desc" selected>逆序(新→旧)</option>
744
+ <option value="asc">顺序(旧→新)</option>
745
+ </select>
746
+ </span>
747
+ <span class="toolbar-actions">
748
+ <button type="button" id="btnRefresh">刷新</button>
749
+ <button type="button" id="btnDeleteSelected" disabled>删除所选</button>
750
+ <button type="button" id="btnInsertMemory">插入记忆</button>
751
+ </span>
752
+ </div>
753
+ </div>
754
+ </div>
755
+ <div id="insertModal" class="modal-backdrop" aria-hidden="true">
756
+ <div class="modal-dialog" role="dialog" aria-modal="true" aria-labelledby="insertModalTitle">
757
+ <h2 id="insertModalTitle">插入记忆</h2>
758
+ <p class="modal-note">仅做 embedding 后写入数据库;不经过相似度去重与 LLM 冲突处理。会话固定为 manual_insert,记忆权重固定为 1。</p>
759
+ <div class="add-row">
760
+ <label>Agent ID</label>
761
+ <input type="text" id="addAgentId" placeholder="必填,例如 main" autocomplete="off"/>
762
+ </div>
763
+ <div class="add-row">
764
+ <label>记忆类型</label>
765
+ <select id="addCategory"></select>
766
+ </div>
767
+ <div class="add-row">
768
+ <label>正文</label>
769
+ <textarea id="addText" placeholder="记忆内容"></textarea>
770
+ </div>
771
+ <div class="modal-actions">
772
+ <button type="button" id="btnModalCancel">取消</button>
773
+ <button type="button" class="primary" id="btnModalInsert">确定插入</button>
774
+ </div>
775
+ </div>
776
+ </div>
777
+ <div id="textPeekModal" class="modal-backdrop" aria-hidden="true">
778
+ <div class="modal-dialog modal-dialog-wide" role="dialog" aria-modal="true" aria-labelledby="textPeekTitle">
779
+ <h2 id="textPeekTitle">记忆召回信息</h2>
780
+ <pre id="textPeekBody" class="text-peek-pre"></pre>
781
+ <div class="modal-actions">
782
+ <button type="button" class="primary" id="btnTextPeekClose">关闭</button>
783
+ </div>
784
+ </div>
785
+ </div>
786
+ <div class="panel-card panel-card--flush">
787
+ <div id="dashWrap" class="dash-wrap"></div>
788
+ <div id="tableWrap" style="display:none"></div>
789
+ <div class="pager" id="pager" style="display:none"></div>
790
+ </div>
791
+ <script>${js}</script>
792
+ </body>
793
+ </html>`;
794
+ }
795
+
796
+ function buildClientScript(): string {
797
+ // 外层是模板字符串反引号:内层不要用 \ 转义双引号,否则生成到浏览器里的 JS 会断串(例如 class="mono")。
798
+ const s = `
799
+ (function () {
800
+ var LS_TOKEN_KEY = "openclaw_memory_gateway_token";
801
+ /** Plugin routes are fixed; do not derive from pathname ("/" 首页或 hash 路由会错成 /api)。 */
802
+ var API_BASE = "/plugins/memory/api";
803
+
804
+ function readTokenFromQueryString(qs) {
805
+ if (!qs || qs.charAt(0) !== "?") {
806
+ return "";
807
+ }
808
+ var t = new URLSearchParams(qs.slice(1)).get("token");
809
+ return t && String(t).trim() ? String(t).trim() : "";
810
+ }
811
+
812
+ function getGatewayToken() {
813
+ var fromSearch = readTokenFromQueryString(window.location.search);
814
+ if (fromSearch) {
815
+ try {
816
+ localStorage.setItem(LS_TOKEN_KEY, fromSearch);
817
+ } catch (e) {}
818
+ return fromSearch;
819
+ }
820
+ var hash = window.location.hash || "";
821
+ var qm = hash.indexOf("?");
822
+ if (qm >= 0) {
823
+ var fromHash = readTokenFromQueryString(hash.slice(qm));
824
+ if (fromHash) {
825
+ try {
826
+ localStorage.setItem(LS_TOKEN_KEY, fromHash);
827
+ } catch (e) {}
828
+ return fromHash;
829
+ }
830
+ }
831
+ try {
832
+ var s = localStorage.getItem(LS_TOKEN_KEY);
833
+ return s && String(s).trim() ? String(s).trim() : "";
834
+ } catch (e) {
835
+ return "";
836
+ }
837
+ }
838
+
839
+ function withAuth(url) {
840
+ var u = url.indexOf("?") >= 0 ? url + "&" : url + "?";
841
+ var tok = getGatewayToken();
842
+ if (tok) {
843
+ u += "token=" + encodeURIComponent(tok) + "&";
844
+ }
845
+ return u.replace(/[&?]$/, "").replace("?&", "?");
846
+ }
847
+
848
+ /** 避免页面带 <base href> 时相对路径跑偏;网关层也认 Bearer。 */
849
+ function memoryApiAbsolute(pathWithLeadingSlash) {
850
+ var p = pathWithLeadingSlash.charAt(0) === "/" ? pathWithLeadingSlash : "/" + pathWithLeadingSlash;
851
+ return window.location.origin + API_BASE + p;
852
+ }
853
+
854
+ function authFetchHeaders(base) {
855
+ var h = {};
856
+ if (base) {
857
+ for (var k in base) {
858
+ if (Object.prototype.hasOwnProperty.call(base, k)) {
859
+ h[k] = base[k];
860
+ }
861
+ }
862
+ }
863
+ var tok = getGatewayToken();
864
+ if (tok) {
865
+ h["Authorization"] = "Bearer " + tok;
866
+ }
867
+ return h;
868
+ }
869
+
870
+ function fetchMemoryApi(pathWithLeadingSlash, init) {
871
+ var url = withAuth(memoryApiAbsolute(pathWithLeadingSlash));
872
+ var o = init ? Object.assign({}, init) : {};
873
+ o.headers = authFetchHeaders(o.headers || {});
874
+ return fetch(url, o);
875
+ }
876
+
877
+ function showBanner(kind, msg) {
878
+ var el = document.getElementById("banner");
879
+ el.className = "banner " + (kind || "");
880
+ el.textContent = msg || "";
881
+ el.style.display = msg ? "block" : "none";
882
+ }
883
+
884
+ function esc(s) {
885
+ if (s == null) return "";
886
+ return String(s)
887
+ .replace(/&/g, "&amp;")
888
+ .replace(/</g, "&lt;")
889
+ .replace(/>/g, "&gt;")
890
+ .replace(/"/g, "&quot;");
891
+ }
892
+
893
+ function categoryLabel(c) {
894
+ var m = state.cfg && state.cfg.categoryLabelsZh;
895
+ return (m && m[c]) ? m[c] : String(c);
896
+ }
897
+
898
+ var state = {
899
+ tab: "dash",
900
+ cfg: null,
901
+ page: 1,
902
+ pageSize: 100,
903
+ total: 0,
904
+ items: [],
905
+ loading: false,
906
+ selectedAgentId: "",
907
+ selectedSessionId: "",
908
+ sortOrderByTab: { user: "desc", self: "desc", full: "desc" },
909
+ /** 非 null 表示当前起止时间来自「最近 N ms」快捷;刷新时先按此刻重算,避免结束时间停在旧时刻 */
910
+ timeQuickMs: null,
911
+ categoryFilterByTab: { user: "", self: "", full: "" }
912
+ };
913
+
914
+ var MS_QUICK_24H = 24 * 3600 * 1000;
915
+ var MS_QUICK_7D = 7 * 24 * 3600 * 1000;
916
+ var MS_QUICK_30D = 30 * 24 * 3600 * 1000;
917
+
918
+ /** 快捷与自定义时间来源:快捷则高亮对应按钮,否则高亮起止输入框 */
919
+ function syncTimeRangeHighlight() {
920
+ var q24 = document.getElementById("btnTime24h");
921
+ var q7 = document.getElementById("btnTime7d");
922
+ var q30 = document.getElementById("btnTime30d");
923
+ var fi = document.getElementById("timeFromInput");
924
+ var ti = document.getElementById("timeToInput");
925
+ [q24, q7, q30].forEach(function (b) {
926
+ if (b) b.classList.remove("time-range-source-active");
927
+ });
928
+ if (fi) fi.classList.remove("time-range-source-active");
929
+ if (ti) ti.classList.remove("time-range-source-active");
930
+
931
+ var m = state.timeQuickMs;
932
+ if (m === MS_QUICK_24H && q24) {
933
+ q24.classList.add("time-range-source-active");
934
+ } else if (m === MS_QUICK_7D && q7) {
935
+ q7.classList.add("time-range-source-active");
936
+ } else if (m === MS_QUICK_30D && q30) {
937
+ q30.classList.add("time-range-source-active");
938
+ } else {
939
+ if (fi) fi.classList.add("time-range-source-active");
940
+ if (ti) ti.classList.add("time-range-source-active");
941
+ }
942
+ }
943
+
944
+ function pad2(n) {
945
+ return n < 10 ? "0" + n : String(n);
946
+ }
947
+
948
+ /** datetime-local 用本地时间,含秒(step=1) */
949
+ function toDatetimeLocalValue(d) {
950
+ return (
951
+ d.getFullYear() +
952
+ "-" +
953
+ pad2(d.getMonth() + 1) +
954
+ "-" +
955
+ pad2(d.getDate()) +
956
+ "T" +
957
+ pad2(d.getHours()) +
958
+ ":" +
959
+ pad2(d.getMinutes()) +
960
+ ":" +
961
+ pad2(d.getSeconds())
962
+ );
963
+ }
964
+
965
+ function applyQuickRangeMs(ms) {
966
+ var now = Date.now();
967
+ var from = new Date(now - ms);
968
+ var to = new Date(now);
969
+ var fi = document.getElementById("timeFromInput");
970
+ var ti = document.getElementById("timeToInput");
971
+ if (fi) fi.value = toDatetimeLocalValue(from);
972
+ if (ti) ti.value = toDatetimeLocalValue(to);
973
+ state.timeQuickMs = ms;
974
+ syncTimeRangeHighlight();
975
+ }
976
+
977
+ /** 返回 { timeFrom, timeTo } ISO 字符串,或 { error: string } */
978
+ function readTimeRange() {
979
+ var fromEl = document.getElementById("timeFromInput");
980
+ var toEl = document.getElementById("timeToInput");
981
+ var fromV = fromEl && fromEl.value;
982
+ var toV = toEl && toEl.value;
983
+ if (!fromV || !toV) {
984
+ return { error: "请填写开始时间与结束时间(可使用「最近24小时」等快捷填充)" };
985
+ }
986
+ var fromMs = new Date(fromV).getTime();
987
+ var toMs = new Date(toV).getTime();
988
+ if (Number.isNaN(fromMs) || Number.isNaN(toMs)) {
989
+ return { error: "时间格式无效" };
990
+ }
991
+ if (fromMs > toMs) {
992
+ return { error: "开始时间不能晚于结束时间" };
993
+ }
994
+ return {
995
+ timeFrom: new Date(fromMs).toISOString(),
996
+ timeTo: new Date(toMs).toISOString()
997
+ };
998
+ }
999
+
1000
+ function facetQueryPath() {
1001
+ var q = new URLSearchParams();
1002
+ q.set("tab", state.tab);
1003
+ return "/facets?" + q.toString();
1004
+ }
1005
+
1006
+ function listQueryPath(page, tr) {
1007
+ var t = tr != null ? tr : readTimeRange();
1008
+ if (t.error) {
1009
+ return t;
1010
+ }
1011
+ var q = new URLSearchParams();
1012
+ q.set("tab", state.tab);
1013
+ var aid = state.selectedAgentId.trim();
1014
+ var sid = state.selectedSessionId.trim();
1015
+ q.set("agentId", aid);
1016
+ if (sid) q.set("sessionId", sid);
1017
+ q.set("timeFrom", t.timeFrom);
1018
+ q.set("timeTo", t.timeTo);
1019
+ q.set("page", String(page || 1));
1020
+ q.set("limit", String(state.pageSize));
1021
+ var ord = document.getElementById("sortOrder");
1022
+ var sortDesc = ord && ord.value === "desc";
1023
+ q.set("sortDesc", sortDesc ? "true" : "false");
1024
+ var catF = state.categoryFilterByTab[state.tab] || "";
1025
+ if (catF) {
1026
+ q.set("category", catF);
1027
+ }
1028
+ return { path: "/list?" + q.toString() };
1029
+ }
1030
+
1031
+ function syncMemoryTypeFilterSelect() {
1032
+ var sel = document.getElementById("memoryTypeFilter");
1033
+ if (!sel || !state.cfg || !state.cfg.memoryTypeFilterOptions) {
1034
+ return;
1035
+ }
1036
+ if (state.tab === "dash") {
1037
+ return;
1038
+ }
1039
+ var tabKey = state.tab;
1040
+ var opts = state.cfg.memoryTypeFilterOptions[tabKey] || [];
1041
+ var saved = state.categoryFilterByTab[tabKey] || "";
1042
+ sel.innerHTML = "";
1043
+ var allOpt = document.createElement("option");
1044
+ allOpt.value = "";
1045
+ allOpt.textContent = "全部";
1046
+ sel.appendChild(allOpt);
1047
+ opts.forEach(function (row) {
1048
+ var o = document.createElement("option");
1049
+ o.value = row.category;
1050
+ o.textContent = row.labelZh;
1051
+ sel.appendChild(o);
1052
+ });
1053
+ var ok = saved && Array.prototype.some.call(sel.options, function (opt) { return opt.value === saved; });
1054
+ if (ok) {
1055
+ sel.value = saved;
1056
+ } else {
1057
+ sel.value = "";
1058
+ state.categoryFilterByTab[tabKey] = "";
1059
+ }
1060
+ }
1061
+
1062
+ function fillAddCategorySelect() {
1063
+ var sel = document.getElementById("addCategory");
1064
+ if (!sel || !state.cfg || !state.cfg.tabCategories) return;
1065
+ var tabKey = state.tab === "dash" ? "user" : state.tab;
1066
+ var cats = state.cfg.tabCategories[tabKey] || [];
1067
+ sel.innerHTML = "";
1068
+ cats.forEach(function (c) {
1069
+ var o = document.createElement("option");
1070
+ o.value = c;
1071
+ o.textContent = categoryLabel(c);
1072
+ sel.appendChild(o);
1073
+ });
1074
+ }
1075
+
1076
+ function openInsertModal() {
1077
+ if (!state.cfg) {
1078
+ showBanner("err", "配置尚未加载完成");
1079
+ return;
1080
+ }
1081
+ fillAddCategorySelect();
1082
+ var aid = document.getElementById("addAgentId");
1083
+ if (aid && state.selectedAgentId.trim()) {
1084
+ aid.value = state.selectedAgentId.trim();
1085
+ }
1086
+ var modal = document.getElementById("insertModal");
1087
+ modal.classList.add("open");
1088
+ modal.setAttribute("aria-hidden", "false");
1089
+ if (aid) aid.focus();
1090
+ }
1091
+
1092
+ function closeInsertModal() {
1093
+ var modal = document.getElementById("insertModal");
1094
+ modal.classList.remove("open");
1095
+ modal.setAttribute("aria-hidden", "true");
1096
+ }
1097
+
1098
+ async function loadConfig() {
1099
+ var r = await fetchMemoryApi("/config");
1100
+ if (!r.ok) {
1101
+ if (r.status === 401) {
1102
+ throw new Error("未授权:请在 URL 使用 ?token=(与 ~/.openclaw/openclaw.json 中 gateway.auth.token 一致),hash 路由可用 #/path?token=;成功一次后会写入本机 localStorage。");
1103
+ }
1104
+ throw new Error("config " + r.status);
1105
+ }
1106
+ state.cfg = await r.json();
1107
+ var fullTab = document.querySelector('[data-tab="full"]');
1108
+ var selfTab = document.querySelector('[data-tab="self"]');
1109
+ if (!state.cfg.enableFullContextMemory) {
1110
+ fullTab.disabled = true;
1111
+ fullTab.title = "已在配置中关闭全文记忆";
1112
+ }
1113
+ if (!state.cfg.enableSelfImprovingMemory) {
1114
+ selfTab.disabled = true;
1115
+ selfTab.title = "已在配置中关闭自进化记忆";
1116
+ }
1117
+ fillAddCategorySelect();
1118
+ syncMemoryTypeFilterSelect();
1119
+ }
1120
+
1121
+ function ensureSelectOption(sel, value) {
1122
+ if (!value) return;
1123
+ var exists = Array.prototype.some.call(sel.options, function (o) { return o.value === value; });
1124
+ if (!exists) {
1125
+ var o = document.createElement("option");
1126
+ o.value = value;
1127
+ o.textContent = value + "(当前筛选)";
1128
+ sel.appendChild(o);
1129
+ }
1130
+ }
1131
+
1132
+ function maybeAutoLoadList() {
1133
+ if (state.selectedAgentId.trim()) {
1134
+ loadList(1);
1135
+ } else {
1136
+ showBanner("info", "请先选择 Agent。");
1137
+ document.getElementById("tableWrap").innerHTML = '<p class="empty">空列表(未选择 Agent)。</p>';
1138
+ document.getElementById("pager").style.display = "none";
1139
+ }
1140
+ }
1141
+
1142
+ function maybeAutoLoadForCurrentTab() {
1143
+ if (state.tab === "dash") {
1144
+ loadDashboard();
1145
+ } else {
1146
+ maybeAutoLoadList();
1147
+ }
1148
+ }
1149
+
1150
+ function updateToolbarForTab() {
1151
+ var isDash = state.tab === "dash";
1152
+ var sw = document.getElementById("toolbarSortWrap");
1153
+ if (sw) {
1154
+ sw.style.display = isDash ? "none" : "";
1155
+ }
1156
+ var delBtn = document.getElementById("btnDeleteSelected");
1157
+ if (delBtn) {
1158
+ delBtn.style.display = isDash ? "none" : "";
1159
+ }
1160
+ var insBtn = document.getElementById("btnInsertMemory");
1161
+ if (insBtn) {
1162
+ insBtn.style.display = isDash ? "none" : "";
1163
+ }
1164
+ var mtw = document.getElementById("memoryTypeFilterWrap");
1165
+ if (mtw) {
1166
+ mtw.style.display = isDash ? "none" : "";
1167
+ }
1168
+ if (!isDash) {
1169
+ syncMemoryTypeFilterSelect();
1170
+ }
1171
+ var dashW = document.getElementById("dashWrap");
1172
+ var tblW = document.getElementById("tableWrap");
1173
+ if (dashW) {
1174
+ dashW.style.display = isDash ? "block" : "none";
1175
+ }
1176
+ if (tblW) {
1177
+ tblW.style.display = isDash ? "none" : "block";
1178
+ }
1179
+ }
1180
+
1181
+ function renderDashboard(data) {
1182
+ var USER_CATS = ["user_memory_fact", "user_memory_preference", "user_memory_decision"];
1183
+ var SELF_CATS = ["self_improving_learnings", "self_improving_errors", "self_improving_feature_requests"];
1184
+ var FULL_CATS = [
1185
+ "full_context_memory",
1186
+ "full_context_user",
1187
+ "full_context_assistant",
1188
+ "full_context_system",
1189
+ "full_context_tool",
1190
+ "full_context_tool_result",
1191
+ "full_context_others"
1192
+ ];
1193
+ var COL_USER = ["#c62828", "#e53935", "#ff8a80"];
1194
+ var COL_SELF = ["#4527a0", "#7e57c2", "#b39ddb"];
1195
+ var COL_FULL = ["#e65100", "#f57c00", "#ff9800", "#ffb74d", "#5d4037", "#546e7a", "#78909c"];
1196
+ function trendXLabelHtml(fullLab) {
1197
+ var lab = String(fullLab || "");
1198
+ var sp = lab.indexOf(" ");
1199
+ if (sp > 0) {
1200
+ return (
1201
+ '<span class="dash-x-l1">' +
1202
+ esc(lab.slice(0, sp)) +
1203
+ '</span><span class="dash-x-l2">' +
1204
+ esc(lab.slice(sp + 1)) +
1205
+ "</span>"
1206
+ );
1207
+ }
1208
+ if (/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(lab)) {
1209
+ return '<span class="dash-x-l1">' + esc(lab.slice(5)) + "</span>";
1210
+ }
1211
+ return esc(lab);
1212
+ }
1213
+ function buildConicGradient(segs, colors) {
1214
+ var tot = 0;
1215
+ segs.forEach(function (x) {
1216
+ tot += x.count;
1217
+ });
1218
+ if (tot <= 0) {
1219
+ return "#eeeeee";
1220
+ }
1221
+ var d = 0;
1222
+ var parts = [];
1223
+ segs.forEach(function (x, i) {
1224
+ var span = (x.count / tot) * 360;
1225
+ if (span <= 0) {
1226
+ return;
1227
+ }
1228
+ var c = colors[i % colors.length];
1229
+ parts.push(c + " " + d + "deg " + (d + span) + "deg");
1230
+ d += span;
1231
+ });
1232
+ if (!parts.length) {
1233
+ return "#eeeeee";
1234
+ }
1235
+ return "conic-gradient(" + parts.join(",") + ")";
1236
+ }
1237
+ function kindDonutCard(title, keys, colors) {
1238
+ var byCat = data.byCategory || {};
1239
+ var segs = [];
1240
+ keys.forEach(function (k) {
1241
+ var n = byCat[k];
1242
+ if (typeof n === "number" && n > 0) {
1243
+ segs.push({ key: k, count: n });
1244
+ }
1245
+ });
1246
+ segs.sort(function (a, b) {
1247
+ return b.count - a.count;
1248
+ });
1249
+ var sum = 0;
1250
+ segs.forEach(function (x) {
1251
+ sum += x.count;
1252
+ });
1253
+ if (sum === 0) {
1254
+ return (
1255
+ '<div class="dash-card"><h3 class="dash-card-title">' +
1256
+ esc(title) +
1257
+ '</h3><div class="dash-dist-inner"><p class="dash-empty">暂无数据</p></div></div>'
1258
+ );
1259
+ }
1260
+ var grad = buildConicGradient(segs, colors);
1261
+ var leg = "";
1262
+ segs.forEach(function (x, idx) {
1263
+ leg +=
1264
+ '<div class="dash-legend-item"><span class="dash-dot" style="background:' +
1265
+ colors[idx % colors.length] +
1266
+ '"></span>' +
1267
+ esc(categoryLabel(x.key)) +
1268
+ " · " +
1269
+ x.count +
1270
+ "</div>";
1271
+ });
1272
+ return (
1273
+ '<div class="dash-card"><h3 class="dash-card-title">' +
1274
+ esc(title) +
1275
+ '</h3><div class="dash-dist-inner">' +
1276
+ '<div class="dash-donut" style="background:' +
1277
+ grad +
1278
+ '"><div class="dash-donut-center"><span class="dash-donut-total">' +
1279
+ sum +
1280
+ '</span><span class="dash-donut-cap">条</span></div></div>' +
1281
+ '<div class="dash-legend dash-legend--below">' +
1282
+ leg +
1283
+ "</div></div></div>"
1284
+ );
1285
+ }
1286
+ var u = data.byKind ? data.byKind.user || 0 : 0;
1287
+ var s = data.byKind ? data.byKind.self || 0 : 0;
1288
+ var f = data.byKind ? data.byKind.full || 0 : 0;
1289
+ var maxB = 0;
1290
+ var buckets = data.byBucket || [];
1291
+ buckets.forEach(function (b) {
1292
+ if (b.count > maxB) {
1293
+ maxB = b.count;
1294
+ }
1295
+ });
1296
+ var plotH = 200;
1297
+ var scaleMax = maxB > 0 ? maxB : 1;
1298
+ var yTicks = [];
1299
+ var yi;
1300
+ for (yi = 4; yi >= 0; yi--) {
1301
+ yTicks.push(Math.round((scaleMax * yi) / 4));
1302
+ }
1303
+ var yAxisHtml = '<div class="dash-chart-y" style="height:' + plotH + 'px">';
1304
+ yTicks.forEach(function (tick) {
1305
+ yAxisHtml += '<span class="dash-chart-y-tick">' + tick + "</span>";
1306
+ });
1307
+ yAxisHtml += "</div>";
1308
+ var barsHtml = "";
1309
+ if (buckets.length === 0) {
1310
+ barsHtml = '<p class="dash-empty" style="padding:40px 8px">当前时间范围内无写入分布数据</p>';
1311
+ } else {
1312
+ buckets.forEach(function (b) {
1313
+ var cnt = b.count || 0;
1314
+ var barPx = scaleMax > 0 ? Math.round((cnt / scaleMax) * plotH) : 0;
1315
+ if (cnt > 0 && barPx < 2) {
1316
+ barPx = 2;
1317
+ }
1318
+ var tip = esc(b.label) + ":" + String(cnt) + " 条";
1319
+ barsHtml +=
1320
+ '<div class="dash-chart-col">' +
1321
+ '<div class="dash-chart-bar-track" style="height:' +
1322
+ plotH +
1323
+ 'px">' +
1324
+ '<div class="dash-chart-bar" style="height:' +
1325
+ barPx +
1326
+ 'px" title="' +
1327
+ tip +
1328
+ '"></div></div>' +
1329
+ '<div class="dash-chart-x-label" title="' +
1330
+ esc(b.label) +
1331
+ '">' +
1332
+ trendXLabelHtml(b.label) +
1333
+ "</div></div>";
1334
+ });
1335
+ var barsWrapCls =
1336
+ buckets.length > 16
1337
+ ? "dash-chart-bars-wrap dash-chart-bars-wrap--dense"
1338
+ : "dash-chart-bars-wrap";
1339
+ barsHtml =
1340
+ '<div class="dash-chart">' +
1341
+ yAxisHtml +
1342
+ '<div class="' +
1343
+ barsWrapCls +
1344
+ '"><div class="dash-chart-bars">' +
1345
+ barsHtml +
1346
+ "</div></div></div>";
1347
+ }
1348
+ var ua = data.uniqueAgents != null ? data.uniqueAgents : 0;
1349
+ var us = data.uniqueSessions != null ? data.uniqueSessions : 0;
1350
+ var kpi =
1351
+ '<div class="dash-kpi-grid dash-kpi-grid--strip">' +
1352
+ '<div class="dash-kpi-card"><div class="dash-kpi-label">总条数</div><div class="dash-kpi-value">' +
1353
+ (data.total != null ? data.total : 0) +
1354
+ '</div><div class="dash-kpi-sub">当前筛选范围内全部类别</div></div>' +
1355
+ '<div class="dash-kpi-card"><div class="dash-kpi-label">用户记忆</div><div class="dash-kpi-value">' +
1356
+ u +
1357
+ "</div></div>" +
1358
+ '<div class="dash-kpi-card"><div class="dash-kpi-label">自进化记忆</div><div class="dash-kpi-value">' +
1359
+ s +
1360
+ "</div></div>" +
1361
+ '<div class="dash-kpi-card"><div class="dash-kpi-label">全文记忆</div><div class="dash-kpi-value">' +
1362
+ f +
1363
+ "</div></div>" +
1364
+ '<div class="dash-kpi-card"><div class="dash-kpi-label">独立 Agent</div><div class="dash-kpi-value">' +
1365
+ ua +
1366
+ '</div><div class="dash-kpi-sub">去重计数</div></div>' +
1367
+ '<div class="dash-kpi-card"><div class="dash-kpi-label">独立会话</div><div class="dash-kpi-value">' +
1368
+ us +
1369
+ '</div><div class="dash-kpi-sub">去重计数</div></div>' +
1370
+ "</div>";
1371
+ var distRow =
1372
+ '<div class="dash-row dash-row--dist">' +
1373
+ kindDonutCard("用户记忆分布", USER_CATS, COL_USER) +
1374
+ kindDonutCard("自进化记忆分布", SELF_CATS, COL_SELF) +
1375
+ kindDonutCard("全文记忆分布", FULL_CATS, COL_FULL) +
1376
+ "</div>";
1377
+ var topA = data.topAgents || [];
1378
+ var agentsRows = "";
1379
+ topA.forEach(function (r) {
1380
+ agentsRows +=
1381
+ '<tr><td class="mono">' +
1382
+ esc(r.agentId) +
1383
+ "</td><td>" +
1384
+ esc(String(r.count)) +
1385
+ "</td></tr>";
1386
+ });
1387
+ if (!agentsRows) {
1388
+ agentsRows = '<tr><td colspan="2" class="dash-empty" style="padding:16px">暂无数据</td></tr>';
1389
+ }
1390
+ var topS = data.topSessions || [];
1391
+ var sessRows = "";
1392
+ topS.forEach(function (r) {
1393
+ sessRows +=
1394
+ '<tr><td class="mono" title="' +
1395
+ esc(r.sessionId) +
1396
+ '">' +
1397
+ esc(r.sessionId.length > 36 ? r.sessionId.slice(0, 18) + "…" : r.sessionId) +
1398
+ "</td><td>" +
1399
+ esc(String(r.count)) +
1400
+ "</td></tr>";
1401
+ });
1402
+ if (!sessRows) {
1403
+ sessRows = '<tr><td colspan="2" class="dash-empty" style="padding:16px">暂无数据</td></tr>';
1404
+ }
1405
+ var topAgentsTable =
1406
+ '<div class="dash-table-wrap"><table class="dash-top-table"><thead><tr><th>Agent</th><th>条数</th></tr></thead><tbody>' +
1407
+ agentsRows +
1408
+ "</tbody></table></div>";
1409
+ var topSessionsTable =
1410
+ '<div class="dash-table-wrap"><table class="dash-top-table"><thead><tr><th>会话</th><th>条数</th></tr></thead><tbody>' +
1411
+ sessRows +
1412
+ "</tbody></table></div>";
1413
+ var html =
1414
+ kpi +
1415
+ '<div class="dash-row dash-row--trend">' +
1416
+ '<div class="dash-card"><h3 class="dash-card-title">写入趋势</h3>' +
1417
+ barsHtml +
1418
+ "</div></div>" +
1419
+ distRow +
1420
+ '<div class="dash-row">' +
1421
+ '<div class="dash-card"><h3 class="dash-card-title">热门 Agent</h3>' +
1422
+ topAgentsTable +
1423
+ "</div>" +
1424
+ '<div class="dash-card"><h3 class="dash-card-title">热门会话</h3>' +
1425
+ topSessionsTable +
1426
+ "</div>" +
1427
+ "</div>";
1428
+ document.getElementById("dashWrap").innerHTML = html;
1429
+ }
1430
+
1431
+ async function loadDashboard() {
1432
+ if (state.tab !== "dash") {
1433
+ return;
1434
+ }
1435
+ var tr = readTimeRange();
1436
+ if (tr.error) {
1437
+ showBanner("err", tr.error);
1438
+ document.getElementById("dashWrap").innerHTML = '<p class="dash-empty">' + esc(tr.error) + "</p>";
1439
+ return;
1440
+ }
1441
+ var aid = state.selectedAgentId.trim();
1442
+ if (!aid) {
1443
+ showBanner("info", "请先选择 Agent。");
1444
+ document.getElementById("dashWrap").innerHTML = '<p class="dash-empty">请先选择 Agent 后再查看记忆大盘。</p>';
1445
+ return;
1446
+ }
1447
+ showBanner("", "");
1448
+ state.loading = true;
1449
+ var q = new URLSearchParams();
1450
+ q.set("timeFrom", tr.timeFrom);
1451
+ q.set("timeTo", tr.timeTo);
1452
+ q.set("agentId", aid);
1453
+ var sid = state.selectedSessionId.trim();
1454
+ if (sid) {
1455
+ q.set("sessionId", sid);
1456
+ }
1457
+ try {
1458
+ var r = await fetchMemoryApi("/dashboard?" + q.toString());
1459
+ var data = await r.json().catch(function () {
1460
+ return {};
1461
+ });
1462
+ if (!r.ok) {
1463
+ showBanner("err", (data && data.error) ? String(data.error) : "大盘加载失败 " + r.status);
1464
+ document.getElementById("dashWrap").innerHTML = '<p class="dash-empty">加载失败</p>';
1465
+ return;
1466
+ }
1467
+ showBanner("", "");
1468
+ renderDashboard(data);
1469
+ } finally {
1470
+ state.loading = false;
1471
+ }
1472
+ }
1473
+
1474
+ async function loadFacets(opts) {
1475
+ var autoLoad = !opts || opts.autoLoad !== false;
1476
+ showBanner("", "");
1477
+ var r = await fetchMemoryApi(facetQueryPath());
1478
+ if (!r.ok) {
1479
+ var err = await r.text();
1480
+ var hint401 = r.status === 401 ? " 需要 Token:URL ?token= 或 #/path?token=(与 gateway.auth.token 一致)" : "";
1481
+ showBanner("err", "加载下拉失败: " + r.status + " " + err + hint401);
1482
+ return;
1483
+ }
1484
+ var data = await r.json().catch(function () {
1485
+ return {};
1486
+ });
1487
+ var agents = data.agents || [];
1488
+ var sessions = data.sessions || [];
1489
+ var selA = document.getElementById("agentId");
1490
+ var selS = document.getElementById("sessionId");
1491
+
1492
+ selA.innerHTML = '<option value="">(请选择)</option>';
1493
+ selS.innerHTML = '<option value="">全部</option>';
1494
+ agents.forEach(function (a) {
1495
+ var o = document.createElement("option");
1496
+ o.value = a;
1497
+ o.textContent = a;
1498
+ selA.appendChild(o);
1499
+ });
1500
+ sessions.forEach(function (s) {
1501
+ var o = document.createElement("option");
1502
+ o.value = s;
1503
+ o.textContent = s;
1504
+ selS.appendChild(o);
1505
+ });
1506
+ var curAgent = state.selectedAgentId.trim();
1507
+ if (agents.length > 0 && (!curAgent || agents.indexOf(curAgent) < 0)) {
1508
+ curAgent = agents.indexOf("main") >= 0 ? "main" : agents[0];
1509
+ state.selectedAgentId = curAgent;
1510
+ }
1511
+ ensureSelectOption(selA, state.selectedAgentId.trim());
1512
+ ensureSelectOption(selS, state.selectedSessionId.trim());
1513
+ selA.value = state.selectedAgentId.trim();
1514
+ selS.value = state.selectedSessionId.trim();
1515
+ state.selectedAgentId = selA.value.trim();
1516
+ state.selectedSessionId = selS.value.trim();
1517
+
1518
+ if (agents.length === 0 && sessions.length === 0) {
1519
+ ensureSelectOption(selA, "main");
1520
+ if (!state.selectedAgentId.trim()) {
1521
+ state.selectedAgentId = "main";
1522
+ selA.value = "main";
1523
+ }
1524
+ }
1525
+
1526
+ if (autoLoad) maybeAutoLoadForCurrentTab();
1527
+ }
1528
+
1529
+ async function loadList(page) {
1530
+ if (state.tab === "dash") {
1531
+ return;
1532
+ }
1533
+ var aid = state.selectedAgentId.trim();
1534
+ if (!aid) {
1535
+ showBanner("info", "请先选择 Agent。");
1536
+ document.getElementById("tableWrap").innerHTML = '<p class="empty">请选择 Agent 后刷新。</p>';
1537
+ document.getElementById("pager").style.display = "none";
1538
+ return;
1539
+ }
1540
+ showBanner("", "");
1541
+ var tr = readTimeRange();
1542
+ if (tr.error) {
1543
+ showBanner("err", tr.error);
1544
+ document.getElementById("pager").style.display = "none";
1545
+ return;
1546
+ }
1547
+ state.loading = true;
1548
+ state.page = page || 1;
1549
+ try {
1550
+ var lq = listQueryPath(state.page, tr);
1551
+ var r = await fetchMemoryApi(lq.path);
1552
+ var data = await r.json().catch(function () { return {}; });
1553
+ if (!r.ok) {
1554
+ showBanner("err", (data && data.error) ? String(data.error) : "列表请求失败 " + r.status);
1555
+ return;
1556
+ }
1557
+ state.total = data.total || 0;
1558
+ state.items = data.items || [];
1559
+ renderTable();
1560
+ renderPager();
1561
+ } finally {
1562
+ state.loading = false;
1563
+ }
1564
+ }
1565
+
1566
+ /** 按开闭标签切分,避免正则 [\s\S]*? 与模板转义导致漏掉 </...> 后的正文。 */
1567
+ function splitInjectBlocks(raw) {
1568
+ if (!raw) return null;
1569
+ var lower = raw.toLowerCase();
1570
+ var parts = [];
1571
+ var pos = 0;
1572
+ var foundAny = false;
1573
+ while (true) {
1574
+ var ir = lower.indexOf("<relevant-memories", pos);
1575
+ var ik = lower.indexOf("<knowledge-context", pos);
1576
+ var openIdx = -1;
1577
+ var closeNeedle = "";
1578
+ if (ir >= 0 && (ik < 0 || ir <= ik)) {
1579
+ openIdx = ir;
1580
+ closeNeedle = "</relevant-memories";
1581
+ } else if (ik >= 0) {
1582
+ openIdx = ik;
1583
+ closeNeedle = "</knowledge-context";
1584
+ } else {
1585
+ break;
1586
+ }
1587
+ if (openIdx > pos) {
1588
+ parts.push({ t: "p", s: raw.slice(pos, openIdx) });
1589
+ }
1590
+ var cs = lower.indexOf(closeNeedle, openIdx);
1591
+ if (cs < 0) return null;
1592
+ var gt = raw.indexOf(">", cs);
1593
+ if (gt < 0) return null;
1594
+ var endEx = gt + 1;
1595
+ parts.push({ t: "i", s: raw.slice(openIdx, endEx) });
1596
+ pos = endEx;
1597
+ foundAny = true;
1598
+ }
1599
+ if (!foundAny) return null;
1600
+ if (pos < raw.length) {
1601
+ parts.push({ t: "p", s: raw.slice(pos) });
1602
+ }
1603
+ return parts;
1604
+ }
1605
+
1606
+ function injectLinkLabel(block) {
1607
+ var b = (block || "").toLowerCase();
1608
+ if (b.indexOf("<relevant-memories") >= 0) {
1609
+ return "记忆召回信息";
1610
+ }
1611
+ if (b.indexOf("<knowledge-context") >= 0) {
1612
+ return "知识上下文信息";
1613
+ }
1614
+ return "注入片段信息";
1615
+ }
1616
+
1617
+ function injectModalTitleClass(block) {
1618
+ var b = (block || "").toLowerCase();
1619
+ if (b.indexOf("<relevant-memories") >= 0) {
1620
+ return "inj-open-modal--primary";
1621
+ }
1622
+ if (b.indexOf("<knowledge-context") >= 0) {
1623
+ return "inj-open-modal--secondary";
1624
+ }
1625
+ return "inj-open-modal--neutral";
1626
+ }
1627
+
1628
+ function modalTitleForInjectClass(cls) {
1629
+ if (cls.indexOf("inj-open-modal--primary") >= 0) {
1630
+ return "记忆召回信息";
1631
+ }
1632
+ if (cls.indexOf("inj-open-modal--secondary") >= 0) {
1633
+ return "知识上下文信息";
1634
+ }
1635
+ return "注入片段信息";
1636
+ }
1637
+
1638
+ function buildTextCellWithInjectFold(raw) {
1639
+ var sp = splitInjectBlocks(raw);
1640
+ if (!sp) {
1641
+ return null;
1642
+ }
1643
+ var html = "";
1644
+ sp.forEach(function (part) {
1645
+ if (part.t === "p") {
1646
+ if (!part.s) {
1647
+ return;
1648
+ }
1649
+ var pl = part.s.length > 200;
1650
+ html +=
1651
+ '<span class="' +
1652
+ (pl ? "text-preview inj-plain" : "text-preview text-preview-short inj-plain") +
1653
+ '"' +
1654
+ (pl ? ' role="button" tabindex="0" title="点击展开或收起"' : "") +
1655
+ ">" +
1656
+ esc(part.s) +
1657
+ "</span>";
1658
+ } else {
1659
+ var ic = injectModalTitleClass(part.s);
1660
+ html +=
1661
+ '<div class="inj-block">' +
1662
+ '<button type="button" class="inj-open-modal ' +
1663
+ ic +
1664
+ '" title="在弹窗中查看全文">' +
1665
+ esc(injectLinkLabel(part.s)) +
1666
+ "</button>" +
1667
+ '<pre class="inj-fold-pre--hidden" aria-hidden="true">' +
1668
+ esc(part.s) +
1669
+ "</pre></div>";
1670
+ }
1671
+ });
1672
+ return html;
1673
+ }
1674
+
1675
+ function buildOneRow(row, i, extraCols) {
1676
+ var raw = row.text == null ? "" : String(row.text);
1677
+ var mixed = buildTextCellWithInjectFold(raw);
1678
+ var seqCell = "";
1679
+ if (extraCols) {
1680
+ seqCell = '<td class="mono">' + esc(row.seqInBatch != null ? String(row.seqInBatch) : "") + "</td>";
1681
+ }
1682
+ var sessTitle = ' title="' + esc(row.sessionId || "") + '"';
1683
+ var sessCls = "mono fm-session-cell";
1684
+ var catTdOpen = '<td class="fm-cat-cell">';
1685
+ var textTd;
1686
+ if (mixed) {
1687
+ textTd = '<td><div class="text-cell text-cell-mixed">' + mixed + "</div></td>";
1688
+ } else {
1689
+ var long = raw.length > 200;
1690
+ var prevCls = "text-preview" + (long ? "" : " text-preview-short");
1691
+ var prevAttrs = long ? ' role="button" tabindex="0" title="点击展开或收起全文"' : "";
1692
+ var hint = long ? '<div class="text-hint">点击上文或此处展开全文</div>' : "";
1693
+ textTd =
1694
+ '<td><div class="text-cell">' +
1695
+ "<div " +
1696
+ prevAttrs +
1697
+ ' class="' +
1698
+ prevCls +
1699
+ '">' +
1700
+ esc(raw) +
1701
+ "</div>" +
1702
+ hint +
1703
+ "</div></td>";
1704
+ }
1705
+ return (
1706
+ "<tr>" +
1707
+ '<td><input type="checkbox" class="rowchk" data-i="' + i + '"/></td>' +
1708
+ '<td class="mono">' + esc(row.agentId) + "</td>" +
1709
+ "<td" +
1710
+ sessTitle +
1711
+ ' class="' +
1712
+ sessCls +
1713
+ '">' +
1714
+ esc(row.sessionId) +
1715
+ "</td>" +
1716
+ catTdOpen +
1717
+ esc(categoryLabel(row.category)) +
1718
+ "</td>" +
1719
+ '<td class="fm-time-cell">' + esc(new Date(row.createdAt).toLocaleString()) + "</td>" +
1720
+ (extraCols ? "" : '<td class="fm-imp-cell mono">' + esc(row.importance) + "</td>") +
1721
+ seqCell +
1722
+ textTd +
1723
+ "</tr>"
1724
+ );
1725
+ }
1726
+
1727
+ function renderTable() {
1728
+ var wrap = document.getElementById("tableWrap");
1729
+ if (!state.items.length) {
1730
+ wrap.innerHTML = '<p class="empty">暂无数据</p>';
1731
+ return;
1732
+ }
1733
+ if (state.tab === "full") {
1734
+ var groups = [];
1735
+ var cur = null;
1736
+ state.items.forEach(function (row, idx) {
1737
+ var bid = row.batchId && String(row.batchId).length ? String(row.batchId) : "";
1738
+ if (!cur || cur.batchId !== bid) {
1739
+ cur = { batchId: bid, rows: [] };
1740
+ groups.push(cur);
1741
+ }
1742
+ cur.rows.push({ row: row, idx: idx });
1743
+ });
1744
+ var colgroup =
1745
+ "<colgroup>" +
1746
+ '<col class="fm-col-chk"/>' +
1747
+ '<col class="fm-col-agent"/>' +
1748
+ '<col class="fm-col-session"/>' +
1749
+ '<col class="fm-col-cat-full"/>' +
1750
+ '<col class="fm-col-time"/>' +
1751
+ '<col class="fm-col-seq"/>' +
1752
+ '<col class="fm-col-text"/>' +
1753
+ "</colgroup>";
1754
+ var thead =
1755
+ "<thead><tr>" +
1756
+ '<th><input type="checkbox" class="batch-chk-all" title="本对话轮次全选"/></th>' +
1757
+ "<th>Agent</th><th>会话</th><th class='fm-th-cat fm-th-cat-full'>类型</th><th class='fm-th-time'>时间</th><th>序</th><th>正文</th></tr></thead>";
1758
+ var blocks = groups.map(function (g) {
1759
+ var label = g.batchId
1760
+ ? "对话轮次 · " + esc(g.batchId.slice(0, 8)) + "… · " + g.rows.length + " 条(组内正序)"
1761
+ : "其他 · " + g.rows.length + " 条";
1762
+ var bodyRows = "";
1763
+ g.rows.forEach(function (pair) {
1764
+ bodyRows += buildOneRow(pair.row, pair.idx, true);
1765
+ });
1766
+ return (
1767
+ '<details class="batch-group" open>' +
1768
+ '<summary class="batch-summary">' +
1769
+ label +
1770
+ "</summary>" +
1771
+ '<table class="mem-admin-table">' +
1772
+ colgroup +
1773
+ thead +
1774
+ "<tbody>" +
1775
+ bodyRows +
1776
+ "</tbody></table></details>"
1777
+ );
1778
+ });
1779
+ wrap.innerHTML = blocks.join("");
1780
+ } else {
1781
+ var rows = state.items.map(function (row, i) {
1782
+ return buildOneRow(row, i, false);
1783
+ }).join("");
1784
+ var listColgroup =
1785
+ "<colgroup>" +
1786
+ '<col class="fm-col-chk"/>' +
1787
+ '<col class="fm-col-agent"/>' +
1788
+ '<col class="fm-col-session"/>' +
1789
+ '<col class="fm-col-cat"/>' +
1790
+ '<col class="fm-col-time"/>' +
1791
+ '<col class="fm-col-imp"/>' +
1792
+ '<col class="fm-col-text"/>' +
1793
+ "</colgroup>";
1794
+ wrap.innerHTML =
1795
+ '<table class="mem-admin-table">' +
1796
+ listColgroup +
1797
+ "<thead><tr>" +
1798
+ '<th><input type="checkbox" id="chkAll"/></th>' +
1799
+ "<th>Agent</th><th>会话</th><th class='fm-th-cat'>记忆类型</th><th class='fm-th-time'>记忆时间</th><th class='fm-th-imp'>记忆权重</th><th>正文</th></tr></thead><tbody>" +
1800
+ rows +
1801
+ "</tbody></table>";
1802
+ }
1803
+ var masterChk = document.getElementById("chkAll");
1804
+ if (masterChk) {
1805
+ masterChk.onchange = function () {
1806
+ var on = masterChk.checked;
1807
+ document.querySelectorAll(".rowchk").forEach(function (c) {
1808
+ c.checked = on;
1809
+ });
1810
+ updateDeleteBtn();
1811
+ };
1812
+ }
1813
+ wrap.querySelectorAll(".batch-chk-all").forEach(function (bc) {
1814
+ bc.onchange = function () {
1815
+ var det = bc.closest("details");
1816
+ if (!det) {
1817
+ return;
1818
+ }
1819
+ var on = bc.checked;
1820
+ det.querySelectorAll(".rowchk").forEach(function (c) {
1821
+ c.checked = on;
1822
+ });
1823
+ updateDeleteBtn();
1824
+ };
1825
+ });
1826
+ document.querySelectorAll(".rowchk").forEach(function (c) {
1827
+ c.onchange = updateDeleteBtn;
1828
+ });
1829
+ updateDeleteBtn();
1830
+ }
1831
+
1832
+ function renderPager() {
1833
+ var p = document.getElementById("pager");
1834
+ if (state.tab === "dash") {
1835
+ p.style.display = "none";
1836
+ p.innerHTML = "";
1837
+ return;
1838
+ }
1839
+ var pages = Math.max(1, Math.ceil(state.total / state.pageSize));
1840
+ if (state.total <= state.pageSize && state.page <= 1) {
1841
+ p.style.display = state.total ? "flex" : "none";
1842
+ p.innerHTML = state.total ? "<span>共 " + state.total + " 条</span>" : "";
1843
+ return;
1844
+ }
1845
+ p.style.display = "flex";
1846
+ p.innerHTML =
1847
+ "<span>共 " + state.total + " 条 · 第 " + state.page + " / " + pages + " 页</span>" +
1848
+ '<button type="button" id="pgPrev"' + (state.page <= 1 ? " disabled" : "") + ">上一页</button>" +
1849
+ '<button type="button" id="pgNext"' + (state.page >= pages ? " disabled" : "") + ">下一页</button>" +
1850
+ '<span class="pager-jump">' +
1851
+ '<label for="pgJumpInput">跳到</label>' +
1852
+ '<input type="number" id="pgJumpInput" min="1" max="' +
1853
+ pages +
1854
+ '" step="1" value="' +
1855
+ state.page +
1856
+ '"/>' +
1857
+ '<span class="pager-jump-suffix">页</span>' +
1858
+ '<button type="button" id="pgGo">跳转</button>' +
1859
+ "</span>";
1860
+ document.getElementById("pgPrev").onclick = function () { loadList(state.page - 1); };
1861
+ document.getElementById("pgNext").onclick = function () { loadList(state.page + 1); };
1862
+ function doJump() {
1863
+ var inp = document.getElementById("pgJumpInput");
1864
+ var n = parseInt(inp && inp.value, 10);
1865
+ if (!Number.isFinite(n)) {
1866
+ return;
1867
+ }
1868
+ n = Math.max(1, Math.min(pages, n));
1869
+ loadList(n);
1870
+ }
1871
+ document.getElementById("pgGo").onclick = doJump;
1872
+ document.getElementById("pgJumpInput").addEventListener("keydown", function (e) {
1873
+ if (e.key === "Enter") {
1874
+ e.preventDefault();
1875
+ doJump();
1876
+ }
1877
+ });
1878
+ }
1879
+
1880
+ function updateDeleteBtn() {
1881
+ var n = document.querySelectorAll(".rowchk:checked").length;
1882
+ document.getElementById("btnDeleteSelected").disabled = n === 0;
1883
+ }
1884
+
1885
+ async function deleteSelected() {
1886
+ var sel = [];
1887
+ document.querySelectorAll(".rowchk:checked").forEach(function (c) {
1888
+ var i = parseInt(c.getAttribute("data-i"), 10);
1889
+ var row = state.items[i];
1890
+ if (row && row.agentId && row.id) sel.push({ agentId: row.agentId, id: row.id });
1891
+ });
1892
+ if (!sel.length) return;
1893
+ if (!window.confirm("确认永久删除所选 " + sel.length + " 条?此操作不可恢复。")) return;
1894
+ var body = JSON.stringify({ items: sel });
1895
+ var r = await fetchMemoryApi("/delete", {
1896
+ method: "POST",
1897
+ headers: { "Content-Type": "application/json" },
1898
+ body: body
1899
+ });
1900
+ var data = await r.json().catch(function () { return {}; });
1901
+ if (!r.ok) {
1902
+ showBanner("err", (data && data.error) ? String(data.error) : "删除失败 " + r.status);
1903
+ return;
1904
+ }
1905
+ showBanner("info", "已删除 " + (data.deleted || 0) + " 条");
1906
+ await loadFacets({ autoLoad: false });
1907
+ if (state.tab === "dash") {
1908
+ await loadDashboard();
1909
+ } else {
1910
+ await loadList(state.page);
1911
+ }
1912
+ }
1913
+
1914
+ async function addMemory() {
1915
+ var agentId = document.getElementById("addAgentId").value.trim();
1916
+ var category = document.getElementById("addCategory").value;
1917
+ var text = document.getElementById("addText").value;
1918
+ if (!agentId) {
1919
+ showBanner("err", "请填写 Agent ID");
1920
+ return;
1921
+ }
1922
+ if (!category) {
1923
+ showBanner("err", "请选择记忆类型");
1924
+ return;
1925
+ }
1926
+ if (!String(text).trim()) {
1927
+ showBanner("err", "请填写正文");
1928
+ return;
1929
+ }
1930
+ showBanner("", "");
1931
+ var r = await fetchMemoryApi("/add", {
1932
+ method: "POST",
1933
+ headers: { "Content-Type": "application/json" },
1934
+ body: JSON.stringify({ agentId: agentId, category: category, text: text })
1935
+ });
1936
+ var data = await r.json().catch(function () { return {}; });
1937
+ if (!r.ok) {
1938
+ showBanner("err", (data && data.error) ? String(data.error) : "添加失败 " + r.status);
1939
+ return;
1940
+ }
1941
+ showBanner("info", "已插入记忆(未做去重/冲突检测)");
1942
+ document.getElementById("addText").value = "";
1943
+ closeInsertModal();
1944
+ await loadFacets({ autoLoad: false });
1945
+ if (state.selectedAgentId.trim() === agentId || state.selectedSessionId.trim() === "manual_insert") {
1946
+ if (state.tab === "dash") {
1947
+ await loadDashboard();
1948
+ } else {
1949
+ await loadList(1);
1950
+ }
1951
+ }
1952
+ }
1953
+
1954
+ function onTabChange(tab) {
1955
+ if (tab === "full" && state.cfg && !state.cfg.enableFullContextMemory) return;
1956
+ if (tab === "self" && state.cfg && !state.cfg.enableSelfImprovingMemory) return;
1957
+ state.tab = tab;
1958
+ document.querySelectorAll(".tab").forEach(function (b) {
1959
+ b.classList.toggle("active", b.getAttribute("data-tab") === tab);
1960
+ });
1961
+ var so = document.getElementById("sortOrder");
1962
+ if (so) {
1963
+ so.value = state.sortOrderByTab[tab] || "desc";
1964
+ }
1965
+ updateToolbarForTab();
1966
+ fillAddCategorySelect();
1967
+ if (tab === "dash") {
1968
+ loadDashboard();
1969
+ renderPager();
1970
+ } else {
1971
+ loadFacets();
1972
+ }
1973
+ }
1974
+
1975
+ document.querySelectorAll(".tab").forEach(function (b) {
1976
+ b.onclick = function () { onTabChange(b.getAttribute("data-tab")); };
1977
+ });
1978
+ document.getElementById("agentId").addEventListener("change", function () {
1979
+ state.selectedAgentId = this.value.trim();
1980
+ maybeAutoLoadForCurrentTab();
1981
+ });
1982
+ document.getElementById("sessionId").addEventListener("change", function () {
1983
+ state.selectedSessionId = this.value.trim();
1984
+ maybeAutoLoadForCurrentTab();
1985
+ });
1986
+ document.getElementById("memoryTypeFilter").addEventListener("change", function () {
1987
+ if (state.tab === "dash") {
1988
+ return;
1989
+ }
1990
+ state.categoryFilterByTab[state.tab] = this.value.trim();
1991
+ loadList(1);
1992
+ });
1993
+ function onTimeFilterChange() {
1994
+ showBanner("", "");
1995
+ loadFacets();
1996
+ }
1997
+ function onTimeManualChange() {
1998
+ state.timeQuickMs = null;
1999
+ syncTimeRangeHighlight();
2000
+ onTimeFilterChange();
2001
+ }
2002
+ document.getElementById("timeFromInput").addEventListener("change", onTimeManualChange);
2003
+ document.getElementById("timeToInput").addEventListener("change", onTimeManualChange);
2004
+ document.getElementById("btnTime24h").onclick = function () {
2005
+ applyQuickRangeMs(24 * 3600 * 1000);
2006
+ onTimeFilterChange();
2007
+ };
2008
+ document.getElementById("btnTime7d").onclick = function () {
2009
+ applyQuickRangeMs(7 * 24 * 3600 * 1000);
2010
+ onTimeFilterChange();
2011
+ };
2012
+ document.getElementById("btnTime30d").onclick = function () {
2013
+ applyQuickRangeMs(30 * 24 * 3600 * 1000);
2014
+ onTimeFilterChange();
2015
+ };
2016
+ document.getElementById("sortOrder").addEventListener("change", function () {
2017
+ if (state.tab === "dash") {
2018
+ return;
2019
+ }
2020
+ state.sortOrderByTab[state.tab] = this.value;
2021
+ loadList(1);
2022
+ });
2023
+ document.getElementById("btnRefresh").onclick = function () {
2024
+ if (state.timeQuickMs != null) {
2025
+ applyQuickRangeMs(state.timeQuickMs);
2026
+ }
2027
+ if (state.tab === "dash") {
2028
+ loadDashboard();
2029
+ } else {
2030
+ loadList(1);
2031
+ }
2032
+ };
2033
+ document.getElementById("btnDeleteSelected").onclick = deleteSelected;
2034
+ document.getElementById("btnInsertMemory").onclick = openInsertModal;
2035
+ document.getElementById("btnModalCancel").onclick = closeInsertModal;
2036
+ document.getElementById("btnModalInsert").onclick = addMemory;
2037
+ document.getElementById("insertModal").addEventListener("click", function (e) {
2038
+ if (e.target === this) closeInsertModal();
2039
+ });
2040
+ function closeTextPeekModal() {
2041
+ var m = document.getElementById("textPeekModal");
2042
+ if (!m) return;
2043
+ m.classList.remove("open");
2044
+ m.setAttribute("aria-hidden", "true");
2045
+ var b = document.getElementById("textPeekBody");
2046
+ if (b) b.textContent = "";
2047
+ var titleEl = document.getElementById("textPeekTitle");
2048
+ if (titleEl) titleEl.textContent = "记忆召回信息";
2049
+ }
2050
+ document.getElementById("btnTextPeekClose").onclick = closeTextPeekModal;
2051
+ document.getElementById("textPeekModal").addEventListener("click", function (e) {
2052
+ if (e.target === this) closeTextPeekModal();
2053
+ });
2054
+ document.addEventListener("keydown", function (e) {
2055
+ if (e.key !== "Escape") return;
2056
+ var pm = document.getElementById("textPeekModal");
2057
+ if (pm && pm.classList.contains("open")) {
2058
+ closeTextPeekModal();
2059
+ return;
2060
+ }
2061
+ if (document.getElementById("insertModal").classList.contains("open")) {
2062
+ closeInsertModal();
2063
+ }
2064
+ });
2065
+
2066
+ function toggleTextCellPreview(cell) {
2067
+ var prev = cell.querySelector(".text-preview");
2068
+ if (!prev || prev.classList.contains("text-preview-short")) return;
2069
+ prev.classList.toggle("expanded");
2070
+ var hint = cell.querySelector(".text-hint");
2071
+ if (hint) {
2072
+ hint.textContent = prev.classList.contains("expanded") ? "点击上文或此处收起全文" : "点击上文或此处展开全文";
2073
+ }
2074
+ }
2075
+
2076
+ document.getElementById("tableWrap").addEventListener("click", function (e) {
2077
+ var om = e.target.closest && e.target.closest(".inj-open-modal");
2078
+ if (om) {
2079
+ e.preventDefault();
2080
+ e.stopPropagation();
2081
+ var pre = om.nextElementSibling;
2082
+ if (pre && pre.tagName === "PRE") {
2083
+ document.getElementById("textPeekBody").textContent = pre.textContent;
2084
+ var titleEl = document.getElementById("textPeekTitle");
2085
+ if (titleEl) {
2086
+ titleEl.textContent = modalTitleForInjectClass(om.className || "");
2087
+ }
2088
+ document.getElementById("textPeekModal").classList.add("open");
2089
+ document.getElementById("textPeekModal").setAttribute("aria-hidden", "false");
2090
+ }
2091
+ return;
2092
+ }
2093
+ var hint = e.target.closest && e.target.closest(".text-hint");
2094
+ var cell = hint ? hint.closest(".text-cell") : e.target.closest && e.target.closest(".text-cell");
2095
+ if (!cell) return;
2096
+ if (e.target.closest && e.target.closest("input, button, a, label, summary")) return;
2097
+ if (hint || (e.target.closest && e.target.closest(".text-preview"))) {
2098
+ toggleTextCellPreview(cell);
2099
+ }
2100
+ });
2101
+
2102
+ document.getElementById("tableWrap").addEventListener("keydown", function (e) {
2103
+ if (e.key !== "Enter" && e.key !== " ") return;
2104
+ var prev = e.target.classList && e.target.classList.contains("text-preview") ? e.target : null;
2105
+ if (!prev || prev.classList.contains("text-preview-short")) return;
2106
+ e.preventDefault();
2107
+ var cell = prev.closest(".text-cell");
2108
+ if (cell) toggleTextCellPreview(cell);
2109
+ });
2110
+
2111
+ applyQuickRangeMs(24 * 3600 * 1000);
2112
+ updateToolbarForTab();
2113
+ loadConfig()
2114
+ .then(function () { return loadFacets(); })
2115
+ .catch(function (e) {
2116
+ showBanner("err", String(e));
2117
+ });
2118
+ })();
2119
+ `;
2120
+ return s.replace(/<\/script/gi, "<\\/script");
2121
+ }