Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

177 lines
5.1 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.

/**
* M-PALACE · 人格分析引擎
* 实时分析玩家每一次输入,识别并量化人格侧面
*
* 分析流程:
* ① 关键词匹配:扫描 persona-dict 的 keywords
* ② 行为信号匹配:判断选项类型对应的 behavior_signals
* ③ 上下文推断:结合对话历史推断语气/态度/策略倾向
* → 输出 8 维人格侧面分数0~100
*/
const fs = require('fs');
const path = require('path');
const DICT_PATH = path.join(__dirname, '..', '..', 'data', 'persona-dict.json');
let _dictCache = null;
function loadDict() {
if (_dictCache) return _dictCache;
_dictCache = JSON.parse(fs.readFileSync(DICT_PATH, 'utf-8'));
return _dictCache;
}
/**
* 生成初始人格分数(基于身份锚点)
*/
function getInitialScores(role) {
const base = {
ambition: 50, warmth: 50, aggression: 50, suspicion: 50,
vanity: 50, cunning: 50, loyalty: 50, fear: 50
};
const adjustments = {
'皇帝': { ambition: 20, vanity: 15 },
'妃子': { warmth: 15, fear: 10 },
'重臣': { loyalty: 20, cunning: 10 },
'奸臣': { cunning: 25, aggression: 15 }
};
const adj = adjustments[role] || {};
for (const [k, v] of Object.entries(adj)) {
base[k] = Math.min(100, base[k] + v);
}
return base;
}
/**
* 关键词匹配:在输入文本中扫描 persona-dict keywords
* @returns {{ [dimensionId]: number }} 命中次数 map
*/
function matchKeywords(text) {
const dict = loadDict();
const hits = {};
for (const dim of dict.dimensions) {
hits[dim.id] = 0;
for (const kw of dim.keywords) {
if (text.includes(kw)) hits[dim.id]++;
}
}
return hits;
}
/**
* 行为信号匹配:将选项 index 映射到行为信号
* 选项设计规则A=最强侧面舒适区B=最弱侧面成长区C=中性
* @param {number|null} choiceIndex 0/1/2 or null(自由输入)
* @param {object} optionMeta 后端为本次选项附加的元数据 { a_dims, b_dims, c_dims }
*/
function matchBehavior(choiceIndex, optionMeta) {
if (choiceIndex === null || !optionMeta) return {};
const key = ['a_dims', 'b_dims', 'c_dims'][choiceIndex];
const dims = (optionMeta && optionMeta[key]) || [];
const hits = {};
for (const d of dims) {
hits[d] = (hits[d] || 0) + 1;
}
return hits;
}
/**
* 合并关键词 + 行为信号 → 更新人格分数
*/
function updateScores(currentScores, keywordHits, behaviorHits) {
const updated = { ...currentScores };
const KEYWORD_WEIGHT = 3;
const BEHAVIOR_WEIGHT = 5;
for (const dim of Object.keys(updated)) {
let delta = 0;
if (keywordHits[dim]) delta += keywordHits[dim] * KEYWORD_WEIGHT;
if (behaviorHits[dim]) delta += behaviorHits[dim] * BEHAVIOR_WEIGHT;
updated[dim] = Math.max(0, Math.min(100, updated[dim] + delta));
}
return updated;
}
/**
* 找到当前最突出特征
*/
function getDominantTrait(scores) {
let maxDim = null;
let maxVal = -1;
for (const [k, v] of Object.entries(scores)) {
if (v > maxVal) { maxVal = v; maxDim = k; }
}
return maxDim;
}
/**
* 检测分数突变(单次 +15 以上)
*/
function detectSurge(oldScores, newScores) {
const surges = [];
for (const dim of Object.keys(newScores)) {
const delta = newScores[dim] - (oldScores[dim] || 50);
if (delta >= 15) surges.push({ dim, delta });
}
return surges;
}
/**
* 检测交叉升高(两个维度同时上升 10+
*/
function detectCrossRise(oldScores, newScores) {
const rising = [];
for (const dim of Object.keys(newScores)) {
const delta = newScores[dim] - (oldScores[dim] || 50);
if (delta >= 10) rising.push(dim);
}
return rising.length >= 2 ? rising : [];
}
/**
* 主分析入口
* @param {string} text 玩家输入文本
* @param {number|null} choiceIndex 选项序号null = 自由输入)
* @param {object} optionMeta 选项元数据
* @param {object} currentScores 当前人格分数
* @returns {{ scores, dominant, surges, crossRise }}
*/
function analyze(text, choiceIndex, optionMeta, currentScores) {
const kwHits = text ? matchKeywords(text) : {};
const bhHits = matchBehavior(choiceIndex, optionMeta);
const oldScores = { ...currentScores };
const newScores = updateScores(currentScores, kwHits, bhHits);
const dominant = getDominantTrait(newScores);
const surges = detectSurge(oldScores, newScores);
const crossRise = detectCrossRise(oldScores, newScores);
return { scores: newScores, dominant, surges, crossRise };
}
/**
* 将 8 维人格分数转换为前端四维状态栏数值
* 权力值 = avg(ambition, cunning)
* 地位值 = avg(vanity, loyalty)
* 情感值 = avg(warmth, fear反转)
* 冲突值 = avg(aggression, suspicion)
*/
function toFourDimensions(scores) {
return {
power: Math.round((scores.ambition + scores.cunning) / 2),
status: Math.round((scores.vanity + scores.loyalty) / 2),
emotion: Math.round((scores.warmth + (100 - scores.fear)) / 2),
conflict: Math.round((scores.aggression + scores.suspicion) / 2)
};
}
module.exports = {
getInitialScores,
analyze,
getDominantTrait,
toFourDimensions,
matchKeywords,
detectSurge,
detectCrossRise
};