From b92a8d4cb171dd25ec103221c52054a7d9c0ab28 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Mar 2026 12:41:33 +0000
Subject: [PATCH 1/3] Initial plan
From 331b50268b4021e89ef8177278a1d2fc6cdc8853 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Mar 2026 12:52:31 +0000
Subject: [PATCH 2/3] feat(M-PALACE): create complete palace-game module
structure
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Data layer: palace-db.json, persona-dict.json, world templates (古代中国 + 架空王朝)
- Backend engines: persona-analyzer, background-gen, plot-engine, save-manager
- Backend routes: start.js, interact.js, save.js
- Backend server: Express on port 3003
- Frontend: index.html, game.html, save.html, game.js, save.js, style.css
- .gitignore: exclude save data files
Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
---
.gitignore | 3 +
.../backend/engines/background-gen.js | 238 ++++++++
.../backend/engines/persona-analyzer.js | 176 ++++++
.../backend/engines/plot-engine.js | 311 ++++++++++
.../backend/engines/save-manager.js | 190 ++++++
.../palace-game/backend/routes/interact.js | 84 +++
modules/palace-game/backend/routes/save.js | 113 ++++
modules/palace-game/backend/routes/start.js | 94 +++
modules/palace-game/backend/server.js | 59 ++
modules/palace-game/data/palace-db.json | 19 +
modules/palace-game/data/persona-dict.json | 52 ++
modules/palace-game/data/saves/.gitkeep | 0
.../data/templates/ancient-china.json | 40 ++
.../data/templates/fantasy-dynasty.json | 40 ++
modules/palace-game/frontend/game.html | 85 +++
modules/palace-game/frontend/game.js | 311 ++++++++++
modules/palace-game/frontend/index.html | 123 ++++
modules/palace-game/frontend/save.html | 50 ++
modules/palace-game/frontend/save.js | 139 +++++
modules/palace-game/frontend/style.css | 564 ++++++++++++++++++
20 files changed, 2691 insertions(+)
create mode 100644 modules/palace-game/backend/engines/background-gen.js
create mode 100644 modules/palace-game/backend/engines/persona-analyzer.js
create mode 100644 modules/palace-game/backend/engines/plot-engine.js
create mode 100644 modules/palace-game/backend/engines/save-manager.js
create mode 100644 modules/palace-game/backend/routes/interact.js
create mode 100644 modules/palace-game/backend/routes/save.js
create mode 100644 modules/palace-game/backend/routes/start.js
create mode 100644 modules/palace-game/backend/server.js
create mode 100644 modules/palace-game/data/palace-db.json
create mode 100644 modules/palace-game/data/persona-dict.json
create mode 100644 modules/palace-game/data/saves/.gitkeep
create mode 100644 modules/palace-game/data/templates/ancient-china.json
create mode 100644 modules/palace-game/data/templates/fantasy-dynasty.json
create mode 100644 modules/palace-game/frontend/game.html
create mode 100644 modules/palace-game/frontend/game.js
create mode 100644 modules/palace-game/frontend/index.html
create mode 100644 modules/palace-game/frontend/save.html
create mode 100644 modules/palace-game/frontend/save.js
create mode 100644 modules/palace-game/frontend/style.css
diff --git a/.gitignore b/.gitignore
index 7285a62b..97624956 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,6 @@ persona-brain-db/brain.db-shm
# persona-studio runtime artifacts
persona-studio/backend/brain/model-benchmark.json
+
+# palace-game runtime artifacts
+modules/palace-game/data/saves/PAL-*
diff --git a/modules/palace-game/backend/engines/background-gen.js b/modules/palace-game/backend/engines/background-gen.js
new file mode 100644
index 00000000..e03e9f9f
--- /dev/null
+++ b/modules/palace-game/backend/engines/background-gen.js
@@ -0,0 +1,238 @@
+/**
+ * M-PALACE · 背景生成引擎
+ * 接收锚点答案 + 人格侧面初始数据
+ * → 从宫廷数据库匹配
+ * → 生成逻辑自洽的专属背景
+ *
+ * 生成流程:
+ * 锚点1(世界观)→ 选择 templates/ 基础模板
+ * 锚点2(身份) → 确定主角定位 + 初始人格快照
+ * → 查询 palace-db 四大维度 → 调用模型 → 输出世界状态
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+const DATA_DIR = path.join(__dirname, '..', '..', 'data');
+const TEMPLATES_DIR = path.join(DATA_DIR, 'templates');
+const PALACE_DB_PATH = path.join(DATA_DIR, 'palace-db.json');
+
+let _palaceDB = null;
+
+function loadPalaceDB() {
+ if (_palaceDB) return _palaceDB;
+ _palaceDB = JSON.parse(fs.readFileSync(PALACE_DB_PATH, 'utf-8'));
+ return _palaceDB;
+}
+
+function loadTemplate(worldview) {
+ const map = {
+ 'ancient-china': 'ancient-china.json',
+ '古代中国': 'ancient-china.json',
+ 'fantasy': 'fantasy-dynasty.json',
+ '架空王朝': 'fantasy-dynasty.json'
+ };
+ const file = map[worldview] || 'ancient-china.json';
+ return JSON.parse(fs.readFileSync(path.join(TEMPLATES_DIR, file), 'utf-8'));
+}
+
+/**
+ * 从模板中随机选取一项
+ */
+function pick(arr) {
+ return arr[Math.floor(Math.random() * arr.length)];
+}
+
+/**
+ * 基于人格侧面决定 NPC 映射
+ * 核心机密:每个 NPC 对应玩家 1~2 个人格侧面的外化
+ */
+function generateNPCMappings(scores) {
+ const npcs = [];
+ const dims = Object.entries(scores).sort((a, b) => b[1] - a[1]);
+
+ // 最强维度 → 镜像对手
+ const strongest = dims[0];
+ npcs.push({
+ archetype: 'mirror',
+ mapped_dims: [strongest[0]],
+ role_hint: 'rival',
+ description: '与你最相似的对手'
+ });
+
+ // 最弱维度 → 放大威胁
+ const weakest = dims[dims.length - 1];
+ npcs.push({
+ archetype: 'shadow',
+ mapped_dims: [weakest[0]],
+ role_hint: 'threat',
+ description: '让你恐惧的存在'
+ });
+
+ // 温情相关 → 情感锚点
+ if (scores.warmth >= 40) {
+ npcs.push({
+ archetype: 'anchor',
+ mapped_dims: ['warmth', 'loyalty'],
+ role_hint: 'ally',
+ description: '需要被保护的弱势角色'
+ });
+ }
+
+ // 多疑相关 → 暧昧盟友
+ if (scores.suspicion >= 45) {
+ npcs.push({
+ archetype: 'trickster',
+ mapped_dims: ['suspicion', 'cunning'],
+ role_hint: 'ambiguous',
+ description: '行为暧昧的盟友'
+ });
+ }
+
+ // 确保至少 3 个 NPC
+ if (npcs.length < 3) {
+ npcs.push({
+ archetype: 'catalyst',
+ mapped_dims: [dims[2][0]],
+ role_hint: 'neutral',
+ description: '推动局势变化的中立角色'
+ });
+ }
+
+ return npcs;
+}
+
+/**
+ * 基于人格分数查询 palace-db,确定初始格局
+ */
+function queryInitialSetup(scores) {
+ const db = loadPalaceDB();
+
+ // 权力维度:基于 ambition+cunning
+ const powerLevel = (scores.ambition + scores.cunning) / 2;
+ const factionCount = powerLevel > 60 ? 3 : 2;
+
+ // 后宫地位:基于 vanity+warmth
+ const statusLevel = (scores.vanity + scores.warmth) / 2;
+
+ // 情感关系:基于 warmth+loyalty
+ const emotionIntensity = (scores.warmth + scores.loyalty) / 2;
+
+ // 矛盾冲突:基于 aggression+suspicion
+ const conflictLevel = (scores.aggression + scores.suspicion) / 2;
+ const conflictType = conflictLevel > 60 ? '争宠' : '自保';
+
+ return {
+ factions: db.power.factions.slice(0, factionCount),
+ initialEvent: pick(db.power.events),
+ startingRank: db.status.ranks[Math.min(Math.floor(statusLevel / 15), db.status.ranks.length - 1)],
+ emotionType: emotionIntensity > 60 ? pick(db.emotion.types.slice(0, 3)) : pick(db.emotion.types.slice(3)),
+ emotionTrigger: pick(db.emotion.triggers),
+ conflictType: conflictType,
+ escalation: db.conflict.escalation[0]
+ };
+}
+
+/**
+ * 构建世界生成 prompt(供模型调用)
+ */
+function buildWorldPrompt(template, role, setup, npcMappings) {
+ const dynasty = template.setting.dynasty;
+ const era = pick(template.setting.era_prefix);
+ const roleConfig = template.roles[role] || template.roles['妃子'];
+ const title = pick(roleConfig.title_options);
+
+ const npcDescriptions = npcMappings.map((npc, i) => {
+ return `NPC${i + 1}(${npc.role_hint}型):${npc.description}`;
+ }).join('\n');
+
+ return {
+ system: [
+ '你是一个古风宫斗叙事生成器。',
+ '要求:文笔古风典雅,有呼吸感,不要白话流水账。',
+ '使用通感手法,注重氛围营造。',
+ '所有NPC的存在都有隐藏的人格映射逻辑,但不能向玩家揭示。'
+ ].join('\n'),
+ user: [
+ `请为以下设定生成宫廷世界开篇:`,
+ `王朝:${dynasty}`,
+ `年号:${era}`,
+ `玩家身份:${role}(${title})`,
+ `初始阵营格局:${setup.factions.join('、')}`,
+ `开局事件:${setup.initialEvent}`,
+ `情感基调:${setup.emotionType}`,
+ `冲突类型:${setup.conflictType}`,
+ ``,
+ `需要生成的NPC:`,
+ npcDescriptions,
+ ``,
+ `输出格式(JSON):`,
+ `{`,
+ ` "dynasty_name": "王朝名",`,
+ ` "era_name": "年号",`,
+ ` "background": "一段时代背景描述(古风文笔·100-200字)",`,
+ ` "player_intro": "主角身世与处境(古风文笔·100-200字)",`,
+ ` "npcs": [{ "name": "角色名", "title": "头衔", "personality": "性格", "relation_to_player": "与主角关系", "hidden_archetype": "mirror|shadow|anchor|trickster|catalyst" }],`,
+ ` "opening_event": "开局事件描述(古风文笔·150-300字)",`,
+ ` "opening_choices": ["选项A", "选项B", "选项C"]`,
+ `}`
+ ].join('\n'),
+ meta: { dynasty, era, title, role }
+ };
+}
+
+/**
+ * 主入口:生成完整背景(不依赖外部模型时,使用本地模板拼接)
+ */
+function generate(worldview, role, personaScores) {
+ const template = loadTemplate(worldview);
+ const setup = queryInitialSetup(personaScores);
+ const npcMappings = generateNPCMappings(personaScores);
+ const prompt = buildWorldPrompt(template, role, setup, npcMappings);
+
+ // 构造本地 fallback 世界(不依赖外部模型时使用)
+ const dynasty = prompt.meta.dynasty;
+ const era = prompt.meta.era;
+ const roleConfig = template.roles[role] || template.roles['妃子'];
+ const title = prompt.meta.title;
+
+ const localWorld = {
+ dynasty_name: dynasty,
+ era_name: era,
+ background: `${dynasty}${era}年间,朝堂暗流涌动。${setup.factions.join('与')}明争暗斗,帝位之下,人心叵测。宫墙之内,灯火通明处未必安宁,暗影幢幢处未必无情。`,
+ player_intro: `你是${dynasty}${title},${pick(roleConfig.family_options || ['名门'])}出身。入宫数载,${setup.conflictType === '争宠' ? '虽有圣眷,却树敌颇多' : '小心翼翼,只求自保平安'}。${setup.emotionType}的暗线,早已悄然铺开。`,
+ npcs: npcMappings.map(function (npc, i) {
+ const names = ['沈婉仪', '赵昭仪', '陈太傅', '李将军', '刘公公'];
+ return {
+ name: names[i] || '无名氏',
+ title: pick(template.setting.era_prefix) + '年间人物',
+ personality: npc.description,
+ relation_to_player: npc.role_hint,
+ hidden_archetype: npc.archetype
+ };
+ }),
+ opening_event: `夜半三更,${template.setting.inner_palace}传来急召。太后身边的掌事姑姑面色如常,语气却带着不容拒绝的冷意。你知道,这一趟,去与不去,都是棋局的一部分。`,
+ opening_choices: [
+ '立刻更衣前往,恭顺以对',
+ '称身体不适,推迟半个时辰',
+ '先派人去打听太后的意图'
+ ]
+ };
+
+ return {
+ world: localWorld,
+ npcMappings: npcMappings,
+ setup: setup,
+ prompt: prompt,
+ template: template
+ };
+}
+
+module.exports = {
+ generate,
+ loadTemplate,
+ loadPalaceDB,
+ generateNPCMappings,
+ queryInitialSetup,
+ buildWorldPrompt
+};
diff --git a/modules/palace-game/backend/engines/persona-analyzer.js b/modules/palace-game/backend/engines/persona-analyzer.js
new file mode 100644
index 00000000..593070d7
--- /dev/null
+++ b/modules/palace-game/backend/engines/persona-analyzer.js
@@ -0,0 +1,176 @@
+/**
+ * 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
+};
diff --git a/modules/palace-game/backend/engines/plot-engine.js b/modules/palace-game/backend/engines/plot-engine.js
new file mode 100644
index 00000000..d8cf2e75
--- /dev/null
+++ b/modules/palace-game/backend/engines/plot-engine.js
@@ -0,0 +1,311 @@
+/**
+ * M-PALACE · 角色情节引擎
+ * 根据持续更新的人格侧面数据,在专属背景下动态生成宫斗剧情
+ *
+ * 生成逻辑:
+ * ① 人格分析引擎更新侧面分数
+ * ② 检测分数变化幅度 → 决定事件类型
+ * ③ 查询 palace-db 匹配剧情模板
+ * ④ 综合 state+persona+history 生成叙事+选项
+ * ⑤ 更新 state + history → 返回前端
+ */
+
+const fs = require('fs');
+const path = require('path');
+const personaAnalyzer = require('./persona-analyzer');
+
+const DATA_DIR = path.join(__dirname, '..', '..', 'data');
+const PALACE_DB_PATH = path.join(DATA_DIR, 'palace-db.json');
+
+let _palaceDB = null;
+
+function loadPalaceDB() {
+ if (_palaceDB) return _palaceDB;
+ _palaceDB = JSON.parse(fs.readFileSync(PALACE_DB_PATH, 'utf-8'));
+ return _palaceDB;
+}
+
+/**
+ * 确定事件类型
+ * 突变(+15) → 关键事件
+ * 交叉升高 → 复合剧情线
+ * 平稳 → 主线叙事
+ */
+function determineEventType(surges, crossRise) {
+ if (surges.length > 0) return 'critical';
+ if (crossRise.length >= 2) return 'compound';
+ return 'mainline';
+}
+
+/**
+ * 从 palace-db 匹配适合当前状态的剧情模板
+ */
+function matchPlotTemplate(eventType, scores, chapter) {
+ const db = loadPalaceDB();
+ const dominant = personaAnalyzer.getDominantTrait(scores);
+
+ if (eventType === 'critical') {
+ // 关键事件:根据突变维度匹配冲突类型
+ const conflictMap = {
+ ambition: '夺权', aggression: '报仇', suspicion: '揭秘',
+ warmth: '救人', fear: '自保', cunning: '争宠',
+ loyalty: '救人', vanity: '争宠'
+ };
+ return {
+ conflict: conflictMap[dominant] || '自保',
+ escalation: db.conflict.escalation[Math.min(chapter, db.conflict.escalation.length - 1)],
+ emotionTrigger: pick(db.emotion.triggers),
+ statusEvent: pick(db.status.events)
+ };
+ }
+
+ if (eventType === 'compound') {
+ return {
+ conflict: pick(db.conflict.types),
+ escalation: db.conflict.escalation[Math.min(Math.floor(chapter / 2), db.conflict.escalation.length - 1)],
+ emotionTrigger: pick(db.emotion.triggers),
+ statusEvent: null
+ };
+ }
+
+ // mainline
+ return {
+ conflict: null,
+ escalation: db.conflict.escalation[0],
+ emotionTrigger: null,
+ statusEvent: null
+ };
+}
+
+/**
+ * 生成选项:A=顺应最强侧面, B=挑战最弱侧面, C=中性探索
+ */
+function generateChoicesMeta(scores) {
+ const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
+ const strongest = sorted[0][0];
+ const weakest = sorted[sorted.length - 1][0];
+ const mid = sorted[Math.floor(sorted.length / 2)][0];
+
+ return {
+ a_dims: [strongest],
+ b_dims: [weakest],
+ c_dims: [mid],
+ strategy: {
+ a: 'comfort', // 舒适区
+ b: 'growth', // 成长区
+ c: 'explore' // 探索区
+ }
+ };
+}
+
+/**
+ * 构建情节生成 prompt(供模型调用)
+ */
+function buildPlotPrompt(state, persona, history, plotTemplate, choicesMeta) {
+ const chapterNum = (state.chapter || 0) + 1;
+ const paragraphNum = (state.paragraph || 0) + 1;
+ const recentChoices = (history.choices || []).slice(-3);
+
+ const sorted = Object.entries(persona.current_scores).sort((a, b) => b[1] - a[1]);
+ const strongest = sorted[0];
+ const weakest = sorted[sorted.length - 1];
+
+ return {
+ system: [
+ '你是一个古风宫斗叙事引擎。',
+ '文笔要求:古风典雅·有呼吸感·注重氛围·善用通感。',
+ '每段叙事100-250字,节奏不急不徐。',
+ '选项设计:A=顺应玩家当前最强侧面,B=挑战最弱侧面,C=中性/意外方向。',
+ '永远不要暗示或揭示人格映射机制。'
+ ].join('\n'),
+ user: [
+ `当前状态:第${chapterNum}章·第${paragraphNum}段`,
+ `世界:${state.world ? state.world.dynasty_name + '·' + state.world.era_name : '未知王朝'}`,
+ `玩家身份:${state.player ? state.player.role : '未知'}`,
+ ``,
+ `NPC状态:${JSON.stringify((state.npcs || []).map(function (n) { return n.name + '(' + n.relation_to_player + ')'; }))}`,
+ ``,
+ `剧情类型:${plotTemplate.conflict || '日常推进'}`,
+ `升级阶段:${plotTemplate.escalation}`,
+ plotTemplate.emotionTrigger ? `情感触发:${plotTemplate.emotionTrigger}` : '',
+ plotTemplate.statusEvent ? `地位事件:${plotTemplate.statusEvent}` : '',
+ ``,
+ `玩家最近选择:${recentChoices.map(function (c) { return c.text; }).join(' → ') || '无'}`,
+ `玩家最强倾向:${strongest[0]}(${strongest[1]})`,
+ `玩家最弱倾向:${weakest[0]}(${weakest[1]})`,
+ ``,
+ `请生成下一段叙事和选项(JSON):`,
+ `{`,
+ ` "chapter_title": "第X章·标题",`,
+ ` "narrative": "叙事文本(古风文笔·100-250字)",`,
+ ` "choices": ["选项A(顺应${strongest[0]})", "选项B(挑战${weakest[0]})", "选项C(中性探索)"],`,
+ ` "dimension_changes": { "power": 0, "status": 0, "emotion": 0, "conflict": 0 }`,
+ `}`
+ ].filter(Boolean).join('\n'),
+ meta: { chapterNum, paragraphNum, choicesMeta }
+ };
+}
+
+/**
+ * 本地生成叙事 fallback(不依赖外部模型时使用)
+ */
+function generateLocalNarrative(state, persona, plotTemplate, choicesMeta) {
+ const db = loadPalaceDB();
+ const chapterNum = (state.chapter || 0) + 1;
+ const paragraphNum = (state.paragraph || 0) + 1;
+ const sorted = Object.entries(persona.current_scores).sort((a, b) => b[1] - a[1]);
+ const strongest = sorted[0][0];
+ const weakest = sorted[sorted.length - 1][0];
+
+ const locations = ['长乐宫', '御花园', '太和殿', '冷宫回廊', '密道暗阁'];
+ const atmosphere = ['月色冷白', '烛火摇曳', '细雨如丝', '暮色四合', '晨曦微露'];
+ const loc = pick(locations);
+ const atm = pick(atmosphere);
+
+ const narrativeTemplates = {
+ critical: `${atm},${loc}内气氛凝重如铁。一封密信悄然送到你案前,字迹潦草,却句句惊心。你知道,有些事一旦知晓,便再无退路。窗外的风卷起帘角,像是催促,又像是警告。`,
+ compound: `${atm},你独坐${loc},心思百转。今日朝堂上的一幕反复在脑海中翻涌——那个眼神,那句话,似乎都别有深意。而你身边的人,究竟哪个是真心,哪个是棋子?`,
+ mainline: `${atm},${loc}一切如常。宫人们来去匆匆,各怀心事。你整理好衣冠,准备迎接新的一天。然而平静之下,暗流从未停歇。`
+ };
+
+ const dimLabels = {
+ ambition: '野心', warmth: '温情', aggression: '攻击', suspicion: '多疑',
+ vanity: '虚荣', cunning: '心机', loyalty: '忠义', fear: '恐惧'
+ };
+
+ const choiceTemplates = {
+ ambition: ['主动出击,争取主导权', '直面挑战,绝不退缩'],
+ warmth: ['以柔化刚,寻求和解', '默默守护,不求回报'],
+ aggression: ['果断反击,以牙还牙', '设下陷阱,一击致命'],
+ suspicion: ['暗中调查,不动声色', '保持距离,静观其变'],
+ vanity: ['展示实力,彰显地位', '争取荣耀,不甘人后'],
+ cunning: ['迂回布局,暗中谋划', '利用关系,借力打力'],
+ loyalty: ['坚守立场,绝不妥协', '为盟友挺身而出'],
+ fear: ['谨慎行事,避免冲突', '退一步,保全实力']
+ };
+
+ const choiceA = pick(choiceTemplates[strongest] || ['继续观望']);
+ const choiceB = pick(choiceTemplates[weakest] || ['另辟蹊径']);
+ const choiceC = '派人暗中打探消息,再做定夺';
+
+ const dims = personaAnalyzer.toFourDimensions(persona.current_scores);
+ const dimChanges = { power: 0, status: 0, emotion: 0, conflict: 0 };
+ if (plotTemplate.conflict) {
+ dimChanges.conflict = Math.floor(Math.random() * 5) + 1;
+ }
+ if (plotTemplate.statusEvent) {
+ dimChanges.status = Math.floor(Math.random() * 5) - 2;
+ }
+
+ return {
+ chapter_title: `第${chapterNum}章·${plotTemplate.conflict || '暗流'}`,
+ narrative: narrativeTemplates[determineEventType(
+ personaAnalyzer.detectSurge({}, persona.current_scores),
+ personaAnalyzer.detectCrossRise({}, persona.current_scores)
+ )] || narrativeTemplates.mainline,
+ choices: [choiceA, choiceB, choiceC],
+ dimension_changes: dimChanges,
+ meta: {
+ chapter: chapterNum,
+ paragraph: paragraphNum,
+ choices_meta: choicesMeta
+ }
+ };
+}
+
+/**
+ * 主入口:推进情节
+ * @param {object} state 当前世界状态
+ * @param {object} persona 玩家人格数据
+ * @param {object} history 剧情历史
+ * @param {string} playerInput 玩家输入
+ * @param {number|null} choiceIndex 选项序号
+ * @param {object} prevOptionMeta 上一轮选项元数据
+ */
+function advance(state, persona, history, playerInput, choiceIndex, prevOptionMeta) {
+ // ① 人格分析更新
+ const analysis = personaAnalyzer.analyze(
+ playerInput, choiceIndex, prevOptionMeta, persona.current_scores
+ );
+
+ // 更新 persona
+ const oldScores = { ...persona.current_scores };
+ persona.current_scores = analysis.scores;
+ persona.dominant_trait = analysis.dominant;
+ if (!persona.history) persona.history = [];
+ persona.history.push({
+ timestamp: new Date().toISOString(),
+ scores: { ...analysis.scores },
+ input: playerInput,
+ choice_index: choiceIndex
+ });
+
+ // ② 确定事件类型
+ const eventType = determineEventType(analysis.surges, analysis.crossRise);
+
+ // ③ 匹配剧情模板
+ const plotTemplate = matchPlotTemplate(eventType, analysis.scores, state.chapter || 0);
+
+ // ④ 生成选项元数据
+ const choicesMeta = generateChoicesMeta(analysis.scores);
+
+ // ⑤ 生成叙事(本地 fallback)
+ const result = generateLocalNarrative(state, persona, plotTemplate, choicesMeta);
+
+ // ⑥ 更新 state
+ state.chapter = result.meta.chapter;
+ state.paragraph = result.meta.paragraph;
+ state.four_dimensions = personaAnalyzer.toFourDimensions(analysis.scores);
+
+ // ⑦ 更新 history
+ if (!history.choices) history.choices = [];
+ history.choices.push({
+ text: playerInput || `选项${(choiceIndex || 0) + 1}`,
+ choice_index: choiceIndex,
+ timestamp: new Date().toISOString()
+ });
+ if (!history.chapters) history.chapters = [];
+ if (history.chapters.length < result.meta.chapter) {
+ history.chapters.push({
+ number: result.meta.chapter,
+ title: result.chapter_title,
+ started_at: new Date().toISOString()
+ });
+ }
+ if (eventType === 'critical') {
+ if (!history.events) history.events = [];
+ history.events.push({
+ type: eventType,
+ chapter: result.meta.chapter,
+ trigger: analysis.surges.map(function (s) { return s.dim; }).join('+'),
+ timestamp: new Date().toISOString()
+ });
+ }
+
+ // 构建 prompt 供可选的模型调用
+ const prompt = buildPlotPrompt(state, persona, history, plotTemplate, choicesMeta);
+
+ return {
+ narrative: result,
+ state: state,
+ persona: persona,
+ history: history,
+ four_dimensions: state.four_dimensions,
+ choices_meta: choicesMeta,
+ event_type: eventType,
+ prompt: prompt
+ };
+}
+
+function pick(arr) {
+ return arr[Math.floor(Math.random() * arr.length)];
+}
+
+module.exports = {
+ advance,
+ determineEventType,
+ matchPlotTemplate,
+ generateChoicesMeta,
+ buildPlotPrompt,
+ generateLocalNarrative
+};
diff --git a/modules/palace-game/backend/engines/save-manager.js b/modules/palace-game/backend/engines/save-manager.js
new file mode 100644
index 00000000..6c281185
--- /dev/null
+++ b/modules/palace-game/backend/engines/save-manager.js
@@ -0,0 +1,190 @@
+/**
+ * M-PALACE · 存档管理器
+ * 打包 / 加密 / 编号 / 解密 / 恢复
+ *
+ * 存档编号规则:PAL-{YYYYMMDD}-{随机4位}
+ * 存档结构:saves/{SAVE-ID}/state.json + persona.json + history.json
+ */
+
+const fs = require('fs');
+const path = require('path');
+const crypto = require('crypto');
+
+const SAVES_DIR = path.join(__dirname, '..', '..', 'data', 'saves');
+
+// AES-256-CBC 加密密钥(生产环境应从环境变量读取)
+const ENCRYPTION_KEY = process.env.PALACE_SAVE_KEY || 'palace-game-default-key-32ch!';
+const IV_LENGTH = 16;
+
+/**
+ * 确保存档目录存在
+ */
+function ensureSavesDir() {
+ if (!fs.existsSync(SAVES_DIR)) {
+ fs.mkdirSync(SAVES_DIR, { recursive: true });
+ }
+}
+
+/**
+ * 生成存档编号:PAL-{YYYYMMDD}-{随机4位字母数字}
+ */
+function generateSaveId() {
+ const now = new Date();
+ const dateStr = now.getFullYear().toString() +
+ String(now.getMonth() + 1).padStart(2, '0') +
+ String(now.getDate()).padStart(2, '0');
+ const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
+ let rand = '';
+ for (let i = 0; i < 4; i++) {
+ rand += chars[Math.floor(Math.random() * chars.length)];
+ }
+ return 'PAL-' + dateStr + '-' + rand;
+}
+
+/**
+ * 派生 32 字节密钥
+ */
+function deriveKey(passphrase) {
+ return crypto.createHash('sha256').update(passphrase).digest();
+}
+
+/**
+ * AES 加密
+ */
+function encrypt(text) {
+ const key = deriveKey(ENCRYPTION_KEY);
+ const iv = crypto.randomBytes(IV_LENGTH);
+ const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
+ let encrypted = cipher.update(text, 'utf-8', 'base64');
+ encrypted += cipher.final('base64');
+ return iv.toString('base64') + ':' + encrypted;
+}
+
+/**
+ * AES 解密
+ */
+function decrypt(encryptedText) {
+ const key = deriveKey(ENCRYPTION_KEY);
+ const parts = encryptedText.split(':');
+ const iv = Buffer.from(parts[0], 'base64');
+ const encrypted = parts.slice(1).join(':');
+ const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
+ let decrypted = decipher.update(encrypted, 'base64', 'utf-8');
+ decrypted += decipher.final('utf-8');
+ return decrypted;
+}
+
+/**
+ * 保存存档:将 state+persona+history 打包加密写入磁盘
+ * @returns {string} 存档编号
+ */
+function save(state, persona, history, existingSaveId) {
+ ensureSavesDir();
+
+ const saveId = existingSaveId || generateSaveId();
+ const saveDir = path.join(SAVES_DIR, saveId);
+
+ if (!fs.existsSync(saveDir)) {
+ fs.mkdirSync(saveDir, { recursive: true });
+ }
+
+ // 加密各文件内容后写入
+ const stateData = encrypt(JSON.stringify(state));
+ const personaData = encrypt(JSON.stringify(persona));
+ const historyData = encrypt(JSON.stringify(history));
+
+ fs.writeFileSync(path.join(saveDir, 'state.json'), stateData, 'utf-8');
+ fs.writeFileSync(path.join(saveDir, 'persona.json'), personaData, 'utf-8');
+ fs.writeFileSync(path.join(saveDir, 'history.json'), historyData, 'utf-8');
+
+ // 写入元数据(不加密,方便索引)
+ const meta = {
+ save_id: saveId,
+ created_at: new Date().toISOString(),
+ chapter: state.chapter || 0,
+ paragraph: state.paragraph || 0,
+ dynasty: state.world ? state.world.dynasty_name : '未知',
+ role: state.player ? state.player.role : '未知'
+ };
+ fs.writeFileSync(path.join(saveDir, 'meta.json'), JSON.stringify(meta, null, 2), 'utf-8');
+
+ return saveId;
+}
+
+/**
+ * 读取存档:解密并恢复完整状态
+ * @param {string} saveId 存档编号
+ * @returns {{ state, persona, history, meta }}
+ */
+function load(saveId) {
+ const saveDir = path.join(SAVES_DIR, saveId);
+
+ if (!fs.existsSync(saveDir)) {
+ return null;
+ }
+
+ const stateEncrypted = fs.readFileSync(path.join(saveDir, 'state.json'), 'utf-8');
+ const personaEncrypted = fs.readFileSync(path.join(saveDir, 'persona.json'), 'utf-8');
+ const historyEncrypted = fs.readFileSync(path.join(saveDir, 'history.json'), 'utf-8');
+
+ const state = JSON.parse(decrypt(stateEncrypted));
+ const persona = JSON.parse(decrypt(personaEncrypted));
+ const history = JSON.parse(decrypt(historyEncrypted));
+
+ let meta = null;
+ const metaPath = path.join(saveDir, 'meta.json');
+ if (fs.existsSync(metaPath)) {
+ meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
+ }
+
+ return { state, persona, history, meta };
+}
+
+/**
+ * 列出所有存档
+ */
+function listSaves() {
+ ensureSavesDir();
+ const dirs = fs.readdirSync(SAVES_DIR).filter(function (d) {
+ return d.startsWith('PAL-') && fs.statSync(path.join(SAVES_DIR, d)).isDirectory();
+ });
+ return dirs.map(function (d) {
+ const metaPath = path.join(SAVES_DIR, d, 'meta.json');
+ if (fs.existsSync(metaPath)) {
+ return JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
+ }
+ return { save_id: d };
+ });
+}
+
+/**
+ * 删除存档
+ */
+function deleteSave(saveId) {
+ const saveDir = path.join(SAVES_DIR, saveId);
+ if (!fs.existsSync(saveDir)) return false;
+ const files = fs.readdirSync(saveDir);
+ for (const f of files) {
+ fs.unlinkSync(path.join(saveDir, f));
+ }
+ fs.rmdirSync(saveDir);
+ return true;
+}
+
+/**
+ * 检查存档是否存在
+ */
+function exists(saveId) {
+ return fs.existsSync(path.join(SAVES_DIR, saveId));
+}
+
+module.exports = {
+ save,
+ load,
+ listSaves,
+ deleteSave,
+ exists,
+ generateSaveId,
+ encrypt,
+ decrypt
+};
diff --git a/modules/palace-game/backend/routes/interact.js b/modules/palace-game/backend/routes/interact.js
new file mode 100644
index 00000000..1c13fa3d
--- /dev/null
+++ b/modules/palace-game/backend/routes/interact.js
@@ -0,0 +1,84 @@
+/**
+ * M-PALACE · 玩家互动 API
+ * POST /api/palace/interact
+ *
+ * 接收玩家输入 → 人格分析 → 情节推进 → 返回新叙事+选项
+ */
+
+const express = require('express');
+const router = express.Router();
+const plotEngine = require('../engines/plot-engine');
+const saveManager = require('../engines/save-manager');
+
+/**
+ * POST /api/palace/interact
+ * body: {
+ * save_id: "PAL-XXXXXXXX-XXXX",
+ * input: "玩家输入文本" | null,
+ * choice_index: 0|1|2|null,
+ * option_meta: { a_dims, b_dims, c_dims } | null
+ * }
+ */
+router.post('/', function (req, res) {
+ try {
+ var body = req.body || {};
+ var saveId = body.save_id;
+ var playerInput = body.input || '';
+ var choiceIndex = body.choice_index != null ? body.choice_index : null;
+ var optionMeta = body.option_meta || null;
+
+ if (!saveId) {
+ return res.status(400).json({
+ error: true,
+ code: 'MISSING_SAVE_ID',
+ message: '缺少存档编号'
+ });
+ }
+
+ // 读取存档
+ var saveData = saveManager.load(saveId);
+ if (!saveData) {
+ return res.status(404).json({
+ error: true,
+ code: 'SAVE_NOT_FOUND',
+ message: '存档不存在:' + saveId
+ });
+ }
+
+ var state = saveData.state;
+ var persona = saveData.persona;
+ var history = saveData.history;
+
+ // 情节推进
+ var result = plotEngine.advance(
+ state, persona, history,
+ playerInput, choiceIndex, optionMeta
+ );
+
+ // 自动存档
+ saveManager.save(result.state, result.persona, result.history, saveId);
+
+ res.json({
+ error: false,
+ save_id: saveId,
+ narrative: {
+ chapter_title: result.narrative.chapter_title,
+ narrative: result.narrative.narrative,
+ choices: result.narrative.choices
+ },
+ four_dimensions: result.four_dimensions,
+ choices_meta: result.choices_meta,
+ event_type: result.event_type,
+ chapter: result.state.chapter,
+ paragraph: result.state.paragraph
+ });
+ } catch (err) {
+ res.status(500).json({
+ error: true,
+ code: 'INTERACT_ERROR',
+ message: '剧情推进失败:' + err.message
+ });
+ }
+});
+
+module.exports = router;
diff --git a/modules/palace-game/backend/routes/save.js b/modules/palace-game/backend/routes/save.js
new file mode 100644
index 00000000..94ebdc6e
--- /dev/null
+++ b/modules/palace-game/backend/routes/save.js
@@ -0,0 +1,113 @@
+/**
+ * M-PALACE · 存档/读档 API
+ * POST /api/palace/save → 保存当前状态
+ * POST /api/palace/load → 读取存档
+ * GET /api/palace/saves → 列出所有存档
+ */
+
+const express = require('express');
+const router = express.Router();
+const saveManager = require('../engines/save-manager');
+
+/**
+ * POST /api/palace/save
+ * body: { save_id, state, persona, history }
+ */
+router.post('/', function (req, res) {
+ try {
+ var body = req.body || {};
+ var saveId = body.save_id;
+ var state = body.state;
+ var persona = body.persona;
+ var history = body.history;
+
+ if (!state || !persona || !history) {
+ return res.status(400).json({
+ error: true,
+ code: 'MISSING_DATA',
+ message: '缺少存档数据'
+ });
+ }
+
+ var resultId = saveManager.save(state, persona, history, saveId);
+
+ res.json({
+ error: false,
+ save_id: resultId,
+ message: '存档成功',
+ saved_at: new Date().toISOString()
+ });
+ } catch (err) {
+ res.status(500).json({
+ error: true,
+ code: 'SAVE_ERROR',
+ message: '存档失败:' + err.message
+ });
+ }
+});
+
+/**
+ * POST /api/palace/load
+ * body: { save_id: "PAL-XXXXXXXX-XXXX" }
+ */
+router.post('/load', function (req, res) {
+ try {
+ var body = req.body || {};
+ var saveId = body.save_id;
+
+ if (!saveId) {
+ return res.status(400).json({
+ error: true,
+ code: 'MISSING_SAVE_ID',
+ message: '请输入存档编号'
+ });
+ }
+
+ var saveData = saveManager.load(saveId);
+
+ if (!saveData) {
+ return res.status(404).json({
+ error: true,
+ code: 'SAVE_NOT_FOUND',
+ message: '存档不存在:' + saveId
+ });
+ }
+
+ res.json({
+ error: false,
+ save_id: saveId,
+ state: saveData.state,
+ persona: saveData.persona,
+ history: saveData.history,
+ meta: saveData.meta
+ });
+ } catch (err) {
+ res.status(500).json({
+ error: true,
+ code: 'LOAD_ERROR',
+ message: '读档失败:' + err.message
+ });
+ }
+});
+
+/**
+ * GET /api/palace/saves
+ */
+router.get('/list', function (_req, res) {
+ try {
+ var saves = saveManager.listSaves();
+ res.json({
+ error: false,
+ saves: saves,
+ count: saves.length
+ });
+ } catch (err) {
+ res.status(500).json({
+ error: true,
+ code: 'LIST_ERROR',
+ message: '获取存档列表失败:' + err.message
+ });
+ }
+});
+
+module.exports = router;
diff --git a/modules/palace-game/backend/routes/start.js b/modules/palace-game/backend/routes/start.js
new file mode 100644
index 00000000..c5d2aa0d
--- /dev/null
+++ b/modules/palace-game/backend/routes/start.js
@@ -0,0 +1,94 @@
+/**
+ * M-PALACE · 快速启动 API
+ * POST /api/palace/start
+ *
+ * 接收世界观+身份锚点 → 生成初始人格快照 → 背景生成 → 返回世界状态
+ */
+
+const express = require('express');
+const router = express.Router();
+const personaAnalyzer = require('../engines/persona-analyzer');
+const backgroundGen = require('../engines/background-gen');
+const saveManager = require('../engines/save-manager');
+
+/**
+ * POST /api/palace/start
+ * body: { worldview: "古代中国"|"架空王朝", role: "妃子"|"皇帝"|"重臣"|"奸臣"|"随机" }
+ */
+router.post('/', function (req, res) {
+ try {
+ var body = req.body || {};
+ var worldview = body.worldview || '古代中国';
+ var role = body.role || '随机';
+
+ // 随机角色
+ if (role === '随机') {
+ var roles = ['妃子', '皇帝', '重臣', '奸臣'];
+ role = roles[Math.floor(Math.random() * roles.length)];
+ }
+
+ // ① 基于身份锚点生成初始人格分数
+ var initialScores = personaAnalyzer.getInitialScores(role);
+
+ // ② 背景生成引擎
+ var bgResult = backgroundGen.generate(worldview, role, initialScores);
+
+ // ③ 构建初始状态
+ var state = {
+ world: bgResult.world,
+ npcs: bgResult.world.npcs || [],
+ player: {
+ role: role,
+ title: bgResult.world.player_intro,
+ worldview: worldview
+ },
+ chapter: 1,
+ paragraph: 1,
+ four_dimensions: personaAnalyzer.toFourDimensions(initialScores)
+ };
+
+ var persona = {
+ current_scores: initialScores,
+ history: [{
+ timestamp: new Date().toISOString(),
+ scores: { ...initialScores },
+ input: '__init__',
+ choice_index: null
+ }],
+ dominant_trait: personaAnalyzer.getDominantTrait(initialScores)
+ };
+
+ var history = {
+ chapters: [{
+ number: 1,
+ title: '第一章·' + (bgResult.world.dynasty_name || '序幕'),
+ started_at: new Date().toISOString()
+ }],
+ events: [],
+ choices: []
+ };
+
+ // ④ 自动存档
+ var saveId = saveManager.save(state, persona, history);
+
+ res.json({
+ error: false,
+ save_id: saveId,
+ state: state,
+ narrative: {
+ chapter_title: history.chapters[0].title,
+ narrative: bgResult.world.opening_event,
+ choices: bgResult.world.opening_choices
+ },
+ four_dimensions: state.four_dimensions
+ });
+ } catch (err) {
+ res.status(500).json({
+ error: true,
+ code: 'START_ERROR',
+ message: '宫廷世界生成失败:' + err.message
+ });
+ }
+});
+
+module.exports = router;
diff --git a/modules/palace-game/backend/server.js b/modules/palace-game/backend/server.js
new file mode 100644
index 00000000..15c96bfc
--- /dev/null
+++ b/modules/palace-game/backend/server.js
@@ -0,0 +1,59 @@
+/**
+ * M-PALACE · Express 主服务
+ * 动态人格宫廷生成系统 · 后端核心
+ *
+ * API 路由:
+ * POST /api/palace/start → 快速启动(生成世界)
+ * POST /api/palace/interact → 玩家互动(剧情推进)
+ * POST /api/palace/save → 存档
+ * POST /api/palace/load → 读档
+ * GET /api/palace/saves → 存档列表
+ * GET /api/palace/health → 健康检查
+ */
+
+const express = require('express');
+const cors = require('cors');
+const path = require('path');
+
+const startRoutes = require('./routes/start');
+const interactRoutes = require('./routes/interact');
+const saveRoutes = require('./routes/save');
+
+const app = express();
+const PORT = process.env.PALACE_PORT || 3003;
+
+// 中间件
+app.use(cors());
+app.use(express.json({ limit: '2mb' }));
+
+// 静态文件:前端
+app.use('/palace-game', express.static(path.join(__dirname, '..', 'frontend')));
+
+// API 路由
+app.use('/api/palace/start', startRoutes);
+app.use('/api/palace/interact', interactRoutes);
+app.use('/api/palace/save', saveRoutes);
+
+// 健康检查
+app.get('/api/palace/health', function (_req, res) {
+ res.json({
+ status: 'ok',
+ service: 'palace-game',
+ version: '1.0.0',
+ timestamp: new Date().toISOString()
+ });
+});
+
+// 根路由 → 前端入口
+app.get('/', function (_req, res) {
+ res.redirect('/palace-game/index.html');
+});
+
+// 启动服务
+if (require.main === module) {
+ app.listen(PORT, function () {
+ console.log('🏯 M-PALACE · 宫廷纪服务启动 · 端口 ' + PORT);
+ });
+}
+
+module.exports = app;
diff --git a/modules/palace-game/data/palace-db.json b/modules/palace-game/data/palace-db.json
new file mode 100644
index 00000000..9f4e8c24
--- /dev/null
+++ b/modules/palace-game/data/palace-db.json
@@ -0,0 +1,19 @@
+{
+ "power": {
+ "positions": ["皇帝", "太后", "皇后", "贵妃", "丞相", "大将军", "太监总管"],
+ "factions": ["太后党", "皇后党", "新贵派", "清流派", "孤臣"],
+ "events": ["朝堂弹劾", "密谋政变", "联姻结盟", "战事告急"]
+ },
+ "status": {
+ "ranks": ["答应", "常在", "贵人", "嫔", "妃", "贵妃", "皇贵妃", "皇后"],
+ "events": ["晋封", "降位", "禁足", "受宠", "失宠", "怀孕", "小产"]
+ },
+ "emotion": {
+ "types": ["暗恋", "深情", "嫉妒", "怨恨", "愧疚", "依赖", "背叛"],
+ "triggers": ["偶遇旧人", "收到密信", "目睹真相", "被迫抉择"]
+ },
+ "conflict": {
+ "types": ["争宠", "夺权", "报仇", "自保", "救人", "揭秘"],
+ "escalation": ["暗示", "试探", "交锋", "撕破脸", "鱼死网破"]
+ }
+}
diff --git a/modules/palace-game/data/persona-dict.json b/modules/palace-game/data/persona-dict.json
new file mode 100644
index 00000000..3caf09d2
--- /dev/null
+++ b/modules/palace-game/data/persona-dict.json
@@ -0,0 +1,52 @@
+{
+ "dimensions": [
+ {
+ "id": "ambition",
+ "name": "野心",
+ "keywords": ["权力", "登上", "掌控", "不甘", "凭什么"],
+ "behavior_signals": ["选择攻击性选项", "主动挑战权威", "拒绝退让"]
+ },
+ {
+ "id": "warmth",
+ "name": "温情",
+ "keywords": ["保护", "心疼", "陪伴", "不忍", "算了"],
+ "behavior_signals": ["选择和解", "保护弱势角色", "牺牲利益"]
+ },
+ {
+ "id": "aggression",
+ "name": "攻击性",
+ "keywords": ["杀", "除掉", "报复", "不放过", "代价"],
+ "behavior_signals": ["选择对抗", "设计陷阱", "直接冲突"]
+ },
+ {
+ "id": "suspicion",
+ "name": "多疑",
+ "keywords": ["试探", "不对劲", "背后", "真的吗", "骗"],
+ "behavior_signals": ["频繁试探", "不信任NPC", "选择观望"]
+ },
+ {
+ "id": "vanity",
+ "name": "虚荣",
+ "keywords": ["面子", "配得上", "最好的", "羡慕", "风光"],
+ "behavior_signals": ["追求地位象征", "在意他人评价", "选择炫耀"]
+ },
+ {
+ "id": "cunning",
+ "name": "心机",
+ "keywords": ["计划", "利用", "棋子", "布局", "暗中"],
+ "behavior_signals": ["迂回策略", "利用他人", "长线布局"]
+ },
+ {
+ "id": "loyalty",
+ "name": "忠义",
+ "keywords": ["信任", "誓言", "守护", "不背叛", "承诺"],
+ "behavior_signals": ["坚守阵营", "拒绝背叛", "为盟友冒险"]
+ },
+ {
+ "id": "fear",
+ "name": "恐惧",
+ "keywords": ["害怕", "不敢", "万一", "小心", "退"],
+ "behavior_signals": ["回避冲突", "选择保守", "犹豫不决"]
+ }
+ ]
+}
diff --git a/modules/palace-game/data/saves/.gitkeep b/modules/palace-game/data/saves/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/modules/palace-game/data/templates/ancient-china.json b/modules/palace-game/data/templates/ancient-china.json
new file mode 100644
index 00000000..cdce45c9
--- /dev/null
+++ b/modules/palace-game/data/templates/ancient-china.json
@@ -0,0 +1,40 @@
+{
+ "id": "ancient-china",
+ "name": "古代中国",
+ "description": "大周王朝·正统朝堂·后宫嫔妃·权臣博弈",
+ "setting": {
+ "dynasty": "大周",
+ "era_prefix": ["永安", "承平", "天启", "景和", "元丰"],
+ "capital": "长安",
+ "palace_name": "未央宫",
+ "inner_palace": "长乐宫"
+ },
+ "roles": {
+ "妃子": {
+ "title_options": ["淑妃", "德妃", "贤妃", "丽嫔", "婉嫔"],
+ "family_options": ["书香门第", "武将世家", "寒门出身", "没落贵族"],
+ "starting_rank": "嫔",
+ "intro_template": "你是{family}出身的{title},入宫{years}年。{situation}"
+ },
+ "皇帝": {
+ "title_options": ["新帝", "少年天子", "中兴之主"],
+ "situation_options": ["初登大宝·朝局未稳", "亲政在即·太后干政", "盛世之君·暗流涌动"],
+ "intro_template": "你是{dynasty}的{title},{situation}。"
+ },
+ "重臣": {
+ "title_options": ["丞相", "御史大夫", "太傅", "兵部尚书"],
+ "faction_options": ["清流派", "保皇派", "中立派"],
+ "intro_template": "你是{dynasty}{title},{faction}中流砥柱。{situation}"
+ },
+ "奸臣": {
+ "title_options": ["内阁首辅", "司礼监掌印", "锦衣卫指挥使"],
+ "ambition_options": ["权倾朝野", "挟天子以令诸侯", "暗中培植势力"],
+ "intro_template": "你是{title},表面忠良,实则{ambition}。"
+ }
+ },
+ "atmosphere": {
+ "time_of_day": ["寅时·天未亮", "辰时·早朝", "午时·日正中", "酉时·暮色", "亥时·深夜"],
+ "weather": ["细雨绵绵", "月朗星稀", "寒风凛冽", "春光明媚", "大雪纷飞"],
+ "locations": ["御书房", "太和殿", "长乐宫", "御花园", "冷宫", "密道", "城墙"]
+ }
+}
diff --git a/modules/palace-game/data/templates/fantasy-dynasty.json b/modules/palace-game/data/templates/fantasy-dynasty.json
new file mode 100644
index 00000000..818595bc
--- /dev/null
+++ b/modules/palace-game/data/templates/fantasy-dynasty.json
@@ -0,0 +1,40 @@
+{
+ "id": "fantasy-dynasty",
+ "name": "架空王朝",
+ "description": "九霄帝国·灵力与权谋交织·玄幻宫廷",
+ "setting": {
+ "dynasty": "九霄",
+ "era_prefix": ["灵元", "天衍", "混沌", "太初", "紫极"],
+ "capital": "天枢城",
+ "palace_name": "凌霄殿",
+ "inner_palace": "瑶光阁"
+ },
+ "roles": {
+ "妃子": {
+ "title_options": ["星妃", "月嫔", "霜华夫人", "云裳贵人"],
+ "family_options": ["上古灵族后裔", "剑修世家", "炼丹宗门", "凡人修仙"],
+ "starting_rank": "嫔",
+ "intro_template": "你是{family}出身的{title},入宫{years}年。{situation}"
+ },
+ "皇帝": {
+ "title_options": ["灵帝", "天选之主", "末代帝君"],
+ "situation_options": ["灵力觉醒·诸族觊觎", "仙魔大战后·百废待兴", "天命将尽·寻找继承"],
+ "intro_template": "你是{dynasty}的{title},{situation}。"
+ },
+ "重臣": {
+ "title_options": ["星辰阁大长老", "天机院主", "护国神将", "灵枢司正"],
+ "faction_options": ["仙道派", "人道派", "中立长老会"],
+ "intro_template": "你是{dynasty}{title},{faction}核心。{situation}"
+ },
+ "奸臣": {
+ "title_options": ["暗影阁主", "禁术研究者", "外域使者"],
+ "ambition_options": ["窃取帝国灵脉", "复活远古邪神", "暗中操控傀儡帝"],
+ "intro_template": "你是{title},表面忠良,实则{ambition}。"
+ }
+ },
+ "atmosphere": {
+ "time_of_day": ["灵潮初涌·黎明", "日中·灵力最盛", "黄昏·阴阳交替", "子夜·暗灵活跃"],
+ "weather": ["灵雨飘洒", "星河低垂", "雷灵暴动", "万里晴空·灵气充沛", "迷雾笼罩"],
+ "locations": ["灵枢大殿", "星象台", "瑶光阁", "灵兽园", "封印之地", "虚空裂缝", "万灵池"]
+ }
+}
diff --git a/modules/palace-game/frontend/game.html b/modules/palace-game/frontend/game.html
new file mode 100644
index 00000000..7ad7d294
--- /dev/null
+++ b/modules/palace-game/frontend/game.html
@@ -0,0 +1,85 @@
+
+
+
+
+
+ 宫廷纪 · 叙事
+
+
+
+
+
+
+
+
+
+
+
+
第一章 · 序幕
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 📖 第1章 · 第1段
+ 存档号: —
+
+
+
+
+
+
+
diff --git a/modules/palace-game/frontend/game.js b/modules/palace-game/frontend/game.js
new file mode 100644
index 00000000..3fb4ce0a
--- /dev/null
+++ b/modules/palace-game/frontend/game.js
@@ -0,0 +1,311 @@
+/**
+ * M-PALACE · 游戏交互逻辑
+ * 管理叙事渲染、选项交互、状态栏更新、存档操作
+ */
+
+(function () {
+ 'use strict';
+
+ var API_BASE = window.location.origin;
+ var saveId = sessionStorage.getItem('palace_save_id') || '';
+ var choicesMeta = null;
+ var isTyping = false;
+
+ // ---------- Initialization ----------
+ function init() {
+ var startData = sessionStorage.getItem('palace_state');
+ var loadedData = sessionStorage.getItem('palace_loaded');
+
+ if (startData) {
+ var data = JSON.parse(startData);
+ sessionStorage.removeItem('palace_state');
+ renderGameData(data);
+ } else if (loadedData) {
+ var loaded = JSON.parse(loadedData);
+ sessionStorage.removeItem('palace_loaded');
+ renderLoadedData(loaded);
+ } else if (saveId) {
+ loadSave(saveId);
+ } else {
+ window.location.href = 'index.html';
+ }
+
+ bindEvents();
+ }
+
+ // ---------- Rendering ----------
+ function renderGameData(data) {
+ saveId = data.save_id || saveId;
+ document.getElementById('save-id-display').textContent = '存档号: ' + saveId;
+
+ if (data.state && data.state.player) {
+ var p = data.state.player;
+ document.getElementById('top-title').textContent =
+ '🏯 宫廷纪 · ' + (p.worldview || '') + ' · ' + (p.role || '');
+ }
+
+ if (data.narrative) {
+ renderNarrative(data.narrative);
+ }
+
+ if (data.four_dimensions) {
+ updateStatusBar(data.four_dimensions);
+ }
+
+ updateProgressInfo(data.chapter || 1, data.paragraph || 1);
+ }
+
+ function renderLoadedData(data) {
+ document.getElementById('save-id-display').textContent = '存档号: ' + saveId;
+
+ if (data.state) {
+ var s = data.state;
+ if (s.player) {
+ document.getElementById('top-title').textContent =
+ '🏯 宫廷纪 · ' + (s.player.worldview || '') + ' · ' + (s.player.role || '');
+ }
+ if (s.four_dimensions) {
+ updateStatusBar(s.four_dimensions);
+ }
+ updateProgressInfo(s.chapter || 1, s.paragraph || 1);
+ }
+
+ // Render last narrative from history or show resume message
+ var narrativeEl = document.getElementById('narrative-text');
+ narrativeEl.textContent = '存档已恢复。继续你的宫廷故事……';
+ renderChoices(['继续前行', '回顾往事', '观察四周']);
+ }
+
+ function renderNarrative(narrative) {
+ if (narrative.chapter_title) {
+ document.getElementById('chapter-title').textContent = narrative.chapter_title;
+ }
+
+ var textEl = document.getElementById('narrative-text');
+ if (narrative.narrative) {
+ typewriter(textEl, narrative.narrative);
+ }
+
+ if (narrative.choices && narrative.choices.length > 0) {
+ // Delay choices until typewriter finishes
+ var delay = narrative.narrative ? narrative.narrative.length * 50 + 500 : 200;
+ setTimeout(function () {
+ renderChoices(narrative.choices);
+ }, delay);
+ }
+
+ choicesMeta = narrative.choices_meta || null;
+ }
+
+ function renderChoices(choices) {
+ var container = document.getElementById('choices-container');
+ container.innerHTML = '';
+
+ var nums = ['①', '②', '③'];
+ choices.forEach(function (text, i) {
+ var btn = document.createElement('button');
+ btn.className = 'choice-btn fade-in';
+ btn.innerHTML = '' + (nums[i] || '·') + '' + escapeHtml(text);
+ btn.addEventListener('click', function () {
+ submitChoice(i, text);
+ });
+ container.appendChild(btn);
+ });
+ }
+
+ // ---------- Typewriter Effect ----------
+ function typewriter(el, text) {
+ isTyping = true;
+ el.textContent = '';
+ var cursor = document.createElement('span');
+ cursor.className = 'typewriter-cursor';
+ el.appendChild(cursor);
+
+ var i = 0;
+ var interval = setInterval(function () {
+ if (i < text.length) {
+ el.insertBefore(document.createTextNode(text[i]), cursor);
+ i++;
+ } else {
+ clearInterval(interval);
+ if (cursor.parentNode) cursor.remove();
+ isTyping = false;
+ }
+ }, 45);
+ }
+
+ // ---------- Status Bar ----------
+ function updateStatusBar(dims) {
+ var keys = ['power', 'status', 'emotion', 'conflict'];
+ keys.forEach(function (k) {
+ var val = dims[k] != null ? dims[k] : 50;
+ var bar = document.getElementById('bar-' + k);
+ var valEl = document.getElementById('val-' + k);
+ if (bar) bar.style.width = val + '%';
+ if (valEl) valEl.textContent = val;
+ });
+ }
+
+ function updateProgressInfo(chapter, paragraph) {
+ var el = document.getElementById('progress-info');
+ if (el) el.textContent = '📖 第' + chapter + '章 · 第' + paragraph + '段';
+ }
+
+ // ---------- Interactions ----------
+ function submitChoice(index, text) {
+ if (isTyping) return;
+ disableChoices();
+
+ fetch(API_BASE + '/api/palace/interact', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ save_id: saveId,
+ input: text,
+ choice_index: index,
+ option_meta: choicesMeta
+ })
+ })
+ .then(function (r) { return r.json(); })
+ .then(function (data) {
+ if (data.error) {
+ showToast(data.message || '互动失败');
+ enableChoices();
+ return;
+ }
+ choicesMeta = data.choices_meta || null;
+ if (data.narrative) renderNarrative(data.narrative);
+ if (data.four_dimensions) updateStatusBar(data.four_dimensions);
+ updateProgressInfo(data.chapter || 1, data.paragraph || 1);
+ })
+ .catch(function (err) {
+ showToast('网络错误:' + err.message);
+ enableChoices();
+ });
+ }
+
+ function submitFreeInput() {
+ var input = document.getElementById('free-input');
+ var text = input.value.trim();
+ if (!text || isTyping) return;
+ input.value = '';
+ disableChoices();
+
+ fetch(API_BASE + '/api/palace/interact', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ save_id: saveId,
+ input: text,
+ choice_index: null,
+ option_meta: choicesMeta
+ })
+ })
+ .then(function (r) { return r.json(); })
+ .then(function (data) {
+ if (data.error) {
+ showToast(data.message || '互动失败');
+ enableChoices();
+ return;
+ }
+ choicesMeta = data.choices_meta || null;
+ if (data.narrative) renderNarrative(data.narrative);
+ if (data.four_dimensions) updateStatusBar(data.four_dimensions);
+ updateProgressInfo(data.chapter || 1, data.paragraph || 1);
+ })
+ .catch(function (err) {
+ showToast('网络错误:' + err.message);
+ enableChoices();
+ });
+ }
+
+ function disableChoices() {
+ var btns = document.querySelectorAll('.choice-btn');
+ btns.forEach(function (b) { b.disabled = true; b.style.opacity = '0.5'; });
+ }
+
+ function enableChoices() {
+ var btns = document.querySelectorAll('.choice-btn');
+ btns.forEach(function (b) { b.disabled = false; b.style.opacity = '1'; });
+ }
+
+ // ---------- Save / Load ----------
+ function doSave() {
+ fetch(API_BASE + '/api/palace/save', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ save_id: saveId, state: {}, persona: {}, history: {} })
+ })
+ .then(function (r) { return r.json(); })
+ .then(function (data) {
+ if (data.error) { showToast(data.message); return; }
+ showToast('存档成功:' + data.save_id);
+ })
+ .catch(function (err) {
+ showToast('存档失败:' + err.message);
+ });
+ }
+
+ function loadSave(id) {
+ fetch(API_BASE + '/api/palace/save/load', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ save_id: id })
+ })
+ .then(function (r) { return r.json(); })
+ .then(function (data) {
+ if (data.error) {
+ showToast(data.message || '读档失败');
+ window.location.href = 'index.html';
+ return;
+ }
+ renderLoadedData(data);
+ })
+ .catch(function (err) {
+ showToast('网络错误:' + err.message);
+ window.location.href = 'index.html';
+ });
+ }
+
+ // ---------- Events ----------
+ function bindEvents() {
+ document.getElementById('btn-save').addEventListener('click', doSave);
+
+ document.getElementById('btn-free-submit').addEventListener('click', submitFreeInput);
+
+ document.getElementById('free-input').addEventListener('keydown', function (e) {
+ if (e.key === 'Enter') submitFreeInput();
+ });
+
+ var toggle = document.getElementById('status-toggle');
+ var bar = document.getElementById('status-bar');
+ toggle.addEventListener('click', function () {
+ bar.classList.toggle('collapsed');
+ toggle.textContent = bar.classList.contains('collapsed') ? '▶ 状态栏' : '▼ 状态栏';
+ });
+ }
+
+ // ---------- Utilities ----------
+ function escapeHtml(str) {
+ var div = document.createElement('div');
+ div.appendChild(document.createTextNode(str));
+ return div.innerHTML;
+ }
+
+ function showToast(msg) {
+ var old = document.querySelector('.toast');
+ if (old) old.remove();
+ var el = document.createElement('div');
+ el.className = 'toast';
+ el.textContent = msg;
+ document.body.appendChild(el);
+ setTimeout(function () { if (el.parentNode) el.remove(); }, 3000);
+ }
+
+ // ---------- Boot ----------
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();
diff --git a/modules/palace-game/frontend/index.html b/modules/palace-game/frontend/index.html
new file mode 100644
index 00000000..e3969f3f
--- /dev/null
+++ b/modules/palace-game/frontend/index.html
@@ -0,0 +1,123 @@
+
+
+
+
+
+ 宫·廷·纪 — 动态人格宫廷生成系统
+
+
+
+
+
+
+
🏯
+
宫 · 廷 · 纪
+
动态人格宫廷生成系统
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
已有存档?输入编号继续
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/palace-game/frontend/save.html b/modules/palace-game/frontend/save.html
new file mode 100644
index 00000000..ac38470b
--- /dev/null
+++ b/modules/palace-game/frontend/save.html
@@ -0,0 +1,50 @@
+
+
+
+
+
+ 宫廷纪 · 存档管理
+
+
+
+
+
+
+
🏯 存档管理
+
+
+
+
+
+
+
+
+
+
已有存档
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/palace-game/frontend/save.js b/modules/palace-game/frontend/save.js
new file mode 100644
index 00000000..bc6551e0
--- /dev/null
+++ b/modules/palace-game/frontend/save.js
@@ -0,0 +1,139 @@
+/**
+ * M-PALACE · 存档读档逻辑
+ * 列出存档、读取存档、删除存档
+ */
+
+(function () {
+ 'use strict';
+
+ var API_BASE = window.location.origin;
+
+ function init() {
+ loadSaveList();
+ bindEvents();
+ }
+
+ // ---------- Load Save List ----------
+ function loadSaveList() {
+ var container = document.getElementById('save-list');
+
+ fetch(API_BASE + '/api/palace/save/list')
+ .then(function (r) { return r.json(); })
+ .then(function (data) {
+ if (data.error) {
+ container.innerHTML = '获取存档列表失败
';
+ return;
+ }
+ renderSaveList(data.saves || []);
+ })
+ .catch(function () {
+ container.innerHTML = '无法连接服务器
';
+ });
+ }
+
+ function renderSaveList(saves) {
+ var container = document.getElementById('save-list');
+
+ if (saves.length === 0) {
+ container.innerHTML = '暂无存档
';
+ return;
+ }
+
+ container.innerHTML = '';
+ saves.forEach(function (save) {
+ var item = document.createElement('div');
+ item.className = 'save-item fade-in';
+
+ var info = document.createElement('div');
+ info.className = 'save-info';
+
+ var idEl = document.createElement('div');
+ idEl.className = 'save-id';
+ idEl.textContent = save.save_id;
+
+ var detail = document.createElement('div');
+ detail.className = 'save-detail';
+ var parts = [];
+ if (save.dynasty) parts.push(save.dynasty);
+ if (save.role) parts.push(save.role);
+ if (save.chapter) parts.push('第' + save.chapter + '章');
+ if (save.created_at) parts.push(save.created_at.slice(0, 16).replace('T', ' '));
+ detail.textContent = parts.join(' · ');
+
+ info.appendChild(idEl);
+ info.appendChild(detail);
+
+ var actions = document.createElement('div');
+ actions.className = 'save-actions';
+
+ var loadBtn = document.createElement('button');
+ loadBtn.className = 'btn btn-small';
+ loadBtn.textContent = '读档';
+ loadBtn.addEventListener('click', function () {
+ doLoad(save.save_id);
+ });
+
+ actions.appendChild(loadBtn);
+
+ item.appendChild(info);
+ item.appendChild(actions);
+ container.appendChild(item);
+ });
+ }
+
+ // ---------- Load Save ----------
+ function doLoad(saveId) {
+ fetch(API_BASE + '/api/palace/save/load', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ save_id: saveId })
+ })
+ .then(function (r) { return r.json(); })
+ .then(function (data) {
+ if (data.error) {
+ showToast(data.message || '读档失败');
+ return;
+ }
+ sessionStorage.setItem('palace_save_id', saveId);
+ sessionStorage.setItem('palace_loaded', JSON.stringify(data));
+ window.location.href = 'game.html';
+ })
+ .catch(function (err) {
+ showToast('网络错误:' + err.message);
+ });
+ }
+
+ // ---------- Events ----------
+ function bindEvents() {
+ document.getElementById('btn-load').addEventListener('click', function () {
+ var id = document.getElementById('load-id').value.trim();
+ if (!id) { showToast('请输入存档编号'); return; }
+ doLoad(id);
+ });
+
+ document.getElementById('load-id').addEventListener('keydown', function (e) {
+ if (e.key === 'Enter') {
+ var id = this.value.trim();
+ if (id) doLoad(id);
+ }
+ });
+ }
+
+ // ---------- Utilities ----------
+ function showToast(msg) {
+ var old = document.querySelector('.toast');
+ if (old) old.remove();
+ var el = document.createElement('div');
+ el.className = 'toast';
+ el.textContent = msg;
+ document.body.appendChild(el);
+ setTimeout(function () { if (el.parentNode) el.remove(); }, 3000);
+ }
+
+ // ---------- Boot ----------
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();
diff --git a/modules/palace-game/frontend/style.css b/modules/palace-game/frontend/style.css
new file mode 100644
index 00000000..ffd770f1
--- /dev/null
+++ b/modules/palace-game/frontend/style.css
@@ -0,0 +1,564 @@
+/* ====================================
+ M-PALACE · 宫廷视觉风格
+ 暗红+金色主题 · 古风字体 · 逐字动画
+ ==================================== */
+
+/* ---------- CSS Variables ---------- */
+:root {
+ --bg-primary: #1a0a0a;
+ --bg-secondary: #0d0d1a;
+ --gold: #c9a96e;
+ --gold-light: #e8d5a3;
+ --danger: #8b0000;
+ --text-primary: #f5f0e8;
+ --text-secondary:#a0937d;
+ --border-gold: rgba(201,169,110,0.15);
+ --border-gold-md:rgba(201,169,110,0.3);
+ --glow-gold: rgba(201,169,110,0.25);
+}
+
+/* ---------- Reset & Base ---------- */
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+body {
+ font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ min-height: 100vh;
+ line-height: 1.6;
+ overflow-x: hidden;
+}
+
+/* ---------- Background Texture ---------- */
+body::before {
+ content: '';
+ position: fixed;
+ top: 0; left: 0; right: 0; bottom: 0;
+ background:
+ radial-gradient(ellipse at 20% 50%, rgba(139,0,0,0.08) 0%, transparent 60%),
+ radial-gradient(ellipse at 80% 20%, rgba(201,169,110,0.05) 0%, transparent 50%),
+ radial-gradient(ellipse at 50% 80%, rgba(13,13,26,0.6) 0%, transparent 70%);
+ pointer-events: none;
+ z-index: 0;
+}
+
+/* ---------- Layout ---------- */
+.container {
+ max-width: 720px;
+ margin: 0 auto;
+ padding: 2rem 1.5rem;
+ position: relative;
+ z-index: 1;
+}
+
+/* ---------- Typography ---------- */
+.serif {
+ font-family: 'Noto Serif SC', 'Source Han Serif CN', 'STSong', serif;
+}
+
+h1, h2, h3 { color: var(--gold); font-weight: 400; }
+
+/* ---------- Golden Divider ---------- */
+.divider {
+ height: 1px;
+ background: linear-gradient(90deg, transparent, var(--gold), transparent);
+ margin: 1.5rem 0;
+ opacity: 0.4;
+}
+
+.divider-text {
+ text-align: center;
+ color: var(--text-secondary);
+ font-size: 0.85rem;
+ padding: 0.5rem 0;
+}
+
+/* ====================================
+ INDEX PAGE — Quick Start
+ ==================================== */
+.start-page {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 100vh;
+ text-align: center;
+ padding: 2rem;
+}
+
+.start-icon {
+ font-size: 4rem;
+ margin-bottom: 1rem;
+ filter: drop-shadow(0 0 20px var(--glow-gold));
+}
+
+.start-title {
+ font-family: 'Noto Serif SC', 'Source Han Serif CN', serif;
+ font-size: 2.2rem;
+ letter-spacing: 0.8rem;
+ color: var(--gold);
+ margin-bottom: 0.3rem;
+}
+
+.start-subtitle {
+ font-size: 0.9rem;
+ color: var(--text-secondary);
+ margin-bottom: 2.5rem;
+ letter-spacing: 0.2rem;
+}
+
+/* Form Card */
+.start-card {
+ background: rgba(26,10,10,0.85);
+ border: 1px solid var(--border-gold);
+ border-radius: 8px;
+ padding: 2rem 2.5rem;
+ width: 100%;
+ max-width: 420px;
+ backdrop-filter: blur(8px);
+}
+
+.form-group {
+ margin-bottom: 1.5rem;
+ text-align: left;
+}
+
+.form-group label {
+ display: block;
+ font-size: 0.9rem;
+ color: var(--text-secondary);
+ margin-bottom: 0.5rem;
+}
+
+select, input[type="text"] {
+ width: 100%;
+ padding: 0.7rem 1rem;
+ background: rgba(13,13,26,0.6);
+ border: 1px solid var(--border-gold-md);
+ border-radius: 4px;
+ color: var(--text-primary);
+ font-size: 0.95rem;
+ font-family: inherit;
+ outline: none;
+ transition: border-color 0.3s;
+}
+
+select:focus, input[type="text"]:focus {
+ border-color: var(--gold);
+ box-shadow: 0 0 8px var(--glow-gold);
+}
+
+select option {
+ background: var(--bg-primary);
+ color: var(--text-primary);
+}
+
+/* ---------- Buttons ---------- */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ padding: 0.75rem 2rem;
+ border: 1px solid var(--gold);
+ border-radius: 4px;
+ background: rgba(139,0,0,0.3);
+ color: var(--gold);
+ font-size: 1rem;
+ font-family: 'Noto Serif SC', 'Source Han Serif CN', serif;
+ cursor: pointer;
+ transition: all 0.3s;
+ letter-spacing: 0.15rem;
+}
+
+.btn:hover {
+ background: rgba(139,0,0,0.6);
+ box-shadow: 0 0 16px var(--glow-gold);
+ transform: translateY(-1px);
+}
+
+.btn:active { transform: translateY(0); }
+
+.btn-primary {
+ width: 100%;
+ padding: 1rem;
+ font-size: 1.1rem;
+ margin-top: 0.5rem;
+}
+
+.btn-small {
+ padding: 0.5rem 1.2rem;
+ font-size: 0.85rem;
+}
+
+/* Load Section */
+.load-section {
+ width: 100%;
+ max-width: 420px;
+ margin-top: 1rem;
+}
+
+.load-row {
+ display: flex;
+ gap: 0.5rem;
+ margin-top: 0.5rem;
+}
+
+.load-row input { flex: 1; }
+
+/* Footer */
+.start-footer {
+ margin-top: 3rem;
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+ opacity: 0.6;
+}
+
+/* ====================================
+ GAME PAGE — Main Narrative
+ ==================================== */
+.game-page {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+}
+
+/* Top Bar */
+.top-bar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.8rem 1.5rem;
+ border-bottom: 1px solid var(--border-gold);
+ background: rgba(26,10,10,0.9);
+ backdrop-filter: blur(6px);
+ position: sticky;
+ top: 0;
+ z-index: 10;
+}
+
+.top-bar-title {
+ font-family: 'Noto Serif SC', 'Source Han Serif CN', serif;
+ font-size: 1rem;
+ color: var(--gold);
+}
+
+.top-bar-actions {
+ display: flex;
+ gap: 0.5rem;
+}
+
+/* Narrative Area */
+.narrative-area {
+ flex: 1;
+ padding: 2rem 1.5rem;
+ max-width: 720px;
+ margin: 0 auto;
+ width: 100%;
+}
+
+.chapter-title {
+ font-family: 'Noto Serif SC', 'Source Han Serif CN', serif;
+ font-size: 1.4rem;
+ color: var(--gold);
+ text-align: center;
+ margin-bottom: 1.5rem;
+ letter-spacing: 0.3rem;
+}
+
+.narrative-text {
+ font-family: 'Noto Serif SC', 'Source Han Serif CN', serif;
+ font-size: 1rem;
+ line-height: 1.9;
+ color: var(--text-primary);
+ text-align: justify;
+ margin-bottom: 2rem;
+ min-height: 120px;
+}
+
+/* Typewriter Animation */
+.typewriter-cursor {
+ display: inline-block;
+ width: 2px;
+ height: 1.1em;
+ background: var(--gold);
+ margin-left: 2px;
+ vertical-align: text-bottom;
+ animation: blink 0.8s infinite;
+}
+
+@keyframes blink {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0; }
+}
+
+/* Choices Area */
+.choices-area {
+ padding: 0 1.5rem 1.5rem;
+ max-width: 720px;
+ margin: 0 auto;
+ width: 100%;
+}
+
+.choices-label {
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ margin-bottom: 0.8rem;
+ padding-left: 0.3rem;
+}
+
+.choice-btn {
+ display: block;
+ width: 100%;
+ padding: 0.9rem 1.2rem;
+ margin-bottom: 0.6rem;
+ background: rgba(139,0,0,0.15);
+ border: 1px solid var(--border-gold-md);
+ border-radius: 6px;
+ color: var(--text-primary);
+ font-size: 0.95rem;
+ font-family: 'Noto Serif SC', 'Source Han Serif CN', serif;
+ text-align: left;
+ cursor: pointer;
+ transition: all 0.3s;
+}
+
+.choice-btn:hover {
+ background: rgba(139,0,0,0.35);
+ border-color: var(--gold);
+ box-shadow: 0 0 12px var(--glow-gold);
+ transform: translateX(4px);
+}
+
+.choice-btn:active {
+ transform: translateX(2px);
+}
+
+.choice-btn .choice-num {
+ color: var(--gold);
+ margin-right: 0.5rem;
+ font-weight: 600;
+}
+
+/* Free Input */
+.free-input-row {
+ display: flex;
+ gap: 0.5rem;
+ margin-top: 0.3rem;
+}
+
+.free-input-row input {
+ flex: 1;
+ font-family: 'Noto Serif SC', 'Source Han Serif CN', serif;
+}
+
+/* Status Bar */
+.status-bar {
+ padding: 1rem 1.5rem;
+ border-top: 1px solid var(--border-gold);
+ background: rgba(13,13,26,0.6);
+ backdrop-filter: blur(6px);
+}
+
+.status-toggle {
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+ cursor: pointer;
+ margin-bottom: 0.5rem;
+ user-select: none;
+}
+
+.status-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 0.6rem 1.5rem;
+}
+
+.stat-row {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.85rem;
+}
+
+.stat-icon { width: 1.2rem; text-align: center; }
+
+.stat-label {
+ width: 3rem;
+ color: var(--text-secondary);
+ flex-shrink: 0;
+}
+
+.stat-bar-bg {
+ flex: 1;
+ height: 6px;
+ background: rgba(201,169,110,0.1);
+ border-radius: 3px;
+ overflow: hidden;
+}
+
+.stat-bar-fill {
+ height: 100%;
+ border-radius: 3px;
+ transition: width 0.6s ease;
+}
+
+.stat-bar-fill.power { background: linear-gradient(90deg, #8b0000, #c9a96e); }
+.stat-bar-fill.status { background: linear-gradient(90deg, #6b21a8, #c9a96e); }
+.stat-bar-fill.emotion { background: linear-gradient(90deg, #b8860b, #e8d5a3); }
+.stat-bar-fill.conflict{ background: linear-gradient(90deg, #4a1a2e, #8b0000); }
+
+.stat-value {
+ width: 1.8rem;
+ text-align: right;
+ color: var(--gold-light);
+ font-size: 0.8rem;
+}
+
+/* Bottom Bar */
+.bottom-bar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.5rem 1.5rem;
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+ border-top: 1px solid var(--border-gold);
+ background: rgba(26,10,10,0.9);
+}
+
+/* Status collapsed */
+.status-bar.collapsed .status-grid { display: none; }
+
+/* ====================================
+ SAVE PAGE
+ ==================================== */
+.save-page {
+ min-height: 100vh;
+ padding: 2rem 1.5rem;
+}
+
+.save-page h1 {
+ font-family: 'Noto Serif SC', 'Source Han Serif CN', serif;
+ text-align: center;
+ margin-bottom: 2rem;
+ letter-spacing: 0.4rem;
+}
+
+.save-list {
+ max-width: 600px;
+ margin: 0 auto;
+}
+
+.save-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1rem 1.2rem;
+ margin-bottom: 0.8rem;
+ background: rgba(26,10,10,0.7);
+ border: 1px solid var(--border-gold);
+ border-radius: 6px;
+ transition: border-color 0.3s;
+}
+
+.save-item:hover {
+ border-color: var(--gold);
+}
+
+.save-info { flex: 1; }
+
+.save-id {
+ font-family: 'Noto Serif SC', 'Source Han Serif CN', serif;
+ font-size: 1rem;
+ color: var(--gold);
+}
+
+.save-detail {
+ font-size: 0.8rem;
+ color: var(--text-secondary);
+ margin-top: 0.2rem;
+}
+
+.save-actions {
+ display: flex;
+ gap: 0.4rem;
+}
+
+.empty-state {
+ text-align: center;
+ padding: 3rem;
+ color: var(--text-secondary);
+}
+
+/* ====================================
+ LOADING & TRANSITIONS
+ ==================================== */
+.loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ padding: 2rem;
+ color: var(--text-secondary);
+}
+
+.loading-dot {
+ width: 6px;
+ height: 6px;
+ background: var(--gold);
+ border-radius: 50%;
+ animation: pulse 1.2s infinite;
+}
+
+.loading-dot:nth-child(2) { animation-delay: 0.2s; }
+.loading-dot:nth-child(3) { animation-delay: 0.4s; }
+
+@keyframes pulse {
+ 0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
+ 40% { opacity: 1; transform: scale(1.2); }
+}
+
+/* Fade transition */
+.fade-in {
+ animation: fadeIn 0.6s ease forwards;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(8px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* Toast notification */
+.toast {
+ position: fixed;
+ top: 1rem;
+ right: 1rem;
+ padding: 0.8rem 1.5rem;
+ background: rgba(26,10,10,0.95);
+ border: 1px solid var(--gold);
+ border-radius: 6px;
+ color: var(--gold-light);
+ font-size: 0.9rem;
+ z-index: 100;
+ animation: slideIn 0.3s ease, fadeOut 0.3s ease 2.5s forwards;
+}
+
+@keyframes slideIn {
+ from { transform: translateX(100%); opacity: 0; }
+ to { transform: translateX(0); opacity: 1; }
+}
+
+@keyframes fadeOut {
+ to { opacity: 0; transform: translateY(-10px); }
+}
+
+/* ====================================
+ Responsive
+ ==================================== */
+@media (max-width: 600px) {
+ .start-card { padding: 1.5rem; }
+ .start-title { font-size: 1.8rem; letter-spacing: 0.5rem; }
+ .status-grid { grid-template-columns: 1fr; }
+ .top-bar-title { font-size: 0.85rem; }
+}
From e6269b77538db8595056b65432b0cc3eb4a07d3d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Mar 2026 12:56:37 +0000
Subject: [PATCH 3/3] fix(M-PALACE): address code review - secure encryption
key, improve error handling
- Require PALACE_SAVE_KEY env var in production (fail-fast)
- Add try-catch to deleteSave for robustness
Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
---
.../backend/engines/save-manager.js | 24 +-
modules/palace-game/backend/package-lock.json | 854 ++++++++++++++++++
modules/palace-game/backend/package.json | 14 +
3 files changed, 884 insertions(+), 8 deletions(-)
create mode 100644 modules/palace-game/backend/package-lock.json
create mode 100644 modules/palace-game/backend/package.json
diff --git a/modules/palace-game/backend/engines/save-manager.js b/modules/palace-game/backend/engines/save-manager.js
index 6c281185..0f5a1fbb 100644
--- a/modules/palace-game/backend/engines/save-manager.js
+++ b/modules/palace-game/backend/engines/save-manager.js
@@ -12,8 +12,12 @@ const crypto = require('crypto');
const SAVES_DIR = path.join(__dirname, '..', '..', 'data', 'saves');
-// AES-256-CBC 加密密钥(生产环境应从环境变量读取)
-const ENCRYPTION_KEY = process.env.PALACE_SAVE_KEY || 'palace-game-default-key-32ch!';
+// AES-256-CBC 加密密钥
+const ENCRYPTION_KEY = process.env.PALACE_SAVE_KEY || (
+ process.env.NODE_ENV === 'production'
+ ? (function () { throw new Error('PALACE_SAVE_KEY environment variable is required in production'); })()
+ : 'palace-game-dev-only-key-32ch!!'
+);
const IV_LENGTH = 16;
/**
@@ -161,14 +165,18 @@ function listSaves() {
* 删除存档
*/
function deleteSave(saveId) {
- const saveDir = path.join(SAVES_DIR, saveId);
+ var saveDir = path.join(SAVES_DIR, saveId);
if (!fs.existsSync(saveDir)) return false;
- const files = fs.readdirSync(saveDir);
- for (const f of files) {
- fs.unlinkSync(path.join(saveDir, f));
+ try {
+ var files = fs.readdirSync(saveDir);
+ for (var i = 0; i < files.length; i++) {
+ fs.unlinkSync(path.join(saveDir, files[i]));
+ }
+ fs.rmdirSync(saveDir);
+ return true;
+ } catch (err) {
+ return false;
}
- fs.rmdirSync(saveDir);
- return true;
}
/**
diff --git a/modules/palace-game/backend/package-lock.json b/modules/palace-game/backend/package-lock.json
new file mode 100644
index 00000000..1a6e638d
--- /dev/null
+++ b/modules/palace-game/backend/package-lock.json
@@ -0,0 +1,854 @@
+{
+ "name": "palace-game-backend",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "palace-game-backend",
+ "version": "1.0.0",
+ "dependencies": {
+ "cors": "^2.8.5",
+ "express": "^4.21.2"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+ "license": "MIT"
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.4",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.14.0",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.3",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.14.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.14.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
+ "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/send": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "license": "MIT",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ }
+ }
+}
diff --git a/modules/palace-game/backend/package.json b/modules/palace-game/backend/package.json
new file mode 100644
index 00000000..5c4ec56b
--- /dev/null
+++ b/modules/palace-game/backend/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "palace-game-backend",
+ "version": "1.0.0",
+ "description": "M-PALACE · 动态人格宫廷生成系统 · 后端服务",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js",
+ "dev": "node server.js"
+ },
+ "dependencies": {
+ "cors": "^2.8.5",
+ "express": "^4.21.2"
+ }
+}