diff --git a/graph/knowledge-state.js b/graph/knowledge-state.js index 9fcd094..f13b81c 100644 --- a/graph/knowledge-state.js +++ b/graph/knowledge-state.js @@ -1526,6 +1526,452 @@ export function updateRegionAdjacencyManual(graph, region = "", adjacent = []) { return { ok: true, region: normalizedRegion }; } +function buildKnowledgeOwnerAliasVariantSet(owner = {}) { + return buildOwnerAliasVariantSet([ + owner?.ownerName, + ...(Array.isArray(owner?.aliases) ? owner.aliases : []), + ]); +} + +function doesKnowledgeOwnerVariantSetMatchValue(variantSet, value = "") { + if (!(variantSet instanceof Set) || variantSet.size === 0) return false; + for (const variant of collectAliasMatchVariants(value)) { + if (variantSet.has(variant)) return true; + } + return false; +} + +function doesScopeReferenceCharacterOwner(graph, scope = {}, owner = {}) { + const normalizedScope = normalizeMemoryScope(scope); + if (normalizeOwnerType(normalizedScope.ownerType) !== OWNER_TYPE_CHARACTER) { + return false; + } + + const normalizedOwnerKey = normalizeString(owner?.ownerKey); + if (normalizedOwnerKey) { + const resolvedOwnerKey = resolveKnowledgeOwner(graph, { + ownerType: OWNER_TYPE_CHARACTER, + ownerName: normalizedScope.ownerName || normalizedScope.ownerId, + ownerId: normalizedScope.ownerId, + }).ownerKey; + if (resolvedOwnerKey && resolvedOwnerKey === normalizedOwnerKey) { + return true; + } + } + + const aliasVariantSet = buildKnowledgeOwnerAliasVariantSet(owner); + return ( + doesKnowledgeOwnerVariantSetMatchValue(aliasVariantSet, normalizedScope.ownerName) || + doesKnowledgeOwnerVariantSetMatchValue(aliasVariantSet, normalizedScope.ownerId) + ); +} + +function doesCharacterNodeReferenceOwner(graph, node = {}, owner = {}) { + if ( + !node || + node.archived === true || + normalizeString(node?.type) !== "character" || + !normalizeString(node?.fields?.name) + ) { + return false; + } + + const normalizedOwnerKey = normalizeString(owner?.ownerKey); + if (normalizedOwnerKey) { + const resolvedOwnerKey = resolveKnowledgeOwner(graph, { + ownerType: OWNER_TYPE_CHARACTER, + ownerName: node?.fields?.name, + nodeId: node?.id, + }).ownerKey; + if (resolvedOwnerKey && resolvedOwnerKey === normalizedOwnerKey) { + return true; + } + } + + const aliasVariantSet = buildKnowledgeOwnerAliasVariantSet(owner); + return ( + doesKnowledgeOwnerVariantSetMatchValue(aliasVariantSet, node?.fields?.name) || + normalizeString(owner?.nodeId) === normalizeString(node?.id) + ); +} + +function collectCharacterNodesForOwner(graph, owner = {}) { + return Array.isArray(graph?.nodes) + ? graph.nodes.filter((node) => doesCharacterNodeReferenceOwner(graph, node, owner)) + : []; +} + +function rewriteScopedCharacterOwnerReferences( + graph, + owner = {}, + { ownerName = "", ownerId = "" } = {}, +) { + const normalizedOwnerName = normalizeString(ownerName || ownerId); + const normalizedOwnerId = normalizeString(ownerId || ownerName); + if (!normalizedOwnerName || !normalizedOwnerId) { + return { + changedNodeIds: [], + changedEdgeCount: 0, + }; + } + + const changedNodeIds = []; + for (const node of Array.isArray(graph?.nodes) ? graph.nodes : []) { + const normalizedScope = normalizeMemoryScope(node?.scope); + if (!doesScopeReferenceCharacterOwner(graph, normalizedScope, owner)) { + continue; + } + node.scope = normalizeMemoryScope({ + ...normalizedScope, + layer: "pov", + ownerType: OWNER_TYPE_CHARACTER, + ownerName: normalizedOwnerName, + ownerId: normalizedOwnerId, + }); + const normalizedNodeId = normalizeString(node?.id); + if (normalizedNodeId) changedNodeIds.push(normalizedNodeId); + } + + let changedEdgeCount = 0; + for (const edge of Array.isArray(graph?.edges) ? graph.edges : []) { + const normalizedScope = normalizeMemoryScope(edge?.scope); + if (!doesScopeReferenceCharacterOwner(graph, normalizedScope, owner)) { + continue; + } + edge.scope = normalizeMemoryScope({ + ...normalizedScope, + layer: "pov", + ownerType: OWNER_TYPE_CHARACTER, + ownerName: normalizedOwnerName, + ownerId: normalizedOwnerId, + }); + changedEdgeCount += 1; + } + + return { + changedNodeIds: uniqueIds(changedNodeIds), + changedEdgeCount, + }; +} + +function renameCharacterNodesForOwner(graph, owner = {}, nextName = "") { + const normalizedNextName = normalizeString(nextName); + if (!normalizedNextName) return []; + + const changedNodeIds = []; + for (const node of collectCharacterNodesForOwner(graph, owner)) { + node.fields = { + ...(node?.fields || {}), + name: normalizedNextName, + }; + const normalizedNodeId = normalizeString(node?.id); + if (normalizedNodeId) changedNodeIds.push(normalizedNodeId); + } + return uniqueIds(changedNodeIds); +} + +function updateHistoryOwnerReferences(historyState, previousOwner = {}, nextOwner = null) { + if (!historyState || typeof historyState !== "object") return; + + const previousOwnerKey = normalizeString(previousOwner?.ownerKey); + const nextOwnerKey = normalizeString(nextOwner?.ownerKey); + const nextOwnerName = normalizeString(nextOwner?.ownerName); + const previousAliasVariants = buildKnowledgeOwnerAliasVariantSet(previousOwner); + + if ( + doesKnowledgeOwnerVariantSetMatchValue( + previousAliasVariants, + historyState.activeCharacterPovOwner, + ) + ) { + historyState.activeCharacterPovOwner = nextOwnerName; + } + + if (normalizeString(historyState.activeRecallOwnerKey) === previousOwnerKey) { + historyState.activeRecallOwnerKey = nextOwnerKey; + } + + historyState.recentRecallOwnerKeys = uniqueStrings( + (historyState.recentRecallOwnerKeys || []) + .map((value) => { + const normalizedValue = normalizeString(value); + if (!normalizedValue) return ""; + return normalizedValue === previousOwnerKey ? nextOwnerKey : normalizedValue; + }) + .filter(Boolean), + ).slice(0, RECENT_RECALL_OWNER_LIMIT); +} + +function archiveOwnerCharacterNodesForMerge(graph, sourceOwner = {}, targetOwner = {}) { + const sourceNodes = collectCharacterNodesForOwner(graph, sourceOwner); + const archivedNodeIds = []; + let adoptedNodeId = normalizeString(targetOwner?.nodeId); + + if (!adoptedNodeId && sourceNodes.length > 0) { + const keeper = sourceNodes[0]; + keeper.fields = { + ...(keeper?.fields || {}), + name: normalizeString(targetOwner?.ownerName || keeper?.fields?.name), + }; + keeper.archived = false; + adoptedNodeId = normalizeString(keeper?.id); + + for (const node of sourceNodes.slice(1)) { + node.archived = true; + const normalizedNodeId = normalizeString(node?.id); + if (normalizedNodeId) archivedNodeIds.push(normalizedNodeId); + } + return { + adoptedNodeId, + archivedNodeIds: uniqueIds(archivedNodeIds), + }; + } + + for (const node of sourceNodes) { + node.archived = true; + const normalizedNodeId = normalizeString(node?.id); + if (normalizedNodeId) archivedNodeIds.push(normalizedNodeId); + } + return { + adoptedNodeId, + archivedNodeIds: uniqueIds(archivedNodeIds), + }; +} + +function archiveCharacterNodesForOwner(graph, owner = {}) { + const archivedNodeIds = []; + for (const node of collectCharacterNodesForOwner(graph, owner)) { + node.archived = true; + const normalizedNodeId = normalizeString(node?.id); + if (normalizedNodeId) archivedNodeIds.push(normalizedNodeId); + } + return uniqueIds(archivedNodeIds); +} + +function archivePovNodesForOwner(graph, owner = {}) { + const archivedNodeIds = []; + for (const node of Array.isArray(graph?.nodes) ? graph.nodes : []) { + const normalizedScope = normalizeMemoryScope(node?.scope); + if (normalizedScope.layer !== "pov") continue; + if (!doesScopeReferenceCharacterOwner(graph, normalizedScope, owner)) continue; + node.archived = true; + const normalizedNodeId = normalizeString(node?.id); + if (normalizedNodeId) archivedNodeIds.push(normalizedNodeId); + } + return uniqueIds(archivedNodeIds); +} + +export function renameKnowledgeOwner(graph, ownerKey = "", nextName = "") { + normalizeGraphCognitiveState(graph); + const normalizedOwnerKey = normalizeString(ownerKey); + const normalizedNextName = normalizeString(nextName); + if (!normalizedOwnerKey || !normalizedNextName) { + return { ok: false, reason: "missing-owner-or-name" }; + } + + const sourceEntry = createDefaultKnowledgeOwnerState( + graph?.knowledgeState?.owners?.[normalizedOwnerKey] || {}, + ); + if (!sourceEntry.ownerKey) { + return { ok: false, reason: "owner-not-found" }; + } + if (normalizeOwnerType(sourceEntry.ownerType) !== OWNER_TYPE_CHARACTER) { + return { ok: false, reason: "unsupported-owner-type" }; + } + if (normalizeKey(sourceEntry.ownerName) === normalizeKey(normalizedNextName)) { + return { + ok: true, + ownerKey: normalizedOwnerKey, + unchanged: true, + }; + } + + const scopeRewrite = rewriteScopedCharacterOwnerReferences(graph, sourceEntry, { + ownerName: normalizedNextName, + ownerId: normalizedNextName, + }); + const renamedCharacterNodeIds = renameCharacterNodesForOwner( + graph, + sourceEntry, + normalizedNextName, + ); + const nextOwnerKey = buildOwnerKey( + OWNER_TYPE_CHARACTER, + normalizedNextName, + sourceEntry.nodeId, + graph, + ); + const nextEntry = createDefaultKnowledgeOwnerState({ + ...sourceEntry, + ownerKey: nextOwnerKey, + ownerName: normalizedNextName, + aliases: uniqueStrings([ + normalizedNextName, + sourceEntry.ownerName, + ...(sourceEntry.aliases || []), + ]), + updatedAt: Date.now(), + lastSource: "manual-owner-rename", + }); + + delete graph.knowledgeState.owners[normalizedOwnerKey]; + graph.knowledgeState.owners[nextOwnerKey] = graph.knowledgeState.owners[nextOwnerKey] + ? mergeKnowledgeOwnerEntries(graph.knowledgeState.owners[nextOwnerKey], nextEntry) + : nextEntry; + + const resolvedNextOwner = createDefaultKnowledgeOwnerState( + graph.knowledgeState.owners[nextOwnerKey], + ); + updateHistoryOwnerReferences(graph.historyState, sourceEntry, resolvedNextOwner); + graph.knowledgeState = normalizeKnowledgeState(graph.knowledgeState, graph); + return { + ok: true, + ownerKey: nextOwnerKey, + previousOwnerKey: normalizedOwnerKey, + renamedCharacterNodeIds, + updatedPovNodeIds: scopeRewrite.changedNodeIds, + updatedPovEdgeCount: scopeRewrite.changedEdgeCount, + }; +} + +export function mergeKnowledgeOwners( + graph, + { sourceOwnerKey = "", targetOwnerKey = "" } = {}, +) { + normalizeGraphCognitiveState(graph); + const normalizedSourceOwnerKey = normalizeString(sourceOwnerKey); + const normalizedTargetOwnerKey = normalizeString(targetOwnerKey); + if (!normalizedSourceOwnerKey || !normalizedTargetOwnerKey) { + return { ok: false, reason: "missing-owner-key" }; + } + if (normalizedSourceOwnerKey === normalizedTargetOwnerKey) { + return { ok: false, reason: "same-owner" }; + } + + const sourceEntry = createDefaultKnowledgeOwnerState( + graph?.knowledgeState?.owners?.[normalizedSourceOwnerKey] || {}, + ); + const targetEntry = createDefaultKnowledgeOwnerState( + graph?.knowledgeState?.owners?.[normalizedTargetOwnerKey] || {}, + ); + if (!sourceEntry.ownerKey || !targetEntry.ownerKey) { + return { ok: false, reason: "owner-not-found" }; + } + if ( + normalizeOwnerType(sourceEntry.ownerType) !== OWNER_TYPE_CHARACTER || + normalizeOwnerType(targetEntry.ownerType) !== OWNER_TYPE_CHARACTER + ) { + return { ok: false, reason: "unsupported-owner-type" }; + } + + const scopeRewrite = rewriteScopedCharacterOwnerReferences(graph, sourceEntry, { + ownerName: targetEntry.ownerName, + ownerId: targetEntry.ownerName, + }); + const characterNodeMerge = archiveOwnerCharacterNodesForMerge( + graph, + sourceEntry, + targetEntry, + ); + const mergedEntry = mergeKnowledgeOwnerEntries( + createDefaultKnowledgeOwnerState({ + ...targetEntry, + nodeId: targetEntry.nodeId || characterNodeMerge.adoptedNodeId || sourceEntry.nodeId, + aliases: uniqueStrings([ + ...(targetEntry.aliases || []), + ...(sourceEntry.aliases || []), + targetEntry.ownerName, + sourceEntry.ownerName, + ]), + updatedAt: Date.now(), + lastSource: "manual-owner-merge", + }), + createDefaultKnowledgeOwnerState({ + ...sourceEntry, + ownerKey: targetEntry.ownerKey, + ownerName: targetEntry.ownerName, + nodeId: targetEntry.nodeId || characterNodeMerge.adoptedNodeId || sourceEntry.nodeId, + aliases: uniqueStrings([ + ...(targetEntry.aliases || []), + ...(sourceEntry.aliases || []), + targetEntry.ownerName, + sourceEntry.ownerName, + ]), + updatedAt: Date.now(), + lastSource: "manual-owner-merge", + }), + ); + + graph.knowledgeState.owners[targetEntry.ownerKey] = mergedEntry; + delete graph.knowledgeState.owners[sourceEntry.ownerKey]; + + updateHistoryOwnerReferences( + graph.historyState, + sourceEntry, + createDefaultKnowledgeOwnerState(graph.knowledgeState.owners[targetEntry.ownerKey]), + ); + graph.knowledgeState = normalizeKnowledgeState(graph.knowledgeState, graph); + return { + ok: true, + ownerKey: targetEntry.ownerKey, + sourceOwnerKey: sourceEntry.ownerKey, + archivedCharacterNodeIds: characterNodeMerge.archivedNodeIds, + adoptedCharacterNodeId: characterNodeMerge.adoptedNodeId || "", + updatedPovNodeIds: scopeRewrite.changedNodeIds, + updatedPovEdgeCount: scopeRewrite.changedEdgeCount, + }; +} + +export function deleteKnowledgeOwner( + graph, + ownerKey = "", + { mode = "owner-only" } = {}, +) { + normalizeGraphCognitiveState(graph); + const normalizedOwnerKey = normalizeString(ownerKey); + const normalizedMode = normalizeString(mode) || "owner-only"; + if (!normalizedOwnerKey) { + return { ok: false, reason: "missing-owner-key" }; + } + if ( + !["owner-only", "archive-character", "archive-all"].includes( + normalizedMode, + ) + ) { + return { ok: false, reason: "invalid-delete-mode" }; + } + + const sourceEntry = createDefaultKnowledgeOwnerState( + graph?.knowledgeState?.owners?.[normalizedOwnerKey] || {}, + ); + if (!sourceEntry.ownerKey) { + return { ok: false, reason: "owner-not-found" }; + } + if (normalizeOwnerType(sourceEntry.ownerType) !== OWNER_TYPE_CHARACTER) { + return { ok: false, reason: "unsupported-owner-type" }; + } + + const archivedCharacterNodeIds = + normalizedMode === "archive-character" || normalizedMode === "archive-all" + ? archiveCharacterNodesForOwner(graph, sourceEntry) + : []; + const archivedPovNodeIds = + normalizedMode === "archive-all" + ? archivePovNodesForOwner(graph, sourceEntry) + : []; + + delete graph.knowledgeState.owners[normalizedOwnerKey]; + updateHistoryOwnerReferences(graph.historyState, sourceEntry, null); + graph.knowledgeState = normalizeKnowledgeState(graph.knowledgeState, graph); + return { + ok: true, + ownerKey: normalizedOwnerKey, + mode: normalizedMode, + archivedCharacterNodeIds, + archivedPovNodeIds, + }; +} + export function getKnowledgeOwnerEntry(graph, ownerKey = "") { normalizeGraphCognitiveState(graph); const normalizedOwnerKey = normalizeString(ownerKey); diff --git a/index.js b/index.js index 3681d78..8c912bd 100644 --- a/index.js +++ b/index.js @@ -271,6 +271,9 @@ import { DEFAULT_NODE_SCHEMA, validateSchema } from "./graph/schema.js"; import { applyManualKnowledgeOverride, clearManualKnowledgeOverride, + deleteKnowledgeOwner, + mergeKnowledgeOwners, + renameKnowledgeOwner, setManualActiveRegion, updateRegionAdjacencyManual, } from "./graph/knowledge-state.js"; @@ -17689,6 +17692,89 @@ function onClearPanelKnowledgeOverride(payload = {}) { }; } +function onRenamePanelKnowledgeOwner(payload = {}) { + const ownerKey = String(payload.ownerKey || "").trim(); + const nextName = String(payload.nextName || "").trim(); + if (!currentGraph || !ownerKey || !nextName) { + return { ok: false, error: "invalid-payload" }; + } + if (!ensureGraphMutationReady("角色认知重命名", { notify: false })) { + return { ok: false, error: "graph-write-blocked" }; + } + + const result = renameKnowledgeOwner(currentGraph, ownerKey, nextName); + if (!result?.ok) { + return { ok: false, error: result?.reason || "rename-owner-failed" }; + } + + const persist = saveGraphToChat({ reason: "panel-knowledge-owner-rename" }); + refreshPanelLiveState(); + return { + ok: true, + ownerKey: result.ownerKey || ownerKey, + previousOwnerKey: result.previousOwnerKey || ownerKey, + persist, + persistBlocked: Boolean(persist?.blocked), + }; +} + +function onMergePanelKnowledgeOwners(payload = {}) { + const sourceOwnerKey = String(payload.sourceOwnerKey || payload.ownerKey || "").trim(); + const targetOwnerKey = String(payload.targetOwnerKey || "").trim(); + if (!currentGraph || !sourceOwnerKey || !targetOwnerKey) { + return { ok: false, error: "invalid-payload" }; + } + if (!ensureGraphMutationReady("角色认知合并", { notify: false })) { + return { ok: false, error: "graph-write-blocked" }; + } + + const result = mergeKnowledgeOwners(currentGraph, { + sourceOwnerKey, + targetOwnerKey, + }); + if (!result?.ok) { + return { ok: false, error: result?.reason || "merge-owner-failed" }; + } + + const persist = saveGraphToChat({ reason: "panel-knowledge-owner-merge" }); + refreshPanelLiveState(); + return { + ok: true, + ownerKey: result.ownerKey || targetOwnerKey, + sourceOwnerKey: result.sourceOwnerKey || sourceOwnerKey, + persist, + persistBlocked: Boolean(persist?.blocked), + }; +} + +function onDeletePanelKnowledgeOwner(payload = {}) { + const ownerKey = String(payload.ownerKey || "").trim(); + const mode = String(payload.mode || "owner-only").trim() || "owner-only"; + if (!currentGraph || !ownerKey) { + return { ok: false, error: "invalid-payload" }; + } + if (!ensureGraphMutationReady("角色认知删除", { notify: false })) { + return { ok: false, error: "graph-write-blocked" }; + } + + const result = deleteKnowledgeOwner(currentGraph, ownerKey, { mode }); + if (!result?.ok) { + return { ok: false, error: result?.reason || "delete-owner-failed" }; + } + + const persist = saveGraphToChat({ + reason: `panel-knowledge-owner-delete-${result.mode || mode}`, + }); + refreshPanelLiveState(); + return { + ok: true, + ownerKey: result.ownerKey || ownerKey, + mode: result.mode || mode, + persist, + persistBlocked: Boolean(persist?.blocked), + }; +} + function onSetPanelActiveRegion(payload = {}) { const region = String(payload.region || "").trim(); if (!currentGraph) { @@ -18681,6 +18767,9 @@ async function onCompactLukerSidecar() { deleteGraphNode: onDeletePanelGraphNode, applyKnowledgeOverride: onApplyPanelKnowledgeOverride, clearKnowledgeOverride: onClearPanelKnowledgeOverride, + renameKnowledgeOwner: onRenamePanelKnowledgeOwner, + mergeKnowledgeOwners: onMergePanelKnowledgeOwners, + deleteKnowledgeOwner: onDeletePanelKnowledgeOwner, setActiveRegion: onSetPanelActiveRegion, setActiveStoryTime: onSetPanelActiveStoryTime, clearActiveStoryTime: onClearPanelActiveStoryTime, diff --git a/tests/knowledge-state.mjs b/tests/knowledge-state.mjs index 07046c6..3147ed6 100644 --- a/tests/knowledge-state.mjs +++ b/tests/knowledge-state.mjs @@ -5,9 +5,12 @@ import { applyCognitionUpdates, applyManualKnowledgeOverride, clearManualKnowledgeOverride, + deleteKnowledgeOwner, + mergeKnowledgeOwners, applyRegionUpdates, computeKnowledgeGateForNode, listKnowledgeOwners, + renameKnowledgeOwner, resolveActiveRegionContext, resolveAdjacentRegions, resolveKnowledgeOwner, @@ -289,4 +292,305 @@ assert.equal( true, ); +const renameGraph = createEmptyGraph(); +const renameCharacter = createNode({ + type: "character", + fields: { name: "艾琳", state: "守塔人" }, + seq: 1, +}); +const renameObjectiveEvent = createNode({ + type: "event", + fields: { title: "塔楼晨钟", summary: "晨钟再次响起" }, + seq: 2, +}); +const renamePovMemory = createNode({ + type: "pov_memory", + fields: { summary: "艾琳记得晨钟响起" }, + seq: 3, + scope: { + layer: "pov", + ownerType: "character", + ownerName: "艾琳", + ownerId: "艾琳", + }, +}); +addNode(renameGraph, renameCharacter); +addNode(renameGraph, renameObjectiveEvent); +addNode(renameGraph, renamePovMemory); +applyCognitionUpdates( + renameGraph, + [ + { + ownerType: "character", + ownerName: "艾琳", + ownerNodeId: renameCharacter.id, + knownRefs: [renameObjectiveEvent.id], + visibility: [{ ref: renameObjectiveEvent.id, score: 1 }], + }, + ], + { changedNodeIds: [renameObjectiveEvent.id] }, +); +const renameOwner = resolveKnowledgeOwner(renameGraph, { + ownerType: "character", + ownerName: "艾琳", + nodeId: renameCharacter.id, +}); +renameGraph.historyState.activeCharacterPovOwner = "艾琳"; +renameGraph.historyState.activeRecallOwnerKey = renameOwner.ownerKey; +renameGraph.historyState.recentRecallOwnerKeys = [renameOwner.ownerKey]; +const renameResult = renameKnowledgeOwner(renameGraph, renameOwner.ownerKey, "艾琳娜"); +assert.equal(renameResult.ok, true); +assert.equal(renameCharacter.fields.name, "艾琳娜"); +assert.equal(renamePovMemory.scope.ownerName, "艾琳娜"); +assert.equal(renamePovMemory.scope.ownerId, "艾琳娜"); +assert.equal(renameGraph.historyState.activeCharacterPovOwner, "艾琳娜"); +assert.equal(renameGraph.historyState.activeRecallOwnerKey, renameResult.ownerKey); +assert.equal(renameGraph.knowledgeState.owners[renameOwner.ownerKey], undefined); +assert.equal(renameGraph.knowledgeState.owners[renameResult.ownerKey].ownerName, "艾琳娜"); +assert.equal( + renameGraph.knowledgeState.owners[renameResult.ownerKey].aliases.includes("艾琳"), + true, +); + +const mergeGraph = createEmptyGraph(); +const mergeSourceCharacter = createNode({ + type: "character", + fields: { name: "艾琳", state: "旧身份" }, + seq: 1, +}); +const mergeTargetCharacter = createNode({ + type: "character", + fields: { name: "艾琳娜", state: "新身份" }, + seq: 2, +}); +const mergeSourceEvent = createNode({ + type: "event", + fields: { title: "旧钟楼记忆", summary: "她想起了旧钟楼" }, + seq: 3, +}); +const mergeTargetEvent = createNode({ + type: "event", + fields: { title: "新花园记忆", summary: "她想起了新花园" }, + seq: 4, +}); +const mergeSourcePov = createNode({ + type: "pov_memory", + fields: { summary: "艾琳的 POV 记忆" }, + seq: 5, + scope: { + layer: "pov", + ownerType: "character", + ownerName: "艾琳", + ownerId: "艾琳", + }, +}); +addNode(mergeGraph, mergeSourceCharacter); +addNode(mergeGraph, mergeTargetCharacter); +addNode(mergeGraph, mergeSourceEvent); +addNode(mergeGraph, mergeTargetEvent); +addNode(mergeGraph, mergeSourcePov); +applyCognitionUpdates( + mergeGraph, + [ + { + ownerType: "character", + ownerName: "艾琳", + ownerNodeId: mergeSourceCharacter.id, + knownRefs: [mergeSourceEvent.id], + visibility: [{ ref: mergeSourceEvent.id, score: 0.95 }], + }, + { + ownerType: "character", + ownerName: "艾琳娜", + ownerNodeId: mergeTargetCharacter.id, + knownRefs: [mergeTargetEvent.id], + visibility: [{ ref: mergeTargetEvent.id, score: 0.9 }], + }, + ], + { changedNodeIds: [mergeSourceEvent.id, mergeTargetEvent.id] }, +); +const mergeSourceOwner = resolveKnowledgeOwner(mergeGraph, { + ownerType: "character", + ownerName: "艾琳", + nodeId: mergeSourceCharacter.id, +}); +const mergeTargetOwner = resolveKnowledgeOwner(mergeGraph, { + ownerType: "character", + ownerName: "艾琳娜", + nodeId: mergeTargetCharacter.id, +}); +mergeGraph.historyState.activeCharacterPovOwner = "艾琳"; +mergeGraph.historyState.activeRecallOwnerKey = mergeSourceOwner.ownerKey; +mergeGraph.historyState.recentRecallOwnerKeys = [ + mergeSourceOwner.ownerKey, + mergeTargetOwner.ownerKey, +]; +const mergeResult = mergeKnowledgeOwners(mergeGraph, { + sourceOwnerKey: mergeSourceOwner.ownerKey, + targetOwnerKey: mergeTargetOwner.ownerKey, +}); +assert.equal(mergeResult.ok, true); +assert.equal(mergeGraph.knowledgeState.owners[mergeSourceOwner.ownerKey], undefined); +assert.equal(mergeGraph.knowledgeState.owners[mergeTargetOwner.ownerKey].knownNodeIds.includes(mergeSourceEvent.id), true); +assert.equal(mergeGraph.knowledgeState.owners[mergeTargetOwner.ownerKey].knownNodeIds.includes(mergeTargetEvent.id), true); +assert.equal(mergeGraph.knowledgeState.owners[mergeTargetOwner.ownerKey].aliases.includes("艾琳"), true); +assert.equal(mergeSourcePov.scope.ownerName, "艾琳娜"); +assert.equal(mergeSourcePov.scope.ownerId, "艾琳娜"); +assert.equal(mergeSourceCharacter.archived, true); +assert.equal(mergeGraph.historyState.activeCharacterPovOwner, "艾琳娜"); +assert.equal(mergeGraph.historyState.activeRecallOwnerKey, mergeTargetOwner.ownerKey); + +const deleteOwnerOnlyGraph = createEmptyGraph(); +const deleteOwnerOnlyCharacter = createNode({ + type: "character", + fields: { name: "米娅", state: "书记官" }, + seq: 1, +}); +const deleteOwnerOnlyEvent = createNode({ + type: "event", + fields: { title: "记录密报", summary: "米娅记录了一份密报" }, + seq: 2, +}); +const deleteOwnerOnlyPov = createNode({ + type: "pov_memory", + fields: { summary: "米娅的 POV" }, + seq: 3, + scope: { + layer: "pov", + ownerType: "character", + ownerName: "米娅", + ownerId: "米娅", + }, +}); +addNode(deleteOwnerOnlyGraph, deleteOwnerOnlyCharacter); +addNode(deleteOwnerOnlyGraph, deleteOwnerOnlyEvent); +addNode(deleteOwnerOnlyGraph, deleteOwnerOnlyPov); +applyCognitionUpdates( + deleteOwnerOnlyGraph, + [ + { + ownerType: "character", + ownerName: "米娅", + ownerNodeId: deleteOwnerOnlyCharacter.id, + knownRefs: [deleteOwnerOnlyEvent.id], + visibility: [{ ref: deleteOwnerOnlyEvent.id, score: 0.85 }], + }, + ], + { changedNodeIds: [deleteOwnerOnlyEvent.id] }, +); +const deleteOwnerOnly = resolveKnowledgeOwner(deleteOwnerOnlyGraph, { + ownerType: "character", + ownerName: "米娅", + nodeId: deleteOwnerOnlyCharacter.id, +}); +const deleteOwnerOnlyResult = deleteKnowledgeOwner(deleteOwnerOnlyGraph, deleteOwnerOnly.ownerKey, { + mode: "owner-only", +}); +assert.equal(deleteOwnerOnlyResult.ok, true); +assert.equal(deleteOwnerOnlyGraph.knowledgeState.owners[deleteOwnerOnly.ownerKey], undefined); +assert.equal(deleteOwnerOnlyCharacter.archived, false); +assert.equal(deleteOwnerOnlyPov.archived, false); + +const deleteArchiveCharacterGraph = createEmptyGraph(); +const deleteArchiveCharacterNode = createNode({ + type: "character", + fields: { name: "诺拉", state: "侍女" }, + seq: 1, +}); +const deleteArchiveCharacterEvent = createNode({ + type: "event", + fields: { title: "诺拉送信", summary: "诺拉送出了一封信" }, + seq: 2, +}); +const deleteArchiveCharacterPov = createNode({ + type: "pov_memory", + fields: { summary: "诺拉的 POV" }, + seq: 3, + scope: { + layer: "pov", + ownerType: "character", + ownerName: "诺拉", + ownerId: "诺拉", + }, +}); +addNode(deleteArchiveCharacterGraph, deleteArchiveCharacterNode); +addNode(deleteArchiveCharacterGraph, deleteArchiveCharacterEvent); +addNode(deleteArchiveCharacterGraph, deleteArchiveCharacterPov); +applyCognitionUpdates( + deleteArchiveCharacterGraph, + [ + { + ownerType: "character", + ownerName: "诺拉", + ownerNodeId: deleteArchiveCharacterNode.id, + knownRefs: [deleteArchiveCharacterEvent.id], + visibility: [{ ref: deleteArchiveCharacterEvent.id, score: 0.82 }], + }, + ], + { changedNodeIds: [deleteArchiveCharacterEvent.id] }, +); +const deleteArchiveCharacterOwner = resolveKnowledgeOwner(deleteArchiveCharacterGraph, { + ownerType: "character", + ownerName: "诺拉", + nodeId: deleteArchiveCharacterNode.id, +}); +const deleteArchiveCharacterResult = deleteKnowledgeOwner( + deleteArchiveCharacterGraph, + deleteArchiveCharacterOwner.ownerKey, + { mode: "archive-character" }, +); +assert.equal(deleteArchiveCharacterResult.ok, true); +assert.equal(deleteArchiveCharacterNode.archived, true); +assert.equal(deleteArchiveCharacterPov.archived, false); + +const deleteArchiveAllGraph = createEmptyGraph(); +const deleteArchiveAllCharacter = createNode({ + type: "character", + fields: { name: "赛拉", state: "守卫" }, + seq: 1, +}); +const deleteArchiveAllEvent = createNode({ + type: "event", + fields: { title: "赛拉巡逻", summary: "赛拉完成了巡逻" }, + seq: 2, +}); +const deleteArchiveAllPov = createNode({ + type: "pov_memory", + fields: { summary: "赛拉的 POV" }, + seq: 3, + scope: { + layer: "pov", + ownerType: "character", + ownerName: "赛拉", + ownerId: "赛拉", + }, +}); +addNode(deleteArchiveAllGraph, deleteArchiveAllCharacter); +addNode(deleteArchiveAllGraph, deleteArchiveAllEvent); +addNode(deleteArchiveAllGraph, deleteArchiveAllPov); +applyCognitionUpdates( + deleteArchiveAllGraph, + [ + { + ownerType: "character", + ownerName: "赛拉", + ownerNodeId: deleteArchiveAllCharacter.id, + knownRefs: [deleteArchiveAllEvent.id], + visibility: [{ ref: deleteArchiveAllEvent.id, score: 0.88 }], + }, + ], + { changedNodeIds: [deleteArchiveAllEvent.id] }, +); +const deleteArchiveAllOwner = resolveKnowledgeOwner(deleteArchiveAllGraph, { + ownerType: "character", + ownerName: "赛拉", + nodeId: deleteArchiveAllCharacter.id, +}); +const deleteArchiveAllResult = deleteKnowledgeOwner(deleteArchiveAllGraph, deleteArchiveAllOwner.ownerKey, { + mode: "archive-all", +}); +assert.equal(deleteArchiveAllResult.ok, true); +assert.equal(deleteArchiveAllCharacter.archived, true); +assert.equal(deleteArchiveAllPov.archived, true); + console.log("knowledge-state tests passed"); diff --git a/ui/panel.js b/ui/panel.js index 43d561f..cfbf1ae 100644 --- a/ui/panel.js +++ b/ui/panel.js @@ -2778,6 +2778,56 @@ function _renderCogOwnerDetail(graph, loadInfo, canRender, targetEl) { const suppressedCount = new Set([...(ownerState.manualHiddenNodeIds || []), ...(ownerState.mistakenNodeIds || [])]).size; const disabledAttr = !selectedNode || writeBlocked ? "disabled" : ""; const displayInfo = _getOwnerDisplayInfo(selectedOwner, collisionIndex); + const isCharacterOwner = String(selectedOwner.ownerType || "") === "character"; + const ownerActionDisabledAttr = writeBlocked || !isCharacterOwner ? "disabled" : ""; + const mergeCandidates = _getCognitionOwnerCollection(graph).filter( + (entry) => + String(entry?.ownerType || "") === "character" && + String(entry?.ownerKey || "") !== String(selectedOwner.ownerKey || ""), + ); + const mergeOptions = mergeCandidates.length + ? mergeCandidates + .map((entry) => { + const targetDisplayInfo = _getOwnerDisplayInfo(entry, collisionIndex); + return ``; + }) + .join("") + : ''; + const mergeDisabledAttr = + writeBlocked || !isCharacterOwner || mergeCandidates.length === 0 ? "disabled" : ""; + const ownerManagementSection = isCharacterOwner + ? ` +