-
+
@@ -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(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)}
+
${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}
-
-
-
-
${_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}
+
`;
}
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"
}