From 8a6cad7778a366719a2f700bc87ece385a7b9727 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sun, 12 Apr 2026 16:15:55 +0800 Subject: [PATCH 01/24] tests: stub recall prompt headers in retrieval-config harness --- tests/retrieval-config.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/retrieval-config.mjs b/tests/retrieval-config.mjs index 34bc20d..9ec80e5 100644 --- a/tests/retrieval-config.mjs +++ b/tests/retrieval-config.mjs @@ -772,6 +772,10 @@ const retrieve = await loadRetrieve({ describeScopeBucket(bucket = "") { return String(bucket || ""); }, + EXTRACTION_CONTEXT_REVIEW_HEADER: + "--- 以下是上下文回顾(已提取过),仅供理解剧情 ---", + RECALL_TARGET_CONTENT_HEADER: + "--- 以下是本次需要召回记忆的新对话内容 ---", buildTaskPrompt() { return { systemPrompt: "" }; }, From 6045c4fa0d7bc747802e515849b5c395c238732a Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sun, 12 Apr 2026 22:30:41 +0800 Subject: [PATCH 02/24] feat(ui): redesign task monitor workspace --- style.css | 953 ++++++++++++++++++++++++++++++++++++++++++++++++++ ui/panel.html | 171 +++++---- ui/panel.js | 557 ++++++++++++++++++++++++++--- 3 files changed, 1557 insertions(+), 124 deletions(-) diff --git a/style.css b/style.css index 0260fb9..b7348e8 100644 --- a/style.css +++ b/style.css @@ -618,6 +618,910 @@ display: flex; } +/* ==================== Task-Mode (对称 config-mode) ==================== */ + +#st-bme-panel.task-mode .bme-tab-content { + display: none; +} + +#st-bme-panel.task-mode .bme-task-sidebar { + display: flex; +} + +#st-bme-panel.task-mode .bme-graph-workspace { + display: none; +} + +#st-bme-panel.task-mode .bme-task-workspace { + display: flex; +} + +.bme-task-sidebar { + display: none; + flex-direction: column; + gap: 2px; + padding: 12px 8px; + overflow-y: auto; +} + +.bme-task-sidebar-header { + padding: 8px 10px 12px; + margin-bottom: 4px; +} + +.bme-task-sidebar-kicker { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1.2px; + color: var(--bme-primary); + margin-bottom: 4px; +} + +.bme-task-sidebar-title { + font-size: 14px; + font-weight: 700; + color: var(--bme-on-surface); +} + +.bme-task-sidebar-help { + font-size: 11px; + color: var(--bme-on-surface-dim); + margin-top: 4px; + line-height: 1.4; +} + +.bme-task-nav-btn { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + border-radius: 8px; + border: none; + border-left: 3px solid transparent; + background: transparent; + color: var(--bme-on-surface-dim); + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all 0.15s ease; + white-space: nowrap; +} + +.bme-task-nav-btn:hover { + background: rgba(255, 255, 255, 0.04); + color: var(--bme-on-surface); +} + +.bme-task-nav-btn.active { + border-left-color: var(--bme-primary); + background: var(--bme-primary-dim); + color: var(--bme-primary); + font-weight: 600; +} + +.bme-task-nav-btn i { + width: 16px; + text-align: center; + font-size: 13px; +} + +.bme-task-nav-btn .bme-task-nav-badge { + margin-left: auto; + font-size: 10px; + padding: 2px 6px; + border-radius: 10px; + background: var(--bme-primary-dim); + color: var(--bme-primary); + font-weight: 600; +} + +.bme-task-workspace { + display: none; + flex: 1; + flex-direction: column; + min-height: 0; + overflow-y: auto; + background: + radial-gradient( + circle at top right, + var(--bme-primary-dim, rgba(233, 69, 96, 0.15)), + transparent 32% + ), + linear-gradient(180deg, rgba(255, 255, 255, 0.02), transparent 20%), + var(--bme-surface-lowest, #0e0e11); +} + +.bme-task-workspace-header { + padding: 20px 24px 14px; + border-bottom: 1px solid var(--bme-border); + flex-shrink: 0; +} + +.bme-task-workspace-kicker { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1.2px; + color: var(--bme-primary); + margin-bottom: 4px; +} + +.bme-task-workspace-title { + font-size: 20px; + font-weight: 700; + color: var(--bme-on-surface); + margin-bottom: 4px; +} + +.bme-task-workspace-desc { + font-size: 12px; + color: var(--bme-on-surface-dim); + line-height: 1.5; +} + +.bme-task-workspace-body { + padding: 16px 24px 32px; + flex: 1; + min-height: 0; + overflow-y: auto; +} + +.bme-task-section { + display: none; +} + +.bme-task-section.active { + display: block; +} + +/* --- Task nav mobile pill selector (visible only on mobile) --- */ +.bme-task-nav-mobile { + display: none; + gap: 6px; + padding: 8px 16px; + overflow-x: auto; + flex-shrink: 0; + border-bottom: 1px solid var(--bme-border); + -webkit-overflow-scrolling: touch; +} + +.bme-task-nav-mobile .bme-task-nav-pill { + display: flex; + align-items: center; + gap: 4px; + padding: 6px 12px; + border-radius: 20px; + border: 1px solid var(--bme-border); + background: transparent; + color: var(--bme-on-surface-dim); + font-size: 11px; + font-weight: 500; + cursor: pointer; + white-space: nowrap; + transition: all 0.15s ease; +} + +.bme-task-nav-mobile .bme-task-nav-pill:hover { + border-color: var(--bme-primary); + color: var(--bme-on-surface); +} + +.bme-task-nav-mobile .bme-task-nav-pill.active { + background: var(--bme-primary); + border-color: var(--bme-primary); + color: #fff; + font-weight: 600; +} + +/* ==================== Pipeline Overview ==================== */ + +.bme-pipeline-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin-bottom: 16px; +} + +.bme-pipeline-card { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 14px; + background: var(--bme-surface, #131316); + border: 1px solid var(--bme-border); + border-radius: 8px; + border-left: 3px solid var(--bme-border); + transition: border-color 0.2s; +} + +.bme-pipeline-card[data-status="idle"], +.bme-pipeline-card[data-status="ready"] { + border-left-color: #2ecc71; +} +.bme-pipeline-card[data-status="running"], +.bme-pipeline-card[data-status="building"] { + border-left-color: #00d4ff; +} +.bme-pipeline-card[data-status="queued"], +.bme-pipeline-card[data-status="warning"] { + border-left-color: #f39c12; +} +.bme-pipeline-card[data-status="error"], +.bme-pipeline-card[data-status="blocked"] { + border-left-color: #e74c3c; +} + +.bme-pipeline-dot { + width: 10px; + height: 10px; + border-radius: 50%; + margin-top: 3px; + flex-shrink: 0; +} + +.bme-pipeline-dot.green { background: #2ecc71; box-shadow: 0 0 6px rgba(46,204,113,.4); } +.bme-pipeline-dot.cyan { background: #00d4ff; box-shadow: 0 0 6px rgba(0,212,255,.4); animation: bme-pulse-dot 1.5s ease-in-out infinite; } +.bme-pipeline-dot.amber { background: #f39c12; box-shadow: 0 0 6px rgba(243,156,18,.4); } +.bme-pipeline-dot.red { background: #e74c3c; box-shadow: 0 0 6px rgba(231,76,60,.4); } + +@keyframes bme-pulse-dot { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +.bme-pipeline-info { + flex: 1; + min-width: 0; +} + +.bme-pipeline-name { + font-size: 12px; + font-weight: 700; + color: var(--bme-on-surface); + margin-bottom: 2px; +} + +.bme-pipeline-status { + font-size: 11px; + font-weight: 600; + margin-bottom: 2px; +} + +.bme-pipeline-status.green { color: #2ecc71; } +.bme-pipeline-status.cyan { color: #00d4ff; } +.bme-pipeline-status.amber { color: #f39c12; } +.bme-pipeline-status.red { color: #e74c3c; } + +.bme-pipeline-detail { + font-size: 10px; + color: var(--bme-on-surface-dim); +} + +/* --- Batch Progress --- */ + +.bme-batch-progress { + background: var(--bme-surface, #131316); + border: 1px solid var(--bme-border); + border-radius: 8px; + padding: 16px; + margin-bottom: 16px; +} + +.bme-batch-stages { + display: flex; + align-items: flex-start; + gap: 0; + margin-bottom: 12px; +} + +.bme-batch-stage { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + position: relative; +} + +.bme-batch-stage-dot { + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + z-index: 1; + border: 2px solid var(--bme-border); + background: var(--bme-surface-lowest, #0e0e11); + color: var(--bme-on-surface-dim); +} + +.bme-batch-stage-dot.done { + background: #2ecc71; + border-color: #2ecc71; + color: #fff; +} + +.bme-batch-stage-dot.running { + background: #00d4ff; + border-color: #00d4ff; + color: #fff; + animation: bme-pulse-dot 1.5s ease-in-out infinite; +} + +.bme-batch-stage-label { + font-size: 11px; + font-weight: 600; + color: var(--bme-on-surface-dim); + text-align: center; +} + +.bme-batch-stage-detail { + font-size: 10px; + color: var(--bme-on-surface-dim); + text-align: center; + max-width: 100px; + opacity: 0.7; +} + +.bme-batch-stage-line { + position: absolute; + top: 14px; + left: calc(50% + 18px); + width: calc(100% - 36px); + height: 2px; + background: var(--bme-border); + z-index: 0; +} + +.bme-batch-stage-line.done { background: #2ecc71; } +.bme-batch-stage-line.running { background: linear-gradient(90deg, #2ecc71, #00d4ff); } + +.bme-batch-stage:last-child .bme-batch-stage-line { display: none; } + +.bme-batch-meta { + display: flex; + gap: 16px; + font-size: 11px; + color: var(--bme-on-surface-dim); + padding-top: 10px; + border-top: 1px solid var(--bme-border); +} + +.bme-batch-meta i { + font-size: 10px; + color: var(--bme-primary); + opacity: 0.6; + margin-right: 4px; +} + +/* --- Status Summary --- */ + +.bme-status-summary { + background: var(--bme-surface, #131316); + border: 1px solid var(--bme-border); + border-radius: 8px; + padding: 12px 16px; +} + +.bme-status-summary-title { + font-size: 12px; + font-weight: 700; + color: var(--bme-on-surface); + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 6px; +} + +.bme-status-summary-title i { + color: var(--bme-primary); + font-size: 11px; +} + +.bme-status-row { + display: flex; + align-items: baseline; + gap: 10px; + padding: 6px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.03); +} + +.bme-status-row:last-child { border-bottom: none; } + +.bme-status-row-label { + font-size: 11px; + font-weight: 600; + width: 56px; + flex-shrink: 0; + display: flex; + align-items: center; + gap: 6px; + color: var(--bme-on-surface); +} + +.bme-status-row-label .bme-sdot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; +} + +.bme-status-row-value { + font-size: 11px; + color: var(--bme-on-surface-dim); + flex: 1; + min-width: 0; +} + +/* ==================== Task Timeline ==================== */ + +.bme-timeline-toolbar { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: var(--bme-surface, #131316); + border: 1px solid var(--bme-border); + border-radius: 8px; + margin-bottom: 12px; + flex-wrap: wrap; + position: sticky; + top: 0; + z-index: 5; +} + +.bme-timeline-filter { + padding: 4px 10px; + border-radius: 6px; + border: 1px solid var(--bme-border); + background: transparent; + color: var(--bme-on-surface-dim); + font-size: 11px; + cursor: pointer; +} + +.bme-timeline-filter:hover { + border-color: var(--bme-primary); + color: var(--bme-on-surface); +} + +.bme-timeline-search { + flex: 1; + min-width: 100px; + border: none; + background: transparent; + color: var(--bme-on-surface); + font-size: 12px; + outline: none; +} + +.bme-timeline-search::placeholder { + color: var(--bme-on-surface-dim); + opacity: 0.5; +} + +.bme-timeline-divider { + width: 1px; + height: 16px; + background: var(--bme-border); +} + +.bme-timeline-action { + padding: 4px 8px; + border-radius: 6px; + border: none; + background: transparent; + color: var(--bme-on-surface-dim); + cursor: pointer; + font-size: 11px; + display: flex; + align-items: center; + gap: 4px; +} + +.bme-timeline-action:hover { color: var(--bme-on-surface); } +.bme-timeline-action.active { color: var(--bme-primary); } + +.bme-timeline-stack { + display: flex; + flex-direction: column; + gap: 0; +} + +.bme-timeline-entry { + position: relative; + padding: 12px 14px; + background: var(--bme-surface, #131316); + border: 1px solid var(--bme-border); + border-radius: 8px; + margin-bottom: 2px; + transition: border-color 0.15s; +} + +.bme-timeline-entry:hover { + border-color: rgba(255, 255, 255, 0.1); +} + +.bme-timeline-entry__head { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +.bme-timeline-entry__title { + font-size: 12px; + font-weight: 600; + color: var(--bme-on-surface); + flex: 1; + min-width: 0; +} + +.bme-timeline-entry__meta { + font-size: 10px; + color: var(--bme-on-surface-dim); +} + +.bme-timeline-entry__toggle { + background: transparent; + border: none; + color: var(--bme-on-surface-dim); + cursor: pointer; + padding: 2px; + font-size: 11px; + transition: transform 0.2s; +} + +.bme-timeline-entry.is-collapsed .bme-timeline-entry__toggle { + transform: rotate(-90deg); +} + +.bme-timeline-entry__detail { + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid var(--bme-border); +} + +.bme-timeline-entry.is-collapsed .bme-timeline-entry__detail { + display: none; +} + +.bme-timeline-substage { + display: flex; + align-items: baseline; + gap: 8px; + padding: 4px 0; + font-size: 11px; + color: var(--bme-on-surface-dim); +} + +.bme-timeline-substage i { + font-size: 10px; + width: 14px; + text-align: center; +} + +.bme-timeline-connector { + width: 2px; + height: 6px; + background: var(--bme-border); + margin: 0 auto; +} + +.bme-timeline-bottom-bar { + padding: 8px 14px; + font-size: 11px; + color: var(--bme-on-surface-dim); + text-align: center; +} + +/* ==================== Memory Browser Master-Detail ==================== */ + +.bme-memory-master-detail { + display: flex; + height: 100%; + min-height: 400px; + gap: 0; +} + +.bme-memory-list-panel { + width: 40%; + min-width: 200px; + max-width: 360px; + display: flex; + flex-direction: column; + border-right: 1px solid var(--bme-border); + overflow: hidden; +} + +.bme-memory-list-filters { + padding: 10px; + display: flex; + flex-direction: column; + gap: 6px; + border-bottom: 1px solid var(--bme-border); + flex-shrink: 0; +} + +.bme-memory-list-scroll { + flex: 1; + overflow-y: auto; +} + +.bme-memory-node-item { + padding: 10px 12px; + border-bottom: 1px solid rgba(255, 255, 255, 0.03); + border-left: 3px solid transparent; + cursor: pointer; + transition: all 0.12s; +} + +.bme-memory-node-item:hover { + background: rgba(255, 255, 255, 0.03); +} + +.bme-memory-node-item.selected { + border-left-color: var(--bme-primary); + background: var(--bme-primary-dim); +} + +.bme-memory-node-item__header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 4px; +} + +.bme-memory-node-item__type { + font-size: 10px; + font-weight: 700; + padding: 2px 6px; + border-radius: 4px; + text-transform: uppercase; +} + +.bme-memory-node-item__type.type-character { background: rgba(155,89,182,.2); color: #c084fc; } +.bme-memory-node-item__type.type-event { background: rgba(0,212,255,.15); color: #00d4ff; } +.bme-memory-node-item__type.type-location { background: rgba(46,204,113,.15); color: #2ecc71; } +.bme-memory-node-item__type.type-rule { background: rgba(243,156,18,.15); color: #f39c12; } +.bme-memory-node-item__type.type-thread { background: rgba(59,130,246,.15); color: #3b82f6; } +.bme-memory-node-item__type.type-default { background: rgba(255,255,255,.08); color: var(--bme-on-surface-dim); } + +.bme-memory-node-item__imp { + font-size: 10px; + font-family: monospace; + color: var(--bme-primary); +} + +.bme-memory-node-item__title { + font-size: 12px; + font-weight: 600; + color: var(--bme-on-surface); + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.bme-memory-node-item__preview { + font-size: 11px; + color: var(--bme-on-surface-dim); + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + margin-bottom: 4px; +} + +.bme-memory-node-item__meta { + display: flex; + gap: 8px; + font-size: 10px; + color: var(--bme-on-surface-dim); + opacity: 0.6; +} + +.bme-memory-detail-panel { + flex: 1; + overflow-y: auto; + padding: 20px; + min-width: 0; +} + +.bme-memory-detail-empty { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--bme-on-surface-dim); + font-size: 12px; +} + +.bme-memory-detail__title { + font-size: 18px; + font-weight: 700; + color: var(--bme-on-surface); + margin-bottom: 8px; +} + +.bme-memory-detail__badges { + display: flex; + gap: 6px; + flex-wrap: wrap; + margin-bottom: 14px; +} + +.bme-memory-detail__desc { + font-size: 12px; + line-height: 1.6; + color: var(--bme-on-surface); + margin-bottom: 16px; + white-space: pre-wrap; +} + +.bme-memory-detail__fields { + margin-bottom: 16px; +} + +.bme-memory-detail__field-row { + display: flex; + gap: 8px; + padding: 4px 0; + font-size: 11px; + border-bottom: 1px solid rgba(255, 255, 255, 0.03); +} + +.bme-memory-detail__field-key { + color: var(--bme-on-surface-dim); + min-width: 80px; + font-weight: 600; +} + +.bme-memory-detail__field-val { + color: var(--bme-on-surface); +} + +.bme-memory-detail__stats { + display: flex; + gap: 14px; + flex-wrap: wrap; + font-size: 11px; + color: var(--bme-on-surface-dim); + margin-bottom: 16px; + padding: 10px 0; + border-top: 1px solid var(--bme-border); + border-bottom: 1px solid var(--bme-border); +} + +.bme-memory-detail__actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +/* ==================== Injection Preview ==================== */ + +.bme-injection-token-bar { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 14px; + background: var(--bme-surface, #131316); + border: 1px solid var(--bme-border); + border-radius: 8px; + margin-bottom: 12px; +} + +.bme-injection-token-bar__label { + font-size: 13px; + font-weight: 700; + color: var(--bme-on-surface); + white-space: nowrap; +} + +.bme-injection-token-bar__track { + flex: 1; + height: 6px; + border-radius: 3px; + background: var(--bme-border); + overflow: hidden; +} + +.bme-injection-token-bar__fill { + height: 100%; + border-radius: 3px; + background: var(--bme-primary); + transition: width 0.3s; +} + +.bme-injection-token-bar__breakdown { + font-size: 10px; + color: var(--bme-on-surface-dim); + white-space: nowrap; +} + +.bme-injection-card-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin-bottom: 16px; +} + +.bme-injection-card { + background: var(--bme-surface, #131316); + border: 1px solid var(--bme-border); + border-radius: 8px; + padding: 14px; + border-top: 2px solid var(--bme-border); +} + +.bme-injection-card__header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; +} + +.bme-injection-card__type { + font-size: 10px; + font-weight: 700; + padding: 2px 6px; + border-radius: 4px; +} + +.bme-injection-card__tokens { + font-size: 10px; + color: var(--bme-on-surface-dim); +} + +.bme-injection-card__body { + font-size: 11px; + line-height: 1.5; + color: var(--bme-on-surface-dim); + max-height: 120px; + overflow-y: auto; + white-space: pre-wrap; +} + +/* ==================== Persistence Status ==================== */ + +.bme-persist-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; + margin-bottom: 16px; +} + +.bme-persist-kv { + background: var(--bme-surface, #131316); + border: 1px solid var(--bme-border); + border-radius: 8px; + padding: 14px 16px; +} + +.bme-persist-kv__row { + display: flex; + justify-content: space-between; + align-items: baseline; + padding: 5px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.03); + font-size: 11px; +} + +.bme-persist-kv__row:last-child { border-bottom: none; } +.bme-persist-kv__row span { color: var(--bme-on-surface-dim); } +.bme-persist-kv__row strong { color: var(--bme-on-surface); font-weight: 600; } + +.bme-persist-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + .bme-graph-toolbar { display: flex; align-items: center; @@ -5985,3 +6889,52 @@ flex-direction: column; } } + +/* ═══════════════════════════════════════════════════════════ + Task Monitor Mobile Responsive + ═══════════════════════════════════════════════════════════ */ + +@media (max-width: 768px) { + #st-bme-panel.task-mode .bme-task-sidebar { + display: none; + } + .bme-task-nav-mobile { + display: flex; + } + .bme-task-workspace-header { + padding: 14px 16px 10px; + } + .bme-task-workspace-body { + padding: 12px 12px 24px; + } + .bme-pipeline-grid { + grid-template-columns: 1fr; + } + .bme-injection-card-grid { + grid-template-columns: 1fr; + } + .bme-persist-grid { + grid-template-columns: 1fr; + } + .bme-memory-master-detail { + flex-direction: column; + min-height: 0; + } + .bme-memory-list-panel { + width: 100%; + max-width: none; + border-right: none; + border-bottom: 1px solid var(--bme-border); + max-height: 40vh; + } + .bme-memory-detail-panel { + padding: 14px; + } + .bme-batch-stages { + flex-wrap: wrap; + gap: 4px; + } + .bme-batch-stage-line { + display: none; + } +} diff --git a/ui/panel.html b/ui/panel.html index ea83d8a..1ddb734 100644 --- a/ui/panel.html +++ b/ui/panel.html @@ -69,13 +69,9 @@ 总览 - - - + + + + + + + +
@@ -271,45 +319,6 @@
    -
    - -
      -
      - -
      -
      -
      -
      -
      @@ -2638,20 +2647,6 @@ /> -
      -
      -
      消息追踪
      -

      这一轮到底发了什么?

      -

      - 用更白话的方式展示最近一次注入主 AI 的内容,以及送去提取模型的实际请求。 -

      -
      -
      -
      -
      + +
      +
      +
      +
      任务监控
      +

      ST-BME 任务流工作区

      +

      + 实时查看所有任务管线的运行状态与当前批次进度。 +

      +
      +
      + +
      + + + + + + +
      + +
      +
      +
      +
      +
      +
      +
      +
      +
      总览 - - +
      +
      + ${detail ? `
      ${_escHtml(detail)}
      ` : ""} + ${substages} +
      +
      + `; + }).join(""); + + el.innerHTML = ` +
      + + ${timeline.length} 条记录 +
      +
      ${entries}
      + `; +} + +// ---------- Memory Browser (Master-Detail) ---------- + +function _refreshTaskMemoryBrowser() { + const el = document.getElementById("bme-task-memory"); + if (!el) return; + + const graph = _getGraph?.(); + if (!graph) { + el.innerHTML = '
      图谱未加载
      '; + return; + } + + const nodes = graph.nodes || []; + const sorted = nodes.slice().sort((a, b) => (b.importance || 0) - (a.importance || 0)); + + const typeClass = (type) => { + switch (type) { + case "pov_memory": case "character": return "type-character"; + case "event": return "type-event"; + case "location": return "type-location"; + case "rule": return "type-rule"; + case "thread": return "type-thread"; + default: return "type-default"; + } + }; + + const typeLabel = (node) => { + if (node.scope === "characterPov") return "角色POV"; + if (node.scope === "userPov") return "用户POV"; + return node.type || "memory"; + }; + + const listItems = sorted.map((node) => { + const sel = node.id === currentSelectedMemoryNodeId ? "selected" : ""; + const preview = (node.description || "").slice(0, 80); + return ` +
      +
      + ${_escHtml(typeLabel(node))} + IMP: ${typeof node.importance === "number" ? node.importance.toFixed(1) : "—"} +
      +
      ${_escHtml(node.label || node.id)}
      +
      ${_escHtml(preview)}
      +
      + SEQ: ${node.lastSeq ?? "—"} + ${node.region ? `LOC: ${_escHtml(node.region)}` : ""} +
      +
      `; + }).join(""); + + const detailHtml = _renderMemoryDetailPanel(sorted.find((n) => n.id === currentSelectedMemoryNodeId) || null, graph); + + el.innerHTML = ` +
      +
      +
      + + +
      +
      + ${listItems || '
      无节点
      '} +
      +
      +
      + ${detailHtml} +
      +
      + `; + + _bindTaskMemoryListClick(); +} + +function _bindTaskMemoryListClick() { + const list = document.getElementById("bme-task-memory-list"); + if (!list) return; + list.addEventListener("click", (e) => { + const item = e.target.closest(".bme-memory-node-item"); + if (!item) return; + currentSelectedMemoryNodeId = item.dataset.nodeId || ""; + list.querySelectorAll(".bme-memory-node-item").forEach((n) => n.classList.toggle("selected", n.dataset.nodeId === currentSelectedMemoryNodeId)); + const graph = _getGraph?.(); + const node = (graph?.nodes || []).find((n) => n.id === currentSelectedMemoryNodeId) || null; + const detailEl = document.getElementById("bme-task-memory-detail"); + if (detailEl) detailEl.innerHTML = _renderMemoryDetailPanel(node, graph); + }); +} + +function _renderMemoryDetailPanel(node, graph) { + if (!node) return '
      选择左侧节点查看详情
      '; + + const edges = (graph?.edges || []).filter((e) => e.source === node.id || e.target === node.id); + const badges = [ + node.type ? `${_escHtml(node.type)}` : "", + node.scope ? `${_escHtml(node.scope)}` : "", + node.archived ? 'ARCHIVED' : "", + ].filter(Boolean).join(""); + + const fields = [ + ["ID", node.id], + ["Owner", node.owner || "—"], + ["Region", node.region || "—"], + ["Importance", typeof node.importance === "number" ? node.importance.toFixed(2) : "—"], + ["Last Seq", node.lastSeq ?? "—"], + ["Created", node.createdAt || "—"], + ["Updated", node.updatedAt || "—"], + ]; + + return ` +
      ${_escHtml(node.label || node.id)}
      +
      ${badges}
      +
      ${_escHtml(node.description || "无描述")}
      +
      + ${fields.map(([k, v]) => `
      ${_escHtml(k)}${_escHtml(String(v))}
      `).join("")} +
      +
      + ${edges.length} 条连接 + recall ${node.recallCount ?? 0} +
      + `; +} + +// ---------- Injection Preview ---------- + +function _refreshTaskInjectionPreview() { + const el = document.getElementById("bme-task-injection"); + if (!el) return; + + const debug = _getRuntimeDebugSnapshot?.() || {}; + const rd = debug.runtimeDebug || {}; + const injection = rd?.injections?.recall || null; + + if (!injection) { + el.innerHTML = '
      暂无注入数据
      '; + return; + } + + const totalTokens = injection.tokenCount || 0; + const budgetTokens = injection.budgetTokens || totalTokens || 1; + const pct = Math.min(100, Math.round((totalTokens / budgetTokens) * 100)); + + const sections = Array.isArray(injection.sections) ? injection.sections : []; + const injectionText = injection.text || injection.injectionText || ""; + + const cardsHtml = sections.length + ? sections.map((s) => ` +
      +
      + ${_escHtml(s.title || s.type || "section")} + ${typeof s.tokenCount === "number" ? `${s.tokenCount} tok` : ""} +
      +
      ${_escHtml(s.text || s.content || "")}
      +
      + `).join("") + : `
      +
      + Full Injection + ${totalTokens} tok +
      +
      ${_escHtml(injectionText.slice(0, 2000))}${injectionText.length > 2000 ? "…" : ""}
      +
      `; + + el.innerHTML = ` +
      + ${totalTokens} / ${budgetTokens} tok +
      +
      +
      + ${pct}% +
      +
      ${cardsHtml}
      + `; +} + +// ---------- Message Trace ---------- + +function _refreshTaskMessageTrace() { + const el = document.getElementById("bme-task-trace"); + if (!el) return; + + const settings = _getSettings?.() || {}; + const state = _getMessageTraceWorkspaceState(settings); + el.innerHTML = _renderMessageTraceWorkspace(state); +} + +// ---------- Persistence Status ---------- + +function _refreshTaskPersistence() { + const el = document.getElementById("bme-task-persistence"); + if (!el) return; + + const graph = _getGraph?.() || {}; + const ps = graph.graphPersistenceState || {}; + const rs = graph.runtimeState || {}; + + const kvs = [ + ["Load State", ps.loadState || "unknown"], + ["Storage Tier", ps.acceptedStorageTier || "—"], + ["Last Accepted Rev", ps.lastAcceptedRevision ?? "—"], + ["Commit Marker", ps.currentCommitMarker ? "present" : "none"], + ["Blocked Reason", ps.blockedReason || "—"], + ["IDB Snapshot Rev", ps.indexedDbSnapshotRevision ?? "—"], + ["Chat State Rev", ps.chatStateSnapshotRevision ?? "—"], + ["Shadow Snapshot", ps.hasShadowSnapshot ? "yes" : "no"], + ]; + + const kvHtml = kvs.map(([k, v]) => `
      ${_escHtml(k)}${_escHtml(String(v))}
      `).join(""); + + const journalCount = Array.isArray(rs.historyState?.batchJournal) ? rs.historyState.batchJournal.length : 0; + const secondaryKvs = [ + ["Graph Nodes", String((graph.nodes || []).length)], + ["Graph Edges", String((graph.edges || []).length)], + ["Batch Journal", String(journalCount)], + ["Runtime Rev", String(rs.graphRevision ?? "—")], + ]; + const secondaryHtml = secondaryKvs.map(([k, v]) => `
      ${_escHtml(k)}${_escHtml(v)}
      `).join(""); + + el.innerHTML = ` +
      +
      +
      Persistence State
      + ${kvHtml} +
      +
      +
      Runtime Stats
      + ${secondaryHtml} +
      +
      + `; } // ==================== 图谱视图切换 ==================== @@ -4025,19 +4514,8 @@ function _bindActions() { if (!result?.skipDashboardRefresh) { _refreshDashboard(); _refreshGraph(); - if ( - document - .getElementById("bme-pane-memory") - ?.classList.contains("active") - ) { - _refreshMemoryBrowser(); - } - if ( - document - .getElementById("bme-pane-injection") - ?.classList.contains("active") - ) { - await _refreshInjectionPreview(); + if (currentTabId === "task") { + _refreshTaskMonitor(); } } if (!result?.handledToast) { @@ -4117,13 +4595,7 @@ function _bindActions() { }); _refreshDashboard(); _refreshGraph(); - if ( - document - .getElementById("bme-pane-memory") - ?.classList.contains("active") - ) { - _refreshMemoryBrowser(); - } + if (currentTabId === "task") _refreshTaskMonitor(); } catch (error) { console.error("[ST-BME] Action extractTask failed:", error); toastr.error(`重新提取失败: ${error?.message || error}`, "ST-BME"); @@ -4204,13 +4676,7 @@ function _bindActions() { }); _refreshDashboard(); _refreshGraph(); - if ( - document - .getElementById("bme-pane-memory") - ?.classList.contains("active") - ) { - _refreshMemoryBrowser(); - } + if (currentTabId === "task") _refreshTaskMonitor(); } catch (error) { console.error("[ST-BME] Action rebuildSummaryState failed:", error); toastr.error(`重建总结状态失败: ${error?.message || error}`, "ST-BME"); @@ -4248,13 +4714,7 @@ function _bindActions() { ); _refreshDashboard(); _refreshGraph(); - if ( - document - .getElementById("bme-pane-memory") - ?.classList.contains("active") - ) { - _refreshMemoryBrowser(); - } + if (currentTabId === "task") _refreshTaskMonitor(); } catch (error) { console.error("[ST-BME] Action clearGraphRange failed:", error); toastr.error(`按楼层范围清理失败: ${error?.message || error}`, "ST-BME"); @@ -4410,8 +4870,7 @@ function _bindActions() { _refreshDashboard(); _refreshGraph(); _refreshSummaryWorkspace(); - _refreshMemoryBrowser(); - void _refreshInjectionPreview(); + if (currentTabId === "task") _refreshTaskMonitor(); } catch (error) { console.error(`[ST-BME] summary workspace action failed: ${actionKey}`, error); toastr.error(String(error?.message || error || "操作失败"), "ST-BME"); From 88c2703b87e868fa38b671bf4853e4051147abbd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 14:31:00 +0000 Subject: [PATCH 03/24] chore: bump manifest version [skip ci] --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 6b50ff3..45e5b1e 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Youzini", - "version": "4.8.2", + "version": "4.8.3", "homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology" } From 17dc9c92bc8db9d2ec29a9989f16500f9829dc38 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 14:31:29 +0000 Subject: [PATCH 04/24] chore: bump manifest version [skip ci] --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 45e5b1e..fe6a019 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Youzini", - "version": "4.8.3", + "version": "4.8.4", "homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology" } From b193eed0199b41ac0fec08f3f739cedf8badaed7 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sun, 12 Apr 2026 22:38:32 +0800 Subject: [PATCH 05/24] fix(ui): task monitor data binding - use correct getter functions for pipeline/persistence/injection views --- style.css | 4 +++ ui/panel.js | 90 +++++++++++++++++++++++------------------------------ 2 files changed, 43 insertions(+), 51 deletions(-) diff --git a/style.css b/style.css index b7348e8..89f720d 100644 --- a/style.css +++ b/style.css @@ -1246,6 +1246,10 @@ flex-shrink: 0; } +.bme-memory-list-filters .bme-filter-select { + flex: 0 0 auto; +} + .bme-memory-list-scroll { flex: 1; overflow-y: auto; diff --git a/ui/panel.js b/ui/panel.js index 779053c..4dec443 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -1371,23 +1371,21 @@ function _refreshTaskPipelineOverview() { const el = document.getElementById("bme-task-pipeline"); if (!el) return; - const debug = _getRuntimeDebugSnapshot?.() || {}; - const rd = debug.runtimeDebug || {}; const graph = _getGraph?.() || {}; - const historyState = graph.runtimeState?.historyState || {}; - const graphPersistenceState = graph.graphPersistenceState || {}; + const historyState = graph.runtimeState?.historyState || graph.historyState || {}; + const loadInfo = _getGraphPersistenceSnapshot(); - const extraction = _resolvePipelineStatus(rd.lastExtractionStatus); - const vector = _resolvePipelineStatus(rd.lastVectorStatus); - const recall = _resolvePipelineStatus(rd.lastRecallStatus); - const persistLevel = graphPersistenceState.loadState === "loaded" ? "info" : graphPersistenceState.loadState === "loading" ? "info" : "warn"; + const extraction = _resolvePipelineStatus(_getLastExtractionStatus?.()); + const vector = _resolvePipelineStatus(_getLastVectorStatus?.()); + const recall = _resolvePipelineStatus(_getLastRecallStatus?.()); + const persistLevel = loadInfo.loadState === "loaded" ? "info" : loadInfo.loadState === "loading" ? "info" : "warn"; const persistence = _resolvePipelineStatus({ - text: graphPersistenceState.loadState || "unknown", - meta: `rev ${graphPersistenceState.lastAcceptedRevision || 0}`, + text: loadInfo.loadState || "unknown", + meta: `rev ${loadInfo.revision || 0}`, level: persistLevel, }); - const batchStatus = historyState.lastBatchStatus || {}; + const batchStatus = _getLatestBatchStatusSnapshot() || {}; const stages = [ { key: "core", label: "Core" }, { key: "structural", label: "结构" }, @@ -1670,49 +1668,41 @@ function _refreshTaskInjectionPreview() { const el = document.getElementById("bme-task-injection"); if (!el) return; - const debug = _getRuntimeDebugSnapshot?.() || {}; - const rd = debug.runtimeDebug || {}; - const injection = rd?.injections?.recall || null; - - if (!injection) { - el.innerHTML = '
      暂无注入数据
      '; + const injectionText = String(_getLastInjection?.() || "").trim(); + if (!injectionText) { + el.innerHTML = '
      暂无注入数据——等待第一次召回注入后显示。
      '; return; } - const totalTokens = injection.tokenCount || 0; - const budgetTokens = injection.budgetTokens || totalTokens || 1; - const pct = Math.min(100, Math.round((totalTokens / budgetTokens) * 100)); + const debug = _getRuntimeDebugSnapshot?.() || {}; + const rd = debug.runtimeDebug || {}; + const recallSnap = rd?.injections?.recall || {}; + const totalTokens = recallSnap.tokenCount || 0; + const budgetTokens = recallSnap.budgetTokens || totalTokens || 1; + const pct = totalTokens > 0 ? Math.min(100, Math.round((totalTokens / budgetTokens) * 100)) : 0; - const sections = Array.isArray(injection.sections) ? injection.sections : []; - const injectionText = injection.text || injection.injectionText || ""; - - const cardsHtml = sections.length - ? sections.map((s) => ` -
      -
      - ${_escHtml(s.title || s.type || "section")} - ${typeof s.tokenCount === "number" ? `${s.tokenCount} tok` : ""} -
      -
      ${_escHtml(s.text || s.content || "")}
      -
      - `).join("") - : `
      -
      - Full Injection - ${totalTokens} tok -
      -
      ${_escHtml(injectionText.slice(0, 2000))}${injectionText.length > 2000 ? "…" : ""}
      -
      `; - - el.innerHTML = ` + const tokenBarHtml = totalTokens > 0 ? `
      ${totalTokens} / ${budgetTokens} tok
      ${pct}% +
      ` : ""; + + const previewText = injectionText.length > 3000 ? injectionText.slice(0, 3000) + "…" : injectionText; + + el.innerHTML = ` + ${tokenBarHtml} +
      +
      +
      + Recall Injection + ${totalTokens > 0 ? `${totalTokens} tok` : ""} +
      +
      ${_escHtml(previewText)}
      +
      -
      ${cardsHtml}
      `; } @@ -1734,18 +1724,16 @@ function _refreshTaskPersistence() { if (!el) return; const graph = _getGraph?.() || {}; - const ps = graph.graphPersistenceState || {}; + const ps = _getGraphPersistenceSnapshot(); const rs = graph.runtimeState || {}; const kvs = [ ["Load State", ps.loadState || "unknown"], - ["Storage Tier", ps.acceptedStorageTier || "—"], - ["Last Accepted Rev", ps.lastAcceptedRevision ?? "—"], - ["Commit Marker", ps.currentCommitMarker ? "present" : "none"], - ["Blocked Reason", ps.blockedReason || "—"], - ["IDB Snapshot Rev", ps.indexedDbSnapshotRevision ?? "—"], - ["Chat State Rev", ps.chatStateSnapshotRevision ?? "—"], - ["Shadow Snapshot", ps.hasShadowSnapshot ? "yes" : "no"], + ["Storage Tier", ps.acceptedStorageTier || ps.storageTier || "—"], + ["Revision", ps.revision ?? "—"], + ["Commit Marker", ps.commitMarker ? "present" : "none"], + ["Blocked Reason", ps.blockedReason || ps.reason || "—"], + ["Shadow Snapshot", ps.shadowSnapshotUsed ? "yes" : "no"], ]; const kvHtml = kvs.map(([k, v]) => `
      ${_escHtml(k)}${_escHtml(String(v))}
      `).join(""); From 3893807745d076f58067005344d39f92cbb12beb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 14:38:58 +0000 Subject: [PATCH 06/24] chore: bump manifest version [skip ci] --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index fe6a019..77cf55b 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Youzini", - "version": "4.8.4", + "version": "4.8.5", "homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology" } From c7dc4ebba95fa27fc221cd8b7b6e4ec506a93f1d Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sun, 12 Apr 2026 22:45:54 +0800 Subject: [PATCH 07/24] fix(ui): use proper memory node display data in task browser --- ui/panel.js | 140 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 103 insertions(+), 37 deletions(-) diff --git a/ui/panel.js b/ui/panel.js index 4dec443..84bbfe6 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -1538,13 +1538,47 @@ function _refreshTaskMemoryBrowser() { if (!el) return; const graph = _getGraph?.(); - if (!graph) { + const loadInfo = _getGraphPersistenceSnapshot(); + if (!graph || !_canRenderGraphData(loadInfo)) { el.innerHTML = '
      图谱未加载
      '; return; } - const nodes = graph.nodes || []; - const sorted = nodes.slice().sort((a, b) => (b.importance || 0) - (a.importance || 0)); + const currentQuery = String(document.getElementById("bme-task-memory-search")?.value || "") + .trim() + .toLowerCase(); + const currentFilter = document.getElementById("bme-task-memory-filter")?.value || "all"; + + let nodes = Array.isArray(graph.nodes) + ? graph.nodes.filter((node) => !node?.archived) + : []; + + if (currentFilter !== "all") { + nodes = nodes.filter((node) => _matchesMemoryFilter(node, currentFilter)); + } + + if (currentQuery) { + nodes = nodes.filter((node) => { + const name = getNodeDisplayName(node).toLowerCase(); + const snippet = _getNodeSnippet(node).toLowerCase(); + const fieldsText = JSON.stringify(node?.fields || {}).toLowerCase(); + return ( + name.includes(currentQuery) || + snippet.includes(currentQuery) || + fieldsText.includes(currentQuery) + ); + }); + } + + const sorted = nodes.slice().sort((a, b) => { + const importanceDiff = (b.importance || 5) - (a.importance || 5); + if (importanceDiff !== 0) return importanceDiff; + return (b.seqRange?.[1] ?? b.seq ?? 0) - (a.seqRange?.[1] ?? a.seq ?? 0); + }); + + if (!sorted.some((node) => node.id === currentSelectedMemoryNodeId)) { + currentSelectedMemoryNodeId = sorted[0]?.id || ""; + } const typeClass = (type) => { switch (type) { @@ -1557,27 +1591,25 @@ function _refreshTaskMemoryBrowser() { } }; - const typeLabel = (node) => { - if (node.scope === "characterPov") return "角色POV"; - if (node.scope === "userPov") return "用户POV"; - return node.type || "memory"; - }; - const listItems = sorted.map((node) => { const sel = node.id === currentSelectedMemoryNodeId ? "selected" : ""; - const preview = (node.description || "").slice(0, 80); + const preview = _getNodeSnippet(node); + const scopeBadge = buildScopeBadgeText(node.scope); + const metaText = _buildScopeMetaText(node); + const displayName = getNodeDisplayName(node); return `
      - ${_escHtml(typeLabel(node))} + ${_escHtml(_typeLabel(node.type))} IMP: ${typeof node.importance === "number" ? node.importance.toFixed(1) : "—"}
      -
      ${_escHtml(node.label || node.id)}
      +
      ${_escHtml(displayName)}
      ${_escHtml(preview)}
      - SEQ: ${node.lastSeq ?? "—"} - ${node.region ? `LOC: ${_escHtml(node.region)}` : ""} + ${_escHtml(scopeBadge)} + SEQ: ${_formatMemoryInt(node.seqRange?.[1] ?? node.seq, 0)}
      + ${metaText ? `
      ${_escHtml(metaText)}
      ` : ""}
      `; }).join(""); @@ -1587,17 +1619,17 @@ function _refreshTaskMemoryBrowser() {
      - +
      @@ -1611,6 +1643,17 @@ function _refreshTaskMemoryBrowser() { `; _bindTaskMemoryListClick(); + + const searchInput = document.getElementById("bme-task-memory-search"); + const filterSelect = document.getElementById("bme-task-memory-filter"); + if (searchInput) { + let timer = null; + searchInput.addEventListener("input", () => { + clearTimeout(timer); + timer = setTimeout(() => _refreshTaskMemoryBrowser(), 180); + }); + } + filterSelect?.addEventListener("change", () => _refreshTaskMemoryBrowser()); } function _bindTaskMemoryListClick() { @@ -1632,32 +1675,55 @@ function _renderMemoryDetailPanel(node, graph) { if (!node) return '
      选择左侧节点查看详情
      '; const edges = (graph?.edges || []).filter((e) => e.source === node.id || e.target === node.id); + const fields = node?.fields || {}; + const detailSummary = _getNodeSnippet(node); + const scopeMeta = _buildScopeMetaText(node); + const scopeBadge = buildScopeBadgeText(node.scope); + const displayName = getNodeDisplayName(node); const badges = [ - node.type ? `${_escHtml(node.type)}` : "", - node.scope ? `${_escHtml(node.scope)}` : "", + node.type ? `${_escHtml(_typeLabel(node.type))}` : "", + scopeBadge ? `${_escHtml(scopeBadge)}` : "", node.archived ? 'ARCHIVED' : "", ].filter(Boolean).join(""); - const fields = [ + const detailFields = [ ["ID", node.id], - ["Owner", node.owner || "—"], - ["Region", node.region || "—"], - ["Importance", typeof node.importance === "number" ? node.importance.toFixed(2) : "—"], - ["Last Seq", node.lastSeq ?? "—"], - ["Created", node.createdAt || "—"], - ["Updated", node.updatedAt || "—"], + ["名称", displayName], + ["范围", scopeMeta || scopeBadge || "—"], + ["重要度", _formatMemoryMetricNumber(node.importance, { fallback: 5, maxFrac: 2 })], + ["访问次数", _formatMemoryInt(node.accessCount, 0)], + ["最后序列", _formatMemoryInt(node.seqRange?.[1] ?? node.seq, 0)], + ["创建时间", node.createdAt || "—"], + ["更新时间", node.updatedAt || "—"], ]; + const extraFieldRows = Object.entries(fields) + .filter(([key]) => !["embedding", "name", "title", "summary"].includes(key)) + .slice(0, 6) + .map(([key, value]) => { + let text = "—"; + if (typeof value === "string") { + text = value; + } else if (value !== undefined && value !== null) { + try { + text = JSON.stringify(value); + } catch { + text = String(value); + } + } + return [key, text]; + }); + return ` -
      ${_escHtml(node.label || node.id)}
      +
      ${_escHtml(displayName)}
      ${badges}
      -
      ${_escHtml(node.description || "无描述")}
      +
      ${_escHtml(detailSummary || "无补充字段")}
      - ${fields.map(([k, v]) => `
      ${_escHtml(k)}${_escHtml(String(v))}
      `).join("")} + ${detailFields.concat(extraFieldRows).map(([k, v]) => `
      ${_escHtml(k)}${_escHtml(String(v))}
      `).join("")}
      ${edges.length} 条连接 - recall ${node.recallCount ?? 0} + 访问 ${_formatMemoryInt(node.accessCount, 0)}
      `; } From 68883c0d69f5b8fecb845cccb52b764b1aa82709 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 14:46:17 +0000 Subject: [PATCH 08/24] chore: bump manifest version [skip ci] --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 77cf55b..1b61040 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Youzini", - "version": "4.8.5", + "version": "4.8.6", "homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology" } From d4970a528ac70fde7c9af39b2796a64235591ea2 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sun, 12 Apr 2026 22:52:18 +0800 Subject: [PATCH 09/24] fix(ui): use correct edge ids for task memory connection counts --- ui/panel.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ui/panel.js b/ui/panel.js index 84bbfe6..b464e03 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -1674,7 +1674,12 @@ function _bindTaskMemoryListClick() { function _renderMemoryDetailPanel(node, graph) { if (!node) return '
      选择左侧节点查看详情
      '; - const edges = (graph?.edges || []).filter((e) => e.source === node.id || e.target === node.id); + const edges = (graph?.edges || []).filter( + (e) => + !e?.invalidAt && + !e?.expiredAt && + (e?.fromId === node.id || e?.toId === node.id), + ); const fields = node?.fields || {}; const detailSummary = _getNodeSnippet(node); const scopeMeta = _buildScopeMetaText(node); From 86c171f1979e771f0954c71cc830093ec72d3aa6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 14:52:45 +0000 Subject: [PATCH 10/24] chore: bump manifest version [skip ci] --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 1b61040..ddb0863 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Youzini", - "version": "4.8.6", + "version": "4.8.7", "homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology" } From 8c0965a3db6995d9b86f0429be34c47391701c99 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sun, 12 Apr 2026 22:55:45 +0800 Subject: [PATCH 11/24] fix(ui): make task memory panes scroll independently --- style.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/style.css b/style.css index 89f720d..a389d8b 100644 --- a/style.css +++ b/style.css @@ -775,6 +775,12 @@ display: block; } +#bme-task-memory.bme-task-section.active { + height: 100%; + min-height: 0; + overflow: hidden; +} + /* --- Task nav mobile pill selector (visible only on mobile) --- */ .bme-task-nav-mobile { display: none; @@ -1225,6 +1231,7 @@ height: 100%; min-height: 400px; gap: 0; + overflow: hidden; } .bme-memory-list-panel { @@ -1253,6 +1260,7 @@ .bme-memory-list-scroll { flex: 1; overflow-y: auto; + min-height: 0; } .bme-memory-node-item { @@ -1334,6 +1342,7 @@ overflow-y: auto; padding: 20px; min-width: 0; + min-height: 0; } .bme-memory-detail-empty { From eff5ae828a688b392105588d21663740cd335236 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 14:56:07 +0000 Subject: [PATCH 12/24] chore: bump manifest version [skip ci] --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index ddb0863..70fe38d 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Youzini", - "version": "4.8.7", + "version": "4.8.8", "homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology" } From 11fd31c752fff644a7287194ee8db027c0614cc7 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sun, 12 Apr 2026 23:15:23 +0800 Subject: [PATCH 13/24] feat(ui): enable inline editing in task memory detail --- ui/panel.js | 704 +++++++++++++++++++++++++++++----------------------- 1 file changed, 395 insertions(+), 309 deletions(-) diff --git a/ui/panel.js b/ui/panel.js index b464e03..79afe96 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -1533,6 +1533,24 @@ function _refreshTaskTimeline() { // ---------- Memory Browser (Master-Detail) ---------- +function _getMemoryNodeTypeClass(type) { + switch (type) { + case "pov_memory": + case "character": + return "type-character"; + case "event": + return "type-event"; + case "location": + return "type-location"; + case "rule": + return "type-rule"; + case "thread": + return "type-thread"; + default: + return "type-default"; + } +} + function _refreshTaskMemoryBrowser() { const el = document.getElementById("bme-task-memory"); if (!el) return; @@ -1580,17 +1598,6 @@ function _refreshTaskMemoryBrowser() { currentSelectedMemoryNodeId = sorted[0]?.id || ""; } - const typeClass = (type) => { - switch (type) { - case "pov_memory": case "character": return "type-character"; - case "event": return "type-event"; - case "location": return "type-location"; - case "rule": return "type-rule"; - case "thread": return "type-thread"; - default: return "type-default"; - } - }; - const listItems = sorted.map((node) => { const sel = node.id === currentSelectedMemoryNodeId ? "selected" : ""; const preview = _getNodeSnippet(node); @@ -1600,7 +1607,7 @@ function _refreshTaskMemoryBrowser() { return `
      - ${_escHtml(_typeLabel(node.type))} + ${_escHtml(_typeLabel(node.type))} IMP: ${typeof node.importance === "number" ? node.importance.toFixed(1) : "—"}
      ${_escHtml(displayName)}
      @@ -1613,8 +1620,6 @@ function _refreshTaskMemoryBrowser() {
      `; }).join(""); - const detailHtml = _renderMemoryDetailPanel(sorted.find((n) => n.id === currentSelectedMemoryNodeId) || null, graph); - el.innerHTML = `
      @@ -1636,12 +1641,11 @@ function _refreshTaskMemoryBrowser() { ${listItems || '
      无节点
      '}
      -
      - ${detailHtml} -
      +
      `; + _renderTaskMemoryDetailSelection(graph); _bindTaskMemoryListClick(); const searchInput = document.getElementById("bme-task-memory-search"); @@ -1665,14 +1669,25 @@ function _bindTaskMemoryListClick() { currentSelectedMemoryNodeId = item.dataset.nodeId || ""; list.querySelectorAll(".bme-memory-node-item").forEach((n) => n.classList.toggle("selected", n.dataset.nodeId === currentSelectedMemoryNodeId)); const graph = _getGraph?.(); - const node = (graph?.nodes || []).find((n) => n.id === currentSelectedMemoryNodeId) || null; - const detailEl = document.getElementById("bme-task-memory-detail"); - if (detailEl) detailEl.innerHTML = _renderMemoryDetailPanel(node, graph); + _renderTaskMemoryDetailSelection(graph); }); } -function _renderMemoryDetailPanel(node, graph) { - if (!node) return '
      选择左侧节点查看详情
      '; +function _renderTaskMemoryDetailSelection(graph = _getGraph?.()) { + const detailEl = document.getElementById("bme-task-memory-detail"); + if (!detailEl) return; + + const node = (graph?.nodes || []).find((candidate) => candidate.id === currentSelectedMemoryNodeId) || null; + if (!node) { + detailEl.innerHTML = '
      选择左侧节点查看详情
      '; + return; + } + + _renderTaskMemoryDetailPanel(detailEl, node, graph); +} + +function _renderTaskMemoryDetailPanel(detailEl, node, graph) { + if (!detailEl) return; const edges = (graph?.edges || []).filter( (e) => @@ -1680,57 +1695,79 @@ function _renderMemoryDetailPanel(node, graph) { !e?.expiredAt && (e?.fromId === node.id || e?.toId === node.id), ); - const fields = node?.fields || {}; const detailSummary = _getNodeSnippet(node); - const scopeMeta = _buildScopeMetaText(node); const scopeBadge = buildScopeBadgeText(node.scope); const displayName = getNodeDisplayName(node); + const writeBlocked = _isGraphWriteBlocked(); + const disabledAttr = writeBlocked ? " disabled" : ""; const badges = [ - node.type ? `${_escHtml(_typeLabel(node.type))}` : "", + node.type ? `${_escHtml(_typeLabel(node.type))}` : "", scopeBadge ? `${_escHtml(scopeBadge)}` : "", node.archived ? 'ARCHIVED' : "", ].filter(Boolean).join(""); - const detailFields = [ - ["ID", node.id], - ["名称", displayName], - ["范围", scopeMeta || scopeBadge || "—"], - ["重要度", _formatMemoryMetricNumber(node.importance, { fallback: 5, maxFrac: 2 })], - ["访问次数", _formatMemoryInt(node.accessCount, 0)], - ["最后序列", _formatMemoryInt(node.seqRange?.[1] ?? node.seq, 0)], - ["创建时间", node.createdAt || "—"], - ["更新时间", node.updatedAt || "—"], - ]; - - const extraFieldRows = Object.entries(fields) - .filter(([key]) => !["embedding", "name", "title", "summary"].includes(key)) - .slice(0, 6) - .map(([key, value]) => { - let text = "—"; - if (typeof value === "string") { - text = value; - } else if (value !== undefined && value !== null) { - try { - text = JSON.stringify(value); - } catch { - text = String(value); - } - } - return [key, text]; - }); - - return ` + detailEl.innerHTML = `
      ${_escHtml(displayName)}
      ${badges}
      ${_escHtml(detailSummary || "无补充字段")}
      -
      - ${detailFields.concat(extraFieldRows).map(([k, v]) => `
      ${_escHtml(k)}${_escHtml(String(v))}
      `).join("")} -
      ${edges.length} 条连接 访问 ${_formatMemoryInt(node.accessCount, 0)}
      +
      + + +
      +
      `; + + const editorBody = detailEl.querySelector("#bme-task-memory-editor-body"); + if (editorBody) { + editorBody.replaceChildren( + _buildNodeDetailEditorFragment(node, { idPrefix: "bme-task-detail" }), + ); + } + + detailEl + .querySelector('[data-task-memory-action="save"]') + ?.addEventListener("click", () => _saveTaskMemoryDetail()); + detailEl + .querySelector('[data-task-memory-action="delete"]') + ?.addEventListener("click", () => _deleteTaskMemoryDetail()); +} + +function _saveTaskMemoryDetail() { + const detailEl = document.getElementById("bme-task-memory-detail"); + const bodyEl = detailEl?.querySelector("#bme-task-memory-editor-body"); + const nodeId = currentSelectedMemoryNodeId; + if (!nodeId || !bodyEl) return; + + const collected = _collectNodeDetailEditorUpdates(bodyEl, { + idPrefix: "bme-task-detail", + }); + if (!collected.ok) { + toastr.error(collected.errorMessage || "保存失败", "ST-BME"); + return; + } + + _persistNodeDetailEdits(nodeId, collected.updates); +} + +function _deleteTaskMemoryDetail() { + const nodeId = currentSelectedMemoryNodeId; + if (!nodeId) return; + + _deleteGraphNodeById(nodeId, { + afterSuccess: () => { + currentSelectedMemoryNodeId = ""; + }, + }); } // ---------- Injection Preview ---------- @@ -3843,6 +3880,278 @@ function _appendNodeDetailTextareaField( container.appendChild(row); } +function _buildNodeDetailEditorFragment(raw, { idPrefix = "bme-detail" } = {}) { + const fields = raw.fields || {}; + const scope = normalizeMemoryScope(raw.scope); + const fragment = document.createDocumentFragment(); + const inputId = (suffix) => `${idPrefix}-${suffix}`; + + _appendNodeDetailReadOnly(fragment, "类型", _typeLabel(raw.type)); + _appendNodeDetailReadOnly( + fragment, + "作用域", + buildScopeBadgeText(raw.scope), + ); + _appendNodeDetailReadOnly(fragment, "ID", raw.id || "—"); + _appendNodeDetailReadOnly( + fragment, + "序列号", + raw.seqRange?.[1] ?? raw.seq ?? 0, + ); + + if (scope.layer === "pov") { + _appendNodeDetailReadOnly( + fragment, + "POV 归属", + `${scope.ownerType || "unknown"} / ${scope.ownerName || scope.ownerId || "—"}`, + ); + } + const regionLine = buildRegionLine(scope); + if (regionLine) { + _appendNodeDetailReadOnly(fragment, "地区", regionLine); + } + _appendNodeDetailTextInput( + fragment, + "主地区", + inputId("scope-region-primary"), + scope.regionPrimary || "", + ); + _appendNodeDetailTextInput( + fragment, + "地区路径 (用 / 分隔)", + inputId("scope-region-path"), + Array.isArray(scope.regionPath) ? scope.regionPath.join(" / ") : "", + ); + _appendNodeDetailTextInput( + fragment, + "次级地区 (用逗号或 / 分隔)", + inputId("scope-region-secondary"), + Array.isArray(scope.regionSecondary) + ? scope.regionSecondary.join(", ") + : "", + ); + if (Array.isArray(raw.seqRange)) { + _appendNodeDetailReadOnly( + fragment, + "序列范围", + `${raw.seqRange[0]} ~ ${raw.seqRange[1]}`, + ); + } + _appendNodeDetailTextareaField( + fragment, + "剧情时间", + "__storyTime", + "json", + JSON.stringify(raw.storyTime || {}, null, 2), + ); + _appendNodeDetailTextareaField( + fragment, + "剧情时间范围", + "__storyTimeSpan", + "json", + JSON.stringify(raw.storyTimeSpan || {}, null, 2), + ); + + _appendNodeDetailNumberInput( + fragment, + "重要度 (0–10)", + inputId("importance"), + raw.importance ?? 5, + { min: 0, max: 10, step: 0.1 }, + ); + _appendNodeDetailNumberInput( + fragment, + "访问次数", + inputId("accesscount"), + raw.accessCount ?? 0, + { min: 0, step: 1 }, + ); + + const clustersStr = Array.isArray(raw.clusters) + ? raw.clusters.join(", ") + : ""; + _appendNodeDetailTextInput( + fragment, + "聚类标签 (逗号分隔)", + inputId("clusters"), + clustersStr, + ); + + const section = document.createElement("div"); + section.className = "bme-node-detail-section"; + section.textContent = "记忆字段"; + fragment.appendChild(section); + + for (const [key, value] of Object.entries(fields)) { + const isJson = typeof value === "object" && value !== null; + const displayVal = isJson + ? JSON.stringify(value, null, 2) + : String(value ?? ""); + _appendNodeDetailTextareaField( + fragment, + key, + key, + isJson ? "json" : "string", + displayVal, + ); + } + + return fragment; +} + +function _collectNodeDetailEditorUpdates(bodyEl, { idPrefix = "bme-detail" } = {}) { + if (!bodyEl) { + return { ok: false, errorMessage: "未找到可编辑表单" }; + } + + const findInput = (suffix) => + bodyEl.querySelector(`#${idPrefix}-${suffix}`); + const updates = { fields: {} }; + const impEl = findInput("importance"); + if (impEl && impEl.value !== "") { + const imp = Number.parseFloat(impEl.value); + if (Number.isFinite(imp)) { + updates.importance = Math.max(0, Math.min(10, imp)); + } + } + const accessEl = findInput("accesscount"); + if (accessEl && accessEl.value !== "") { + const ac = Number.parseInt(accessEl.value, 10); + if (Number.isFinite(ac)) { + updates.accessCount = Math.max(0, ac); + } + } + const clustersEl = findInput("clusters"); + if (clustersEl) { + updates.clusters = clustersEl.value + .split(/[,,]/) + .map((s) => s.trim()) + .filter(Boolean); + } + const regionPrimaryEl = findInput("scope-region-primary"); + const regionPathEl = findInput("scope-region-path"); + const regionSecondaryEl = findInput("scope-region-secondary"); + if (regionPrimaryEl || regionPathEl || regionSecondaryEl) { + updates.scope = { + regionPrimary: String(regionPrimaryEl?.value || "").trim(), + regionPath: _parseNodeDetailScopeList(regionPathEl?.value, { + allowSlash: true, + }), + regionSecondary: _parseNodeDetailScopeList(regionSecondaryEl?.value, { + allowSlash: true, + }), + }; + } + + const fieldEls = bodyEl.querySelectorAll("[data-bme-field-key]"); + for (const el of fieldEls) { + const key = el.dataset.bmeFieldKey; + const type = el.dataset.bmeFieldType || "string"; + const rawVal = el.value; + if (key === "__storyTime" || key === "__storyTimeSpan") { + try { + updates[key === "__storyTime" ? "storyTime" : "storyTimeSpan"] = JSON.parse( + rawVal || "{}", + ); + } catch { + return { + ok: false, + errorMessage: `字段「${key === "__storyTime" ? "剧情时间" : "剧情时间范围"}」须为合法 JSON`, + }; + } + continue; + } + if (type === "json") { + try { + updates.fields[key] = JSON.parse(rawVal || "null"); + } catch { + return { + ok: false, + errorMessage: `字段「${key}」须为合法 JSON`, + }; + } + } else { + updates.fields[key] = rawVal; + } + } + + return { ok: true, updates }; +} + +function _persistNodeDetailEdits(nodeId, updates, { afterSuccess } = {}) { + if (!nodeId) return false; + if (_isGraphWriteBlocked()) { + toastr.error("当前图谱不可写入,请稍后再试", "ST-BME"); + return false; + } + + const result = _actionHandlers.saveGraphNode?.({ + nodeId, + updates, + }); + if (!result?.ok) { + toastr.error( + result?.error === "node-not-found" + ? "节点已不存在,请关闭后重试" + : "保存失败", + "ST-BME", + ); + return false; + } + if (result.persistBlocked) { + toastr.warning( + "内容已更新,但写回聊天元数据可能被拦截,请查看图谱状态", + "ST-BME", + ); + } else { + toastr.success("节点已保存", "ST-BME"); + } + + afterSuccess?.(); + refreshLiveState(); + return true; +} + +function _deleteGraphNodeById(nodeId, { afterSuccess } = {}) { + if (!nodeId) return false; + if (_isGraphWriteBlocked()) { + toastr.error("当前图谱不可写入,请稍后再试", "ST-BME"); + return false; + } + + const g = _getGraph?.(); + const node = g?.nodes?.find((n) => n.id === nodeId); + const label = node ? getNodeDisplayName(node) : nodeId; + if ( + !confirm( + `确定删除节点「${label}」?\n\n若该节点有层级子节点,将一并删除。此操作不可在本面板内撤销。`, + ) + ) { + return false; + } + + const result = _actionHandlers.deleteGraphNode?.({ nodeId }); + if (!result?.ok) { + toastr.error( + result?.error === "node-not-found" ? "节点已不存在" : "删除失败", + "ST-BME", + ); + return false; + } + if (result.persistBlocked) { + toastr.warning( + "节点已从图中移除,但写回可能被拦截,请查看图谱状态", + "ST-BME", + ); + } else { + toastr.success("节点已删除", "ST-BME"); + } + + afterSuccess?.(); + refreshLiveState(); + return true; +} + function _useMobileGraphNodeDetail() { return _isMobile() && currentTabId === "graph"; } @@ -3884,123 +4193,9 @@ function _showNodeDetail(node) { } const raw = node.raw || node; - const fields = raw.fields || {}; titleEl.textContent = getNodeDisplayName(raw); detailEl.dataset.editNodeId = raw.id || ""; - - const fragment = document.createDocumentFragment(); - - _appendNodeDetailReadOnly(fragment, "类型", _typeLabel(raw.type)); - _appendNodeDetailReadOnly( - fragment, - "作用域", - buildScopeBadgeText(raw.scope), - ); - _appendNodeDetailReadOnly(fragment, "ID", raw.id || "—"); - _appendNodeDetailReadOnly( - fragment, - "序列号", - raw.seqRange?.[1] ?? raw.seq ?? 0, - ); - - const scope = normalizeMemoryScope(raw.scope); - if (scope.layer === "pov") { - _appendNodeDetailReadOnly( - fragment, - "POV 归属", - `${scope.ownerType || "unknown"} / ${scope.ownerName || scope.ownerId || "—"}`, - ); - } - const regionLine = buildRegionLine(scope); - if (regionLine) { - _appendNodeDetailReadOnly(fragment, "地区", regionLine); - } - _appendNodeDetailTextInput( - fragment, - "主地区", - "bme-detail-scope-region-primary", - scope.regionPrimary || "", - ); - _appendNodeDetailTextInput( - fragment, - "地区路径 (用 / 分隔)", - "bme-detail-scope-region-path", - Array.isArray(scope.regionPath) ? scope.regionPath.join(" / ") : "", - ); - _appendNodeDetailTextInput( - fragment, - "次级地区 (用逗号或 / 分隔)", - "bme-detail-scope-region-secondary", - Array.isArray(scope.regionSecondary) - ? scope.regionSecondary.join(", ") - : "", - ); - if (Array.isArray(raw.seqRange)) { - _appendNodeDetailReadOnly( - fragment, - "序列范围", - `${raw.seqRange[0]} ~ ${raw.seqRange[1]}`, - ); - } - _appendNodeDetailTextareaField( - fragment, - "剧情时间", - "__storyTime", - "json", - JSON.stringify(raw.storyTime || {}, null, 2), - ); - _appendNodeDetailTextareaField( - fragment, - "剧情时间范围", - "__storyTimeSpan", - "json", - JSON.stringify(raw.storyTimeSpan || {}, null, 2), - ); - - _appendNodeDetailNumberInput( - fragment, - "重要度 (0–10)", - "bme-detail-importance", - raw.importance ?? 5, - { min: 0, max: 10, step: 0.1 }, - ); - _appendNodeDetailNumberInput( - fragment, - "访问次数", - "bme-detail-accesscount", - raw.accessCount ?? 0, - { min: 0, step: 1 }, - ); - - const clustersStr = Array.isArray(raw.clusters) - ? raw.clusters.join(", ") - : ""; - _appendNodeDetailTextInput( - fragment, - "聚类标签 (逗号分隔)", - "bme-detail-clusters", - clustersStr, - ); - - const section = document.createElement("div"); - section.className = "bme-node-detail-section"; - section.textContent = "记忆字段"; - fragment.appendChild(section); - - for (const [key, value] of Object.entries(fields)) { - const isJson = typeof value === "object" && value !== null; - const displayVal = isJson - ? JSON.stringify(value, null, 2) - : String(value ?? ""); - _appendNodeDetailTextareaField( - fragment, - key, - key, - isJson ? "json" : "string", - displayVal, - ); - } - bodyEl.replaceChildren(fragment); + bodyEl.replaceChildren(_buildNodeDetailEditorFragment(raw)); if (mobile) { scrimEl?.removeAttribute("hidden"); @@ -4014,110 +4209,27 @@ function _saveNodeDetail() { const bodyEl = els?.bodyEl; const nodeId = detailEl?.dataset?.editNodeId; if (!nodeId || !bodyEl) return; - if (_isGraphWriteBlocked()) { - toastr.error("当前图谱不可写入,请稍后再试", "ST-BME"); + const collected = _collectNodeDetailEditorUpdates(bodyEl); + if (!collected.ok) { + toastr.error(collected.errorMessage || "保存失败", "ST-BME"); return; } - const updates = { fields: {} }; - const impEl = document.getElementById("bme-detail-importance"); - if (impEl && impEl.value !== "") { - const imp = Number.parseFloat(impEl.value); - if (Number.isFinite(imp)) { - updates.importance = Math.max(0, Math.min(10, imp)); - } - } - const accessEl = document.getElementById("bme-detail-accesscount"); - if (accessEl && accessEl.value !== "") { - const ac = Number.parseInt(accessEl.value, 10); - if (Number.isFinite(ac)) { - updates.accessCount = Math.max(0, ac); - } - } - const clustersEl = document.getElementById("bme-detail-clusters"); - if (clustersEl) { - updates.clusters = clustersEl.value - .split(/[,,]/) - .map((s) => s.trim()) - .filter(Boolean); - } - const regionPrimaryEl = document.getElementById("bme-detail-scope-region-primary"); - const regionPathEl = document.getElementById("bme-detail-scope-region-path"); - const regionSecondaryEl = document.getElementById("bme-detail-scope-region-secondary"); - if (regionPrimaryEl || regionPathEl || regionSecondaryEl) { - updates.scope = { - regionPrimary: String(regionPrimaryEl?.value || "").trim(), - regionPath: _parseNodeDetailScopeList(regionPathEl?.value, { - allowSlash: true, - }), - regionSecondary: _parseNodeDetailScopeList(regionSecondaryEl?.value, { - allowSlash: true, - }), - }; - } - - const fieldEls = bodyEl.querySelectorAll("[data-bme-field-key]"); - for (const el of fieldEls) { - const key = el.dataset.bmeFieldKey; - const type = el.dataset.bmeFieldType || "string"; - const rawVal = el.value; - if (key === "__storyTime" || key === "__storyTimeSpan") { - try { - updates[key === "__storyTime" ? "storyTime" : "storyTimeSpan"] = JSON.parse( - rawVal || "{}", - ); - } catch { - toastr.error(`字段「${key === "__storyTime" ? "剧情时间" : "剧情时间范围"}」须为合法 JSON`, "ST-BME"); - return; + _persistNodeDetailEdits(nodeId, collected.updates, { + afterSuccess: () => { + const r = _getActiveGraphRenderer(); + const sel = r?.selectedNode; + if (sel?.id === nodeId && sel.raw) { + _showNodeDetail(sel); + } else { + const g = _getGraph?.(); + const rawN = g?.nodes?.find((n) => n.id === nodeId); + if (rawN) { + _showNodeDetail({ raw: rawN, id: rawN.id }); + } } - continue; - } - if (type === "json") { - try { - updates.fields[key] = JSON.parse(rawVal || "null"); - } catch { - toastr.error(`字段「${key}」须为合法 JSON`, "ST-BME"); - return; - } - } else { - updates.fields[key] = rawVal; - } - } - - const result = _actionHandlers.saveGraphNode?.({ - nodeId, - updates, + }, }); - if (!result?.ok) { - toastr.error( - result?.error === "node-not-found" - ? "节点已不存在,请关闭后重试" - : "保存失败", - "ST-BME", - ); - return; - } - if (result.persistBlocked) { - toastr.warning( - "内容已更新,但写回聊天元数据可能被拦截,请查看图谱状态", - "ST-BME", - ); - } else { - toastr.success("节点已保存", "ST-BME"); - } - - const r = _getActiveGraphRenderer(); - const sel = r?.selectedNode; - if (sel?.id === nodeId && sel.raw) { - _showNodeDetail(sel); - } else { - const g = _getGraph?.(); - const rawN = g?.nodes?.find((n) => n.id === nodeId); - if (rawN) { - _showNodeDetail({ raw: rawN, id: rawN.id }); - } - } - refreshLiveState(); } function _bindNodeDetailPanel() { @@ -4148,44 +4260,18 @@ function _deleteNodeDetail() { const detailEl = els?.detailEl; const nodeId = detailEl?.dataset?.editNodeId; if (!nodeId) return; - if (_isGraphWriteBlocked()) { - toastr.error("当前图谱不可写入,请稍后再试", "ST-BME"); - return; - } - const g = _getGraph?.(); - const node = g?.nodes?.find((n) => n.id === nodeId); - const label = node ? getNodeDisplayName(node) : nodeId; - if ( - !confirm( - `确定删除节点「${label}」?\n\n若该节点有层级子节点,将一并删除。此操作不可在本面板内撤销。`, - ) - ) { - return; - } - const result = _actionHandlers.deleteGraphNode?.({ nodeId }); - if (!result?.ok) { - toastr.error( - result?.error === "node-not-found" ? "节点已不存在" : "删除失败", - "ST-BME", - ); - return; - } - if (result.persistBlocked) { - toastr.warning( - "节点已从图中移除,但写回可能被拦截,请查看图谱状态", - "ST-BME", - ); - } else { - toastr.success("节点已删除", "ST-BME"); - } - _closeNodeDetailUi(); - const dDesk = document.getElementById("bme-node-detail"); - const dMob = document.getElementById("bme-mobile-node-detail"); - if (dDesk) delete dDesk.dataset.editNodeId; - if (dMob) delete dMob.dataset.editNodeId; - graphRenderer?.highlightNode?.("__cleared__"); - mobileGraphRenderer?.highlightNode?.("__cleared__"); - refreshLiveState(); + + _deleteGraphNodeById(nodeId, { + afterSuccess: () => { + _closeNodeDetailUi(); + const dDesk = document.getElementById("bme-node-detail"); + const dMob = document.getElementById("bme-mobile-node-detail"); + if (dDesk) delete dDesk.dataset.editNodeId; + if (dMob) delete dMob.dataset.editNodeId; + graphRenderer?.highlightNode?.("__cleared__"); + mobileGraphRenderer?.highlightNode?.("__cleared__"); + }, + }); } function _bindClose() { From 883072639049ca3484eef773aec9efe1b2787348 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:15:44 +0000 Subject: [PATCH 14/24] chore: bump manifest version [skip ci] --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 70fe38d..501864a 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Youzini", - "version": "4.8.8", + "version": "4.8.9", "homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology" } From 1390cd2d7e6da13c215ea3ed21b917ac09797b97 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sun, 12 Apr 2026 23:20:28 +0800 Subject: [PATCH 15/24] fix(ui): move task memory save/delete buttons to title header row --- style.css | 18 +++++++++++++++++- ui/panel.js | 22 +++++++++++----------- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/style.css b/style.css index a389d8b..758e3a9 100644 --- a/style.css +++ b/style.css @@ -1354,11 +1354,27 @@ font-size: 12px; } +.bme-memory-detail__header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 8px; + margin-bottom: 8px; +} + +.bme-memory-detail__header-actions { + display: flex; + align-items: center; + gap: 2px; + flex-shrink: 0; +} + .bme-memory-detail__title { font-size: 18px; font-weight: 700; color: var(--bme-on-surface); - margin-bottom: 8px; + min-width: 0; + word-break: break-word; } .bme-memory-detail__badges { diff --git a/ui/panel.js b/ui/panel.js index 79afe96..a1a092d 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -1707,23 +1707,23 @@ function _renderTaskMemoryDetailPanel(detailEl, node, graph) { ].filter(Boolean).join(""); detailEl.innerHTML = ` -
      ${_escHtml(displayName)}
      +
      +
      ${_escHtml(displayName)}
      +
      + + +
      +
      ${badges}
      ${_escHtml(detailSummary || "无补充字段")}
      ${edges.length} 条连接 访问 ${_formatMemoryInt(node.accessCount, 0)}
      -
      - - -
      `; From 825fd39058307301f7b1e33f19b35f2b16048c42 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:20:51 +0000 Subject: [PATCH 16/24] chore: bump manifest version [skip ci] --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 501864a..c48af20 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Youzini", - "version": "4.8.9", + "version": "4.9.0", "homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology" } From e2706cd91d6b981c9407dd44a75b9ad269a773fa Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sun, 12 Apr 2026 23:33:07 +0800 Subject: [PATCH 17/24] fix(ui): use readable story time editors in node detail --- ui/panel.js | 312 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 283 insertions(+), 29 deletions(-) diff --git a/ui/panel.js b/ui/panel.js index a1a092d..11eb920 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -14,9 +14,8 @@ import { import { listKnowledgeOwners } from "../graph/knowledge-state.js"; import { getHostUserAliasHints } from "../runtime/user-alias-utils.js"; import { - describeNodeStoryTime, - describeStoryTime, - describeStoryTimeSpan, + normalizeStoryTime, + normalizeStoryTimeSpan, } from "../graph/story-timeline.js"; import { compareSummaryEntriesForDisplay, @@ -2442,7 +2441,7 @@ function _formatSummaryEntryCard(entry = {}) { const extractionRange = Array.isArray(entry?.extractionRange) ? entry.extractionRange : ["?", "?"]; - const spanLabel = describeStoryTimeSpan(entry?.storyTimeSpan); + const spanLabel = _describeStoryTimeSpanDisplay(entry?.storyTimeSpan); const meta = [ `L${Math.max(0, Number(entry?.level || 0))}`, String(entry?.kind || "small"), @@ -3795,6 +3794,102 @@ function _bindGraphControls() { // ==================== 节点详情 ==================== +const STORY_TIME_TENSE_OPTIONS = Object.freeze([ + { value: "past", label: "过去" }, + { value: "ongoing", label: "进行中" }, + { value: "future", label: "未来" }, + { value: "flashback", label: "闪回" }, + { value: "hypothetical", label: "假设" }, + { value: "unknown", label: "未知" }, +]); + +const STORY_TIME_RELATION_OPTIONS = Object.freeze([ + { value: "same", label: "同一时点" }, + { value: "after", label: "在锚点之后" }, + { value: "before", label: "在锚点之前" }, + { value: "parallel", label: "与锚点并行" }, + { value: "unknown", label: "未知" }, +]); + +const STORY_TIME_CONFIDENCE_OPTIONS = Object.freeze([ + { value: "high", label: "高" }, + { value: "medium", label: "中" }, + { value: "low", label: "低" }, +]); + +const STORY_TIME_SOURCE_OPTIONS = Object.freeze([ + { value: "extract", label: "提取" }, + { value: "derived", label: "推导" }, + { value: "manual", label: "手动" }, +]); + +const STORY_TIME_MIXED_OPTIONS = Object.freeze([ + { value: "false", label: "否" }, + { value: "true", label: "是" }, +]); + +function _resolveNodeDetailOptionLabel(options = [], value, fallback = "") { + return ( + options.find((option) => option.value === String(value ?? ""))?.label || + fallback || + String(value ?? "") + ); +} + +function _describeStoryTimeDisplay(storyTime = {}) { + const normalized = normalizeStoryTime(storyTime); + if (!normalized.label) return ""; + + const parts = [normalized.label]; + if (normalized.tense && normalized.tense !== "unknown") { + parts.push( + _resolveNodeDetailOptionLabel(STORY_TIME_TENSE_OPTIONS, normalized.tense), + ); + } + if ( + normalized.relation && + normalized.relation !== "unknown" && + normalized.relation !== "same" + ) { + const relationLabel = _resolveNodeDetailOptionLabel( + STORY_TIME_RELATION_OPTIONS, + normalized.relation, + ); + parts.push( + normalized.anchorLabel + ? `${relationLabel} · ${normalized.anchorLabel}` + : relationLabel, + ); + } else if (normalized.anchorLabel) { + parts.push(`锚点 · ${normalized.anchorLabel}`); + } + + return parts.join(" · "); +} + +function _describeStoryTimeSpanDisplay(storyTimeSpan = {}) { + const normalized = normalizeStoryTimeSpan(storyTimeSpan); + const label = + normalized.startLabel && + normalized.endLabel && + normalized.startLabel !== normalized.endLabel + ? `${normalized.startLabel} → ${normalized.endLabel}` + : normalized.startLabel || normalized.endLabel || ""; + + if (!label) { + return normalized.mixed ? "混合时间" : ""; + } + return normalized.mixed ? `${label} · 混合` : label; +} + +function _describeNodeStoryTimeDisplay(node = {}) { + return ( + _describeStoryTimeDisplay(node.storyTime) || + _describeStoryTimeSpanDisplay(node.storyTimeSpan) || + "" + ); +} + function _appendNodeDetailReadOnly(container, labelText, valueText) { const row = document.createElement("div"); row.className = "bme-node-detail-field"; @@ -3847,6 +3942,32 @@ function _appendNodeDetailTextInput(container, labelText, inputId, value) { container.appendChild(row); } +function _appendNodeDetailSelectInput( + container, + labelText, + inputId, + value, + options = [], +) { + const row = document.createElement("div"); + row.className = "bme-node-detail-field"; + const label = document.createElement("label"); + label.setAttribute("for", inputId); + label.textContent = labelText; + const select = document.createElement("select"); + select.id = inputId; + select.className = "bme-node-detail-input"; + options.forEach((option) => { + const optEl = document.createElement("option"); + optEl.value = option.value; + optEl.textContent = option.label; + select.appendChild(optEl); + }); + select.value = String(value ?? ""); + row.append(label, select); + container.appendChild(row); +} + function _parseNodeDetailScopeList(rawValue, { allowSlash = true } = {}) { const normalized = String(rawValue ?? "") .replace(/[>>→]+/g, "/") @@ -3883,6 +4004,8 @@ function _appendNodeDetailTextareaField( function _buildNodeDetailEditorFragment(raw, { idPrefix = "bme-detail" } = {}) { const fields = raw.fields || {}; const scope = normalizeMemoryScope(raw.scope); + const storyTime = normalizeStoryTime(raw.storyTime); + const storyTimeSpan = normalizeStoryTimeSpan(raw.storyTimeSpan); const fragment = document.createDocumentFragment(); const inputId = (suffix) => `${idPrefix}-${suffix}`; @@ -3937,19 +4060,108 @@ function _buildNodeDetailEditorFragment(raw, { idPrefix = "bme-detail" } = {}) { `${raw.seqRange[0]} ~ ${raw.seqRange[1]}`, ); } - _appendNodeDetailTextareaField( + const storyTimeSection = document.createElement("div"); + storyTimeSection.className = "bme-node-detail-section"; + storyTimeSection.textContent = "剧情时间"; + fragment.appendChild(storyTimeSection); + _appendNodeDetailReadOnly( fragment, - "剧情时间", - "__storyTime", - "json", - JSON.stringify(raw.storyTime || {}, null, 2), + "当前摘要", + _describeStoryTimeDisplay(storyTime) || "—", ); - _appendNodeDetailTextareaField( + _appendNodeDetailTextInput( fragment, - "剧情时间范围", - "__storyTimeSpan", - "json", - JSON.stringify(raw.storyTimeSpan || {}, null, 2), + "时间标签", + inputId("story-time-label"), + storyTime.label, + ); + _appendNodeDetailSelectInput( + fragment, + "时态", + inputId("story-time-tense"), + storyTime.tense, + STORY_TIME_TENSE_OPTIONS, + ); + _appendNodeDetailSelectInput( + fragment, + "相对关系", + inputId("story-time-relation"), + storyTime.relation, + STORY_TIME_RELATION_OPTIONS, + ); + _appendNodeDetailTextInput( + fragment, + "锚点标签", + inputId("story-time-anchor-label"), + storyTime.anchorLabel, + ); + _appendNodeDetailSelectInput( + fragment, + "置信度", + inputId("story-time-confidence"), + storyTime.confidence, + STORY_TIME_CONFIDENCE_OPTIONS, + ); + _appendNodeDetailSelectInput( + fragment, + "来源", + inputId("story-time-source"), + storyTime.source, + STORY_TIME_SOURCE_OPTIONS, + ); + _appendNodeDetailTextInput( + fragment, + "段 ID", + inputId("story-time-segment-id"), + storyTime.segmentId, + ); + + const storyTimeSpanSection = document.createElement("div"); + storyTimeSpanSection.className = "bme-node-detail-section"; + storyTimeSpanSection.textContent = "剧情时间范围"; + fragment.appendChild(storyTimeSpanSection); + _appendNodeDetailReadOnly( + fragment, + "当前范围", + _describeStoryTimeSpanDisplay(storyTimeSpan) || "—", + ); + _appendNodeDetailTextInput( + fragment, + "起点标签", + inputId("story-time-span-start-label"), + storyTimeSpan.startLabel, + ); + _appendNodeDetailTextInput( + fragment, + "终点标签", + inputId("story-time-span-end-label"), + storyTimeSpan.endLabel, + ); + _appendNodeDetailSelectInput( + fragment, + "混合时间", + inputId("story-time-span-mixed"), + storyTimeSpan.mixed ? "true" : "false", + STORY_TIME_MIXED_OPTIONS, + ); + _appendNodeDetailSelectInput( + fragment, + "来源", + inputId("story-time-span-source"), + storyTimeSpan.source, + STORY_TIME_SOURCE_OPTIONS, + ); + _appendNodeDetailTextInput( + fragment, + "起点段 ID", + inputId("story-time-span-start-segment-id"), + storyTimeSpan.startSegmentId, + ); + _appendNodeDetailTextInput( + fragment, + "终点段 ID", + inputId("story-time-span-end-segment-id"), + storyTimeSpan.endSegmentId, ); _appendNodeDetailNumberInput( @@ -4043,24 +4255,66 @@ function _collectNodeDetailEditorUpdates(bodyEl, { idPrefix = "bme-detail" } = { }; } + const storyTimeLabelEl = findInput("story-time-label"); + const storyTimeTenseEl = findInput("story-time-tense"); + const storyTimeRelationEl = findInput("story-time-relation"); + const storyTimeAnchorLabelEl = findInput("story-time-anchor-label"); + const storyTimeConfidenceEl = findInput("story-time-confidence"); + const storyTimeSourceEl = findInput("story-time-source"); + const storyTimeSegmentIdEl = findInput("story-time-segment-id"); + if ( + storyTimeLabelEl || + storyTimeTenseEl || + storyTimeRelationEl || + storyTimeAnchorLabelEl || + storyTimeConfidenceEl || + storyTimeSourceEl || + storyTimeSegmentIdEl + ) { + updates.storyTime = normalizeStoryTime({ + segmentId: String(storyTimeSegmentIdEl?.value || "").trim(), + label: String(storyTimeLabelEl?.value || "").trim(), + tense: String(storyTimeTenseEl?.value || ""), + relation: String(storyTimeRelationEl?.value || ""), + anchorLabel: String(storyTimeAnchorLabelEl?.value || "").trim(), + confidence: String(storyTimeConfidenceEl?.value || ""), + source: String(storyTimeSourceEl?.value || ""), + }); + } + + const storyTimeSpanStartLabelEl = findInput("story-time-span-start-label"); + const storyTimeSpanEndLabelEl = findInput("story-time-span-end-label"); + const storyTimeSpanMixedEl = findInput("story-time-span-mixed"); + const storyTimeSpanSourceEl = findInput("story-time-span-source"); + const storyTimeSpanStartSegmentIdEl = findInput( + "story-time-span-start-segment-id", + ); + const storyTimeSpanEndSegmentIdEl = findInput( + "story-time-span-end-segment-id", + ); + if ( + storyTimeSpanStartLabelEl || + storyTimeSpanEndLabelEl || + storyTimeSpanMixedEl || + storyTimeSpanSourceEl || + storyTimeSpanStartSegmentIdEl || + storyTimeSpanEndSegmentIdEl + ) { + updates.storyTimeSpan = normalizeStoryTimeSpan({ + startSegmentId: String(storyTimeSpanStartSegmentIdEl?.value || "").trim(), + endSegmentId: String(storyTimeSpanEndSegmentIdEl?.value || "").trim(), + startLabel: String(storyTimeSpanStartLabelEl?.value || "").trim(), + endLabel: String(storyTimeSpanEndLabelEl?.value || "").trim(), + mixed: String(storyTimeSpanMixedEl?.value || "false") === "true", + source: String(storyTimeSpanSourceEl?.value || ""), + }); + } + const fieldEls = bodyEl.querySelectorAll("[data-bme-field-key]"); for (const el of fieldEls) { const key = el.dataset.bmeFieldKey; const type = el.dataset.bmeFieldType || "string"; const rawVal = el.value; - if (key === "__storyTime" || key === "__storyTimeSpan") { - try { - updates[key === "__storyTime" ? "storyTime" : "storyTimeSpan"] = JSON.parse( - rawVal || "{}", - ); - } catch { - return { - ok: false, - errorMessage: `字段「${key === "__storyTime" ? "剧情时间" : "剧情时间范围"}」须为合法 JSON`, - }; - } - continue; - } if (type === "json") { try { updates.fields[key] = JSON.parse(rawVal || "null"); @@ -11374,7 +11628,7 @@ function _buildScopeMetaText(node) { } const regionLine = buildRegionLine(scope); if (regionLine) parts.push(regionLine); - const storyTime = describeNodeStoryTime(node); + const storyTime = _describeNodeStoryTimeDisplay(node); if (storyTime) parts.push(`剧情时间: ${storyTime}`); return parts.join(" · "); } @@ -11416,7 +11670,7 @@ function _typeLabel(type) { function _getNodeSnippet(node) { const fields = node.fields || {}; - const storyTime = describeNodeStoryTime(node); + const storyTime = _describeNodeStoryTimeDisplay(node); if (fields.summary) return fields.summary; if (fields.state) return fields.state; if (fields.constraint) return fields.constraint; From 387dc3f48fa09e50e8ac5f356742e41700a9ef7d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:33:47 +0000 Subject: [PATCH 18/24] chore: bump manifest version [skip ci] --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index c48af20..e7dd80b 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Youzini", - "version": "4.9.0", + "version": "4.9.1", "homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology" } From e91dd0ed9784d185fd23657bfce1b75eb2013a85 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sun, 12 Apr 2026 23:38:09 +0800 Subject: [PATCH 19/24] ui: collapse advanced story time fields and story time span section --- style.css | 36 ++++++++++++++++++++++++++++++++++++ ui/panel.js | 42 ++++++++++++++++++++++++++---------------- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/style.css b/style.css index 758e3a9..7fa38e0 100644 --- a/style.css +++ b/style.css @@ -4819,6 +4819,42 @@ line-height: 1.4; } +.bme-node-detail-collapse { + margin: 6px 0 8px; +} + +.bme-node-detail-collapse > summary { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--bme-on-surface-dim); + cursor: pointer; + user-select: none; + list-style: none; + display: flex; + align-items: center; + gap: 4px; +} + +.bme-node-detail-collapse > summary::-webkit-details-marker { + display: none; +} + +.bme-node-detail-collapse > summary::before { + content: "▶"; + font-size: 8px; + transition: transform 0.15s; +} + +.bme-node-detail-collapse[open] > summary::before { + transform: rotate(90deg); +} + +.bme-node-detail-collapse > .bme-node-detail-field:first-of-type { + margin-top: 6px; +} + /* --- Scrollbar --- */ .bme-tab-content::-webkit-scrollbar, .bme-config-sidebar::-webkit-scrollbar, diff --git a/ui/panel.js b/ui/panel.js index 11eb920..d4b0b43 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -4082,87 +4082,97 @@ function _buildNodeDetailEditorFragment(raw, { idPrefix = "bme-detail" } = {}) { storyTime.tense, STORY_TIME_TENSE_OPTIONS, ); + + const storyTimeAdvanced = document.createElement("details"); + storyTimeAdvanced.className = "bme-node-detail-collapse"; + const storyTimeAdvancedSummary = document.createElement("summary"); + storyTimeAdvancedSummary.textContent = "高级"; + storyTimeAdvanced.appendChild(storyTimeAdvancedSummary); _appendNodeDetailSelectInput( - fragment, + storyTimeAdvanced, "相对关系", inputId("story-time-relation"), storyTime.relation, STORY_TIME_RELATION_OPTIONS, ); _appendNodeDetailTextInput( - fragment, + storyTimeAdvanced, "锚点标签", inputId("story-time-anchor-label"), storyTime.anchorLabel, ); _appendNodeDetailSelectInput( - fragment, + storyTimeAdvanced, "置信度", inputId("story-time-confidence"), storyTime.confidence, STORY_TIME_CONFIDENCE_OPTIONS, ); _appendNodeDetailSelectInput( - fragment, + storyTimeAdvanced, "来源", inputId("story-time-source"), storyTime.source, STORY_TIME_SOURCE_OPTIONS, ); _appendNodeDetailTextInput( - fragment, + storyTimeAdvanced, "段 ID", inputId("story-time-segment-id"), storyTime.segmentId, ); + fragment.appendChild(storyTimeAdvanced); - const storyTimeSpanSection = document.createElement("div"); - storyTimeSpanSection.className = "bme-node-detail-section"; - storyTimeSpanSection.textContent = "剧情时间范围"; - fragment.appendChild(storyTimeSpanSection); + const storyTimeSpanCollapse = document.createElement("details"); + storyTimeSpanCollapse.className = "bme-node-detail-collapse"; + const storyTimeSpanSummaryEl = document.createElement("summary"); + storyTimeSpanSummaryEl.className = "bme-node-detail-section"; + storyTimeSpanSummaryEl.textContent = "剧情时间范围"; + storyTimeSpanCollapse.appendChild(storyTimeSpanSummaryEl); _appendNodeDetailReadOnly( - fragment, + storyTimeSpanCollapse, "当前范围", _describeStoryTimeSpanDisplay(storyTimeSpan) || "—", ); _appendNodeDetailTextInput( - fragment, + storyTimeSpanCollapse, "起点标签", inputId("story-time-span-start-label"), storyTimeSpan.startLabel, ); _appendNodeDetailTextInput( - fragment, + storyTimeSpanCollapse, "终点标签", inputId("story-time-span-end-label"), storyTimeSpan.endLabel, ); _appendNodeDetailSelectInput( - fragment, + storyTimeSpanCollapse, "混合时间", inputId("story-time-span-mixed"), storyTimeSpan.mixed ? "true" : "false", STORY_TIME_MIXED_OPTIONS, ); _appendNodeDetailSelectInput( - fragment, + storyTimeSpanCollapse, "来源", inputId("story-time-span-source"), storyTimeSpan.source, STORY_TIME_SOURCE_OPTIONS, ); _appendNodeDetailTextInput( - fragment, + storyTimeSpanCollapse, "起点段 ID", inputId("story-time-span-start-segment-id"), storyTimeSpan.startSegmentId, ); _appendNodeDetailTextInput( - fragment, + storyTimeSpanCollapse, "终点段 ID", inputId("story-time-span-end-segment-id"), storyTimeSpan.endSegmentId, ); + fragment.appendChild(storyTimeSpanCollapse); _appendNodeDetailNumberInput( fragment, From 1cc48ade9e20837468b08a0c9869d9ad6978cb1b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:38:33 +0000 Subject: [PATCH 20/24] chore: bump manifest version [skip ci] --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index e7dd80b..bddb0a8 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Youzini", - "version": "4.9.1", + "version": "4.9.2", "homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology" } From c2226026dce00ce621c22b315450193abe7d4c22 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sun, 12 Apr 2026 23:43:01 +0800 Subject: [PATCH 21/24] ui: use rich structured visualization for task injection preview --- ui/panel.js | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/ui/panel.js b/ui/panel.js index d4b0b43..78b5918 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -1788,29 +1788,22 @@ function _refreshTaskInjectionPreview() { const budgetTokens = recallSnap.budgetTokens || totalTokens || 1; const pct = totalTokens > 0 ? Math.min(100, Math.round((totalTokens / budgetTokens) * 100)) : 0; - const tokenBarHtml = totalTokens > 0 ? ` -
      + const wrapper = document.createDocumentFragment(); + + if (totalTokens > 0) { + const bar = document.createElement("div"); + bar.className = "bme-injection-token-bar"; + bar.innerHTML = ` ${totalTokens} / ${budgetTokens} tok
      - ${pct}% -
      ` : ""; + ${pct}%`; + wrapper.appendChild(bar); + } - const previewText = injectionText.length > 3000 ? injectionText.slice(0, 3000) + "…" : injectionText; - - el.innerHTML = ` - ${tokenBarHtml} -
      -
      -
      - Recall Injection - ${totalTokens > 0 ? `${totalTokens} tok` : ""} -
      -
      ${_escHtml(previewText)}
      -
      -
      - `; + wrapper.appendChild(_buildInjectionPreviewNode(injectionText)); + el.replaceChildren(wrapper); } // ---------- Message Trace ---------- From 875a7bfd5ebce4a5ee7008df24362235f7a0d31c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:43:18 +0000 Subject: [PATCH 22/24] chore: bump manifest version [skip ci] --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index bddb0a8..78a75bc 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Youzini", - "version": "4.9.2", + "version": "4.9.3", "homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology" } From c3800a1425a7d3f4d7b342ff61d7887efa190312 Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Sun, 12 Apr 2026 23:46:20 +0800 Subject: [PATCH 23/24] ui: localize persistence panel labels and add field guide --- style.css | 39 +++++++++++++++++++++++++++++++++ ui/panel.js | 62 ++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 89 insertions(+), 12 deletions(-) diff --git a/style.css b/style.css index 7fa38e0..2882889 100644 --- a/style.css +++ b/style.css @@ -1545,6 +1545,45 @@ .bme-persist-kv__row span { color: var(--bme-on-surface-dim); } .bme-persist-kv__row strong { color: var(--bme-on-surface); font-weight: 600; } +.bme-persist-guide { + margin-top: 4px; + padding: 14px 16px; + background: var(--bme-surface, #131316); + border: 1px solid var(--bme-border); + border-radius: 8px; +} + +.bme-persist-guide__title { + font-size: 12px; + font-weight: 700; + color: var(--bme-on-surface); + margin-bottom: 10px; +} + +.bme-persist-guide__item { + display: flex; + gap: 8px; + font-size: 11px; + line-height: 1.5; + padding: 5px 0; + border-bottom: 1px solid rgba(255,255,255,0.04); +} + +.bme-persist-guide__item:last-child { + border-bottom: none; +} + +.bme-persist-guide__item strong { + color: var(--bme-on-surface); + white-space: nowrap; + flex-shrink: 0; + min-width: 90px; +} + +.bme-persist-guide__item span { + color: var(--bme-on-surface-dim); +} + .bme-persist-actions { display: flex; gap: 8px; diff --git a/ui/panel.js b/ui/panel.js index 78b5918..9b3969b 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -1827,37 +1827,75 @@ function _refreshTaskPersistence() { const ps = _getGraphPersistenceSnapshot(); const rs = graph.runtimeState || {}; + const LOAD_STATE_LABELS = { + "no-chat": "无聊天", + loading: "加载中", + loaded: "已加载", + blocked: "已阻塞", + error: "错误", + }; + + const STORAGE_TIER_LABELS = { + none: "无", + metadata: "元数据", + indexeddb: "IndexedDB", + chat: "聊天存档", + }; + + const loadStateLabel = LOAD_STATE_LABELS[ps.loadState] || ps.loadState || "未知"; + const storageTierLabel = STORAGE_TIER_LABELS[ps.acceptedStorageTier || ps.storageTier] || ps.acceptedStorageTier || ps.storageTier || "—"; + const kvs = [ - ["Load State", ps.loadState || "unknown"], - ["Storage Tier", ps.acceptedStorageTier || ps.storageTier || "—"], - ["Revision", ps.revision ?? "—"], - ["Commit Marker", ps.commitMarker ? "present" : "none"], - ["Blocked Reason", ps.blockedReason || ps.reason || "—"], - ["Shadow Snapshot", ps.shadowSnapshotUsed ? "yes" : "no"], + ["加载状态", loadStateLabel], + ["存储层级", storageTierLabel], + ["版本号", ps.revision ?? "—"], + ["提交标记", ps.commitMarker ? "存在" : "无"], + ["阻塞原因", ps.blockedReason || ps.reason || "—"], + ["影子快照", ps.shadowSnapshotUsed ? "已使用" : "未使用"], ]; const kvHtml = kvs.map(([k, v]) => `
      ${_escHtml(k)}${_escHtml(String(v))}
      `).join(""); const journalCount = Array.isArray(rs.historyState?.batchJournal) ? rs.historyState.batchJournal.length : 0; const secondaryKvs = [ - ["Graph Nodes", String((graph.nodes || []).length)], - ["Graph Edges", String((graph.edges || []).length)], - ["Batch Journal", String(journalCount)], - ["Runtime Rev", String(rs.graphRevision ?? "—")], + ["图谱节点", String((graph.nodes || []).length)], + ["图谱边", String((graph.edges || []).length)], + ["批次日志", String(journalCount)], + ["运行版本", String(rs.graphRevision ?? "—")], ]; const secondaryHtml = secondaryKvs.map(([k, v]) => `
      ${_escHtml(k)}${_escHtml(v)}
      `).join(""); + const guidePairs = [ + ["加载状态", "记忆图谱在当前聊天中的加载进度。\"已加载\" 表示正常运行。"], + ["存储层级", "当前持久化使用的最高存储介质。IndexedDB 最快,聊天存档最稳。"], + ["版本号", "图谱修订号,每次写入操作自增。用于检测并发冲突。"], + ["提交标记", "聊天元数据中的标记,指示是否有更高版本存在于本地 IndexedDB。"], + ["阻塞原因", "如果加载被阻塞,这里显示具体原因。\"—\" 表示未阻塞。"], + ["影子快照", "是否在启动时使用了上次会话留下的影子快照来加速加载。"], + ["图谱节点 / 边", "当前内存中图谱的节点和边数量。"], + ["批次日志", "尚未合并到主快照的增量操作日志条目数。"], + ["运行版本", "运行时图谱的内部版本号,和版本号联动。"], + ]; + + const guideHtml = guidePairs.map(([term, desc]) => + `
      ${_escHtml(term)}${_escHtml(desc)}
      ` + ).join(""); + el.innerHTML = `
      -
      Persistence State
      +
      持久化状态
      ${kvHtml}
      -
      Runtime Stats
      +
      运行统计
      ${secondaryHtml}
      +
      +
      字段说明
      + ${guideHtml} +
      `; } From 285fc9ec4538b596614513b47aecace32ffc4512 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:46:33 +0000 Subject: [PATCH 24/24] chore: bump manifest version [skip ci] --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 78a75bc..54d71b8 100644 --- a/manifest.json +++ b/manifest.json @@ -6,6 +6,6 @@ "js": "index.js", "css": "style.css", "author": "Youzini", - "version": "4.9.3", + "version": "4.9.4", "homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology" }