Files
ST-Bionic-Memory-Ecology/graph/memory-scope.js
2026-04-23 18:48:30 +08:00

602 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
}
const SCOPE_REGION_TEXT_KEYS = ["name", "title", "label", "value", "text"];
function isPlainScopeObject(scope = null) {
if (!scope || typeof scope !== "object" || Array.isArray(scope)) {
return false;
}
const prototype = Object.getPrototypeOf(scope);
return prototype === Object.prototype || prototype === null;
}
function hasScopeAccessor(scope = {}, key = "") {
const descriptor = Object.getOwnPropertyDescriptor(scope, key);
return Boolean(
descriptor &&
(typeof descriptor.get === "function" || typeof descriptor.set === "function"),
);
}
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 splitScopeRegionText(value = "", { allowSlash = true } = {}) {
const normalized = normalizeString(value)
.replace(/[>→]+/g, "/")
.replace(/\r/g, "\n");
if (!normalized) {
return [];
}
const separatorPattern = allowSlash
? /[,\n/\\、;;|]+/
: /[,\n;|]+/;
return normalized
.split(separatorPattern)
.map((entry) => normalizeString(entry))
.filter(Boolean);
}
function extractScopeRegionText(value = null) {
if (value == null) {
return "";
}
if (typeof value === "string" || typeof value === "number") {
return normalizeString(value);
}
if (typeof value === "boolean" || typeof value === "symbol") {
return "";
}
if (Array.isArray(value)) {
return "";
}
if (typeof value === "object") {
for (const key of SCOPE_REGION_TEXT_KEYS) {
let candidate = "";
try {
candidate = value?.[key];
} catch {
candidate = "";
}
if (typeof candidate === "string" || typeof candidate === "number") {
return normalizeString(candidate);
}
}
return "";
}
return normalizeString(value);
}
function normalizeScopeRegionList(values = [], { allowSlash = true } = {}) {
const result = [];
const seen = new Set();
const pushValue = (value) => {
const normalized = normalizeString(value);
const key = normalizeKey(normalized);
if (!normalized || seen.has(key)) {
return;
}
seen.add(key);
result.push(normalized);
};
const visit = (value) => {
if (Array.isArray(value)) {
for (const entry of value) {
visit(entry);
}
return;
}
const text = extractScopeRegionText(value);
if (!text) {
return;
}
const parts = splitScopeRegionText(text, { allowSlash });
if (parts.length === 0) {
pushValue(text);
return;
}
for (const part of parts) {
pushValue(part);
}
};
visit(values);
return result;
}
function appendUniqueTokenToPath(values = [], token = "") {
const normalizedToken = normalizeString(token);
if (!normalizedToken) {
return normalizeScopeRegionList(values, { allowSlash: true });
}
const tokenKey = normalizeKey(normalizedToken);
const filtered = normalizeScopeRegionList(values, { allowSlash: true });
if (filtered.some((value) => normalizeKey(value) === tokenKey)) {
return filtered;
}
return [...filtered, normalizedToken];
}
function isAlreadyNormalizedStringArray(values = []) {
if (!Array.isArray(values)) return false;
const seen = new Set();
for (const value of values) {
if (typeof value !== "string") return false;
const normalized = normalizeString(value);
const key = normalizeKey(normalized);
if (!normalized || normalized !== value || seen.has(key)) {
return false;
}
seen.add(key);
}
return true;
}
function canReuseNormalizedMemoryScope(scope = {}, defaults = {}) {
if (
!isPlainScopeObject(scope) ||
(defaults && typeof defaults === "object" && Object.keys(defaults).length > 0)
) {
return false;
}
if (
[
"layer",
"ownerType",
"ownerId",
"ownerName",
"regionPrimary",
"regionPath",
"regionSecondary",
].some((key) => hasScopeAccessor(scope, key))
) {
return false;
}
const layer = normalizeLayer(scope.layer);
const ownerType = normalizeOwnerType(layer, normalizeString(scope.ownerType));
const ownerId = ownerType
? normalizeString(scope.ownerId || scope.ownerName)
: "";
const ownerName = ownerType ? normalizeString(scope.ownerName) : "";
const regionPrimary = normalizeString(scope.regionPrimary);
return (
scope.layer === layer &&
normalizeString(scope.ownerType) === ownerType &&
normalizeString(scope.ownerId || "") === ownerId &&
normalizeString(scope.ownerName || "") === ownerName &&
normalizeString(scope.regionPrimary || "") === regionPrimary &&
isAlreadyNormalizedStringArray(scope.regionPath) &&
isAlreadyNormalizedStringArray(scope.regionSecondary)
);
}
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 = {}) {
if (canReuseNormalizedMemoryScope(scope, defaults)) {
return scope;
}
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 regionPrimaryTokens = normalizeScopeRegionList(merged.regionPrimary, {
allowSlash: true,
});
let regionPath = normalizeScopeRegionList(merged.regionPath, {
allowSlash: true,
});
let regionSecondary = normalizeScopeRegionList(merged.regionSecondary, {
allowSlash: true,
});
if (regionPath.length === 0 && regionPrimaryTokens.length > 1) {
regionPath = [...regionPrimaryTokens];
}
let regionPrimary = regionPrimaryTokens[regionPrimaryTokens.length - 1] || "";
if (!regionPrimary && regionPath.length > 0) {
regionPrimary = regionPath[regionPath.length - 1] || "";
}
if (regionPrimary && regionPath.length > 0) {
regionPath = appendUniqueTokenToPath(regionPath, regionPrimary);
}
if (regionPrimary) {
const regionPrimaryKey = normalizeKey(regionPrimary);
regionSecondary = regionSecondary.filter(
(value) => normalizeKey(value) !== regionPrimaryKey,
);
}
if (regionPath.length > 0) {
const regionPathKeys = new Set(regionPath.map((value) => normalizeKey(value)));
regionSecondary = regionSecondary.filter(
(value) => !regionPathKeys.has(normalizeKey(value)),
);
}
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);
const regionPath = normalizeStringArray(normalized.regionPath);
const regionSecondary = normalizeStringArray(normalized.regionSecondary);
return normalizeStringArray([
normalized.regionPrimary,
...regionPath,
...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 hasMeaningfulMemoryScope(scope) {
const normalized = normalizeMemoryScope(scope);
return (
normalized.layer === MEMORY_SCOPE_LAYER.POV ||
Boolean(normalized.ownerType || normalized.ownerId || normalized.ownerName) ||
Boolean(normalized.regionPrimary) ||
(Array.isArray(normalized.regionPath) && normalized.regionPath.length > 0) ||
(Array.isArray(normalized.regionSecondary) &&
normalized.regionSecondary.length > 0)
);
}
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 regionPath = normalizeStringArray(normalized.regionPath);
const regionSecondary = normalizeStringArray(normalized.regionSecondary);
const parts = [];
if (normalized.regionPrimary) {
parts.push(`主地区: ${normalized.regionPrimary}`);
}
if (regionPath.length > 0) {
parts.push(`地区路径: ${regionPath.join(" / ")}`);
}
if (regionSecondary.length > 0) {
parts.push(`次级地区: ${regionSecondary.join(", ")}`);
}
return parts.join(" | ");
}