mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
389 lines
12 KiB
JavaScript
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(" | ");
|
|
}
|