Files
ST-Bionic-Memory-Ecology/graph/memory-scope.js
2026-04-08 21:29:36 +08:00

389 lines
12 KiB
JavaScript

const MEMORY_SCOPE_LAYER = {
OBJECTIVE: "objective",
POV: "pov",
};
const MEMORY_SCOPE_OWNER_TYPE = {
NONE: "",
CHARACTER: "character",
USER: "user",
};
export const DEFAULT_MEMORY_SCOPE = Object.freeze({
layer: MEMORY_SCOPE_LAYER.OBJECTIVE,
ownerType: MEMORY_SCOPE_OWNER_TYPE.NONE,
ownerId: "",
ownerName: "",
regionPrimary: "",
regionPath: [],
regionSecondary: [],
});
export const MEMORY_SCOPE_BUCKETS = Object.freeze({
CHARACTER_POV: "characterPov",
USER_POV: "userPov",
OBJECTIVE_CURRENT_REGION: "objectiveCurrentRegion",
OBJECTIVE_ADJACENT_REGION: "objectiveAdjacentRegion",
OBJECTIVE_GLOBAL: "objectiveGlobal",
OTHER_POV: "otherPov",
});
export const DEFAULT_SCOPE_BUCKET_WEIGHTS = Object.freeze({
[MEMORY_SCOPE_BUCKETS.CHARACTER_POV]: 1.25,
[MEMORY_SCOPE_BUCKETS.USER_POV]: 1.05,
[MEMORY_SCOPE_BUCKETS.OBJECTIVE_CURRENT_REGION]: 1.15,
[MEMORY_SCOPE_BUCKETS.OBJECTIVE_ADJACENT_REGION]: 0.9,
[MEMORY_SCOPE_BUCKETS.OBJECTIVE_GLOBAL]: 0.75,
[MEMORY_SCOPE_BUCKETS.OTHER_POV]: 0.6,
});
function normalizeString(value) {
return String(value ?? "").trim();
}
function normalizeKey(value) {
return normalizeString(value).toLowerCase();
}
function normalizeStringArray(values = []) {
const result = [];
const seen = new Set();
for (const value of Array.isArray(values) ? values : [values]) {
const normalized = normalizeString(value);
const key = normalizeKey(normalized);
if (!normalized || seen.has(key)) continue;
seen.add(key);
result.push(normalized);
}
return result;
}
function normalizeOwnerValueSet(values = []) {
return new Set(
normalizeStringArray(values).map((value) => normalizeKey(value)),
);
}
function normalizeOwnerType(layer, ownerType) {
if (layer !== MEMORY_SCOPE_LAYER.POV) {
return MEMORY_SCOPE_OWNER_TYPE.NONE;
}
if (
ownerType === MEMORY_SCOPE_OWNER_TYPE.CHARACTER ||
ownerType === MEMORY_SCOPE_OWNER_TYPE.USER
) {
return ownerType;
}
return MEMORY_SCOPE_OWNER_TYPE.NONE;
}
function normalizeLayer(layer) {
return layer === MEMORY_SCOPE_LAYER.POV
? MEMORY_SCOPE_LAYER.POV
: MEMORY_SCOPE_LAYER.OBJECTIVE;
}
export function createDefaultMemoryScope(overrides = {}) {
return normalizeMemoryScope(overrides);
}
export function normalizeMemoryScope(scope = {}, defaults = {}) {
const merged = {
...DEFAULT_MEMORY_SCOPE,
...(defaults || {}),
...(scope || {}),
};
const layer = normalizeLayer(merged.layer);
const ownerType = normalizeOwnerType(layer, normalizeString(merged.ownerType));
const ownerId = ownerType
? normalizeString(merged.ownerId || merged.ownerName)
: "";
const ownerName = ownerType ? normalizeString(merged.ownerName) : "";
const regionPrimary = normalizeString(merged.regionPrimary);
const regionPath = normalizeStringArray(merged.regionPath);
const regionSecondary = normalizeStringArray(merged.regionSecondary);
return {
layer,
ownerType,
ownerId,
ownerName,
regionPrimary,
regionPath,
regionSecondary,
};
}
export function normalizeNodeMemoryScope(node, defaults = {}) {
const scope = normalizeMemoryScope(node?.scope, defaults);
if (node && typeof node === "object") {
node.scope = scope;
}
return scope;
}
export function normalizeEdgeMemoryScope(edge, defaults = {}) {
const scope = normalizeMemoryScope(edge?.scope, defaults);
if (edge && typeof edge === "object") {
edge.scope = scope;
}
return scope;
}
export function isPovScope(scope) {
return normalizeMemoryScope(scope).layer === MEMORY_SCOPE_LAYER.POV;
}
export function isObjectiveScope(scope) {
return normalizeMemoryScope(scope).layer === MEMORY_SCOPE_LAYER.OBJECTIVE;
}
export function getScopeOwnerKey(scope) {
const normalized = normalizeMemoryScope(scope);
const ownerType = normalizeString(normalized.ownerType);
const ownerId = normalizeKey(normalized.ownerId || normalized.ownerName);
return ownerType && ownerId ? `${ownerType}:${ownerId}` : "";
}
export function getScopeRegionTokens(scope) {
const normalized = normalizeMemoryScope(scope);
return normalizeStringArray([
normalized.regionPrimary,
...normalized.regionPath,
...normalized.regionSecondary,
]);
}
export function getScopeRegionKey(scope) {
const normalized = normalizeMemoryScope(scope);
return normalizeString(normalized.regionPrimary);
}
export function getScopeSummary(scope) {
const normalized = normalizeMemoryScope(scope);
const regionTokens = getScopeRegionTokens(normalized);
return {
layer: normalized.layer,
ownerType: normalized.ownerType,
ownerId: normalized.ownerId,
ownerName: normalized.ownerName,
ownerKey: getScopeOwnerKey(normalized),
regionPrimary: normalized.regionPrimary,
regionKey: getScopeRegionKey(normalized),
regionTokens,
};
}
export function matchesScopeOwner(scope, ownerType, ownerValue = "") {
const normalized = normalizeMemoryScope(scope);
if (normalizeString(normalized.ownerType) !== normalizeString(ownerType)) {
return false;
}
const target = normalizeKey(ownerValue);
if (!target) {
return Boolean(normalized.ownerType);
}
return [normalized.ownerId, normalized.ownerName]
.map((value) => normalizeKey(value))
.includes(target);
}
export function isSameLatestScopeBucket(node, options = {}) {
const scope = normalizeMemoryScope(options.scope);
const targetType = normalizeString(options.type);
const primaryKeyField = normalizeString(options.primaryKeyField || "name") || "name";
const primaryKeyValue = normalizeString(options.primaryKeyValue);
if (!node || normalizeString(node.type) !== targetType) return false;
if (normalizeString(node?.fields?.[primaryKeyField]) !== primaryKeyValue) {
return false;
}
return hasSameScopeIdentity(node?.scope, scope);
}
export function hasSameScopeIdentity(a, b) {
const scopeA = normalizeMemoryScope(a);
const scopeB = normalizeMemoryScope(b);
if (scopeA.layer !== scopeB.layer) return false;
if (scopeA.layer === MEMORY_SCOPE_LAYER.POV) {
return getScopeOwnerKey(scopeA) === getScopeOwnerKey(scopeB);
}
return normalizeKey(getScopeRegionKey(scopeA)) === normalizeKey(getScopeRegionKey(scopeB));
}
export function canMergeScopedMemories(a, b) {
const scopeA = normalizeMemoryScope(a?.scope || a);
const scopeB = normalizeMemoryScope(b?.scope || b);
if (scopeA.layer !== scopeB.layer) return false;
if (scopeA.layer === MEMORY_SCOPE_LAYER.POV) {
const ownerKeyA = getScopeOwnerKey(scopeA);
const ownerKeyB = getScopeOwnerKey(scopeB);
return Boolean(ownerKeyA) && ownerKeyA === ownerKeyB;
}
const regionA = normalizeKey(getScopeRegionKey(scopeA));
const regionB = normalizeKey(getScopeRegionKey(scopeB));
return regionA === regionB;
}
export function classifyNodeScopeBucket(
node,
{
activeCharacterPovOwner = "",
activeCharacterPovOwners = [],
activeUserPovOwner = "",
activeUserPovOwners = [],
activeRegion = "",
adjacentRegions = [],
enablePovMemory = true,
enableRegionScopedObjective = true,
allowImplicitCharacterPovFallback = true,
} = {},
) {
const scope = normalizeMemoryScope(node?.scope);
const normalizedActiveRegion = normalizeKey(activeRegion);
const normalizedAdjacentRegions = new Set(
normalizeStringArray(adjacentRegions).map((value) => normalizeKey(value)),
);
const normalizedActiveCharacterOwners = normalizeOwnerValueSet([
...normalizeStringArray(activeCharacterPovOwners),
activeCharacterPovOwner,
]);
const normalizedActiveUserOwners = normalizeOwnerValueSet([
...normalizeStringArray(activeUserPovOwners),
activeUserPovOwner,
]);
const scopeOwnerValues = normalizeOwnerValueSet([
scope.ownerId,
scope.ownerName,
]);
if (scope.layer === MEMORY_SCOPE_LAYER.POV) {
if (!enablePovMemory) {
return MEMORY_SCOPE_BUCKETS.OTHER_POV;
}
if (
scope.ownerType === MEMORY_SCOPE_OWNER_TYPE.CHARACTER &&
scopeOwnerValues.size > 0 &&
[...scopeOwnerValues].some((value) =>
normalizedActiveCharacterOwners.has(value),
)
) {
return MEMORY_SCOPE_BUCKETS.CHARACTER_POV;
}
if (
scope.ownerType === MEMORY_SCOPE_OWNER_TYPE.USER &&
scopeOwnerValues.size > 0 &&
[...scopeOwnerValues].some((value) => normalizedActiveUserOwners.has(value))
) {
return MEMORY_SCOPE_BUCKETS.USER_POV;
}
if (
allowImplicitCharacterPovFallback &&
normalizedActiveCharacterOwners.size === 0 &&
scope.ownerType === MEMORY_SCOPE_OWNER_TYPE.CHARACTER
) {
return MEMORY_SCOPE_BUCKETS.CHARACTER_POV;
}
if (
normalizedActiveUserOwners.size === 0 &&
scope.ownerType === MEMORY_SCOPE_OWNER_TYPE.USER
) {
return MEMORY_SCOPE_BUCKETS.USER_POV;
}
return MEMORY_SCOPE_BUCKETS.OTHER_POV;
}
if (!enableRegionScopedObjective || !normalizedActiveRegion) {
return MEMORY_SCOPE_BUCKETS.OBJECTIVE_GLOBAL;
}
const regionPrimary = normalizeKey(scope.regionPrimary);
if (regionPrimary && regionPrimary === normalizedActiveRegion) {
return MEMORY_SCOPE_BUCKETS.OBJECTIVE_CURRENT_REGION;
}
if (regionPrimary && normalizedAdjacentRegions.has(regionPrimary)) {
return MEMORY_SCOPE_BUCKETS.OBJECTIVE_ADJACENT_REGION;
}
const tokens = getScopeRegionTokens(scope).map((value) => normalizeKey(value));
if (
tokens.includes(normalizedActiveRegion) ||
tokens.some((token) => normalizedAdjacentRegions.has(token))
) {
return MEMORY_SCOPE_BUCKETS.OBJECTIVE_ADJACENT_REGION;
}
return MEMORY_SCOPE_BUCKETS.OBJECTIVE_GLOBAL;
}
export function resolveScopeBucketWeight(bucket, overrides = {}) {
return Number(
overrides?.[bucket] ?? DEFAULT_SCOPE_BUCKET_WEIGHTS[bucket] ?? 1,
) || 1;
}
export function describeScopeBucket(bucket) {
switch (bucket) {
case MEMORY_SCOPE_BUCKETS.CHARACTER_POV:
return "角色 POV";
case MEMORY_SCOPE_BUCKETS.USER_POV:
return "用户 POV";
case MEMORY_SCOPE_BUCKETS.OBJECTIVE_CURRENT_REGION:
return "当前地区客观";
case MEMORY_SCOPE_BUCKETS.OBJECTIVE_ADJACENT_REGION:
return "邻近地区客观";
case MEMORY_SCOPE_BUCKETS.OBJECTIVE_GLOBAL:
return "全局客观";
case MEMORY_SCOPE_BUCKETS.OTHER_POV:
return "其他 POV";
default:
return normalizeString(bucket) || "未知作用域";
}
}
export function describeMemoryScope(scope) {
const normalized = normalizeMemoryScope(scope);
const parts = [];
parts.push(
normalized.layer === MEMORY_SCOPE_LAYER.POV ? "POV" : "客观",
);
if (normalized.ownerType) {
const ownerLabel = normalized.ownerName || normalized.ownerId;
parts.push(`${normalized.ownerType}:${ownerLabel || "未命名"}`);
}
if (normalized.regionPrimary) {
parts.push(`地区:${normalized.regionPrimary}`);
}
return parts.join(" | ");
}
export function buildScopeBadgeText(scope) {
const normalized = normalizeMemoryScope(scope);
if (normalized.layer === MEMORY_SCOPE_LAYER.POV) {
const ownerLabel = normalized.ownerName || normalized.ownerId || "POV";
return normalized.ownerType === MEMORY_SCOPE_OWNER_TYPE.USER
? `用户 POV · ${ownerLabel}`
: `角色 POV · ${ownerLabel}`;
}
return normalized.regionPrimary ? `客观 · ${normalized.regionPrimary}` : "客观 · 全局";
}
export function buildRegionLine(scope) {
const normalized = normalizeMemoryScope(scope);
const parts = [];
if (normalized.regionPrimary) {
parts.push(`主地区: ${normalized.regionPrimary}`);
}
if (normalized.regionPath.length > 0) {
parts.push(`地区路径: ${normalized.regionPath.join(" / ")}`);
}
if (normalized.regionSecondary.length > 0) {
parts.push(`次级地区: ${normalized.regionSecondary.join(", ")}`);
}
return parts.join(" | ");
}