diff --git a/manifest.json b/manifest.json index 45e5b1e..54d71b8 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.9.4", "homePage": "https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology" } diff --git a/style.css b/style.css index 0260fb9..2882889 100644 --- a/style.css +++ b/style.css @@ -618,6 +618,978 @@ 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; +} + +#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; + 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; + overflow: hidden; +} + +.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-filters .bme-filter-select { + flex: 0 0 auto; +} + +.bme-memory-list-scroll { + flex: 1; + overflow-y: auto; + min-height: 0; +} + +.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; + min-height: 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__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); + min-width: 0; + word-break: break-word; +} + +.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-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; + flex-wrap: wrap; +} + .bme-graph-toolbar { display: flex; align-items: center; @@ -3886,6 +4858,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, @@ -5985,3 +6993,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 _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; + + const graph = _getGraph?.(); + const loadInfo = _getGraphPersistenceSnapshot(); + if (!graph || !_canRenderGraphData(loadInfo)) { + el.innerHTML = '
      图谱未加载
      '; + return; + } + + 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 listItems = sorted.map((node) => { + const sel = node.id === currentSelectedMemoryNodeId ? "selected" : ""; + const preview = _getNodeSnippet(node); + const scopeBadge = buildScopeBadgeText(node.scope); + const metaText = _buildScopeMetaText(node); + const displayName = getNodeDisplayName(node); + return ` +
      +
      + ${_escHtml(_typeLabel(node.type))} + IMP: ${typeof node.importance === "number" ? node.importance.toFixed(1) : "—"} +
      +
      ${_escHtml(displayName)}
      +
      ${_escHtml(preview)}
      +
      + ${_escHtml(scopeBadge)} + SEQ: ${_formatMemoryInt(node.seqRange?.[1] ?? node.seq, 0)} +
      + ${metaText ? `
      ${_escHtml(metaText)}
      ` : ""} +
      `; + }).join(""); + + el.innerHTML = ` +
      +
      +
      + + +
      +
      + ${listItems || '
      无节点
      '} +
      +
      +
      +
      + `; + + _renderTaskMemoryDetailSelection(graph); + _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() { + 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?.(); + _renderTaskMemoryDetailSelection(graph); + }); +} + +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) => + !e?.invalidAt && + !e?.expiredAt && + (e?.fromId === node.id || e?.toId === node.id), + ); + const detailSummary = _getNodeSnippet(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))}` : "", + scopeBadge ? `${_escHtml(scopeBadge)}` : "", + node.archived ? 'ARCHIVED' : "", + ].filter(Boolean).join(""); + + detailEl.innerHTML = ` +
      +
      ${_escHtml(displayName)}
      +
      + + +
      +
      +
      ${badges}
      +
      ${_escHtml(detailSummary || "无补充字段")}
      +
      + ${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 ---------- + +function _refreshTaskInjectionPreview() { + const el = document.getElementById("bme-task-injection"); + if (!el) return; + + const injectionText = String(_getLastInjection?.() || "").trim(); + if (!injectionText) { + el.innerHTML = '
      暂无注入数据——等待第一次召回注入后显示。
      '; + return; + } + + 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 wrapper = document.createDocumentFragment(); + + if (totalTokens > 0) { + const bar = document.createElement("div"); + bar.className = "bme-injection-token-bar"; + bar.innerHTML = ` + ${totalTokens} / ${budgetTokens} tok +
      +
      +
      + ${pct}%`; + wrapper.appendChild(bar); + } + + wrapper.appendChild(_buildInjectionPreviewNode(injectionText)); + el.replaceChildren(wrapper); +} + +// ---------- 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 = _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 = [ + ["加载状态", 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 = [ + ["图谱节点", 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 = ` +
      +
      +
      持久化状态
      + ${kvHtml} +
      +
      +
      运行统计
      + ${secondaryHtml} +
      +
      +
      +
      字段说明
      + ${guideHtml} +
      + `; } // ==================== 图谱视图切换 ==================== @@ -1857,7 +2472,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"), @@ -3210,6 +3825,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"; @@ -3262,6 +3973,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, "/") @@ -3295,6 +4032,421 @@ function _appendNodeDetailTextareaField( container.appendChild(row); } +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}`; + + _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]}`, + ); + } + const storyTimeSection = document.createElement("div"); + storyTimeSection.className = "bme-node-detail-section"; + storyTimeSection.textContent = "剧情时间"; + fragment.appendChild(storyTimeSection); + _appendNodeDetailReadOnly( + fragment, + "当前摘要", + _describeStoryTimeDisplay(storyTime) || "—", + ); + _appendNodeDetailTextInput( + fragment, + "时间标签", + inputId("story-time-label"), + storyTime.label, + ); + _appendNodeDetailSelectInput( + fragment, + "时态", + inputId("story-time-tense"), + 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( + storyTimeAdvanced, + "相对关系", + inputId("story-time-relation"), + storyTime.relation, + STORY_TIME_RELATION_OPTIONS, + ); + _appendNodeDetailTextInput( + storyTimeAdvanced, + "锚点标签", + inputId("story-time-anchor-label"), + storyTime.anchorLabel, + ); + _appendNodeDetailSelectInput( + storyTimeAdvanced, + "置信度", + inputId("story-time-confidence"), + storyTime.confidence, + STORY_TIME_CONFIDENCE_OPTIONS, + ); + _appendNodeDetailSelectInput( + storyTimeAdvanced, + "来源", + inputId("story-time-source"), + storyTime.source, + STORY_TIME_SOURCE_OPTIONS, + ); + _appendNodeDetailTextInput( + storyTimeAdvanced, + "段 ID", + inputId("story-time-segment-id"), + storyTime.segmentId, + ); + fragment.appendChild(storyTimeAdvanced); + + 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( + storyTimeSpanCollapse, + "当前范围", + _describeStoryTimeSpanDisplay(storyTimeSpan) || "—", + ); + _appendNodeDetailTextInput( + storyTimeSpanCollapse, + "起点标签", + inputId("story-time-span-start-label"), + storyTimeSpan.startLabel, + ); + _appendNodeDetailTextInput( + storyTimeSpanCollapse, + "终点标签", + inputId("story-time-span-end-label"), + storyTimeSpan.endLabel, + ); + _appendNodeDetailSelectInput( + storyTimeSpanCollapse, + "混合时间", + inputId("story-time-span-mixed"), + storyTimeSpan.mixed ? "true" : "false", + STORY_TIME_MIXED_OPTIONS, + ); + _appendNodeDetailSelectInput( + storyTimeSpanCollapse, + "来源", + inputId("story-time-span-source"), + storyTimeSpan.source, + STORY_TIME_SOURCE_OPTIONS, + ); + _appendNodeDetailTextInput( + storyTimeSpanCollapse, + "起点段 ID", + inputId("story-time-span-start-segment-id"), + storyTimeSpan.startSegmentId, + ); + _appendNodeDetailTextInput( + storyTimeSpanCollapse, + "终点段 ID", + inputId("story-time-span-end-segment-id"), + storyTimeSpan.endSegmentId, + ); + fragment.appendChild(storyTimeSpanCollapse); + + _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 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 (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"; } @@ -3336,123 +4488,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"); @@ -3466,110 +4504,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() { @@ -3600,44 +4555,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() { @@ -4025,19 +4954,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 +5035,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 +5116,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 +5154,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 +5310,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"); @@ -10770,7 +11669,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(" · "); } @@ -10812,7 +11711,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;