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

199 lines
5.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

/**
* M-PALACE · 存档管理器
* 打包 / 加密 / 编号 / 解密 / 恢复
*
* 存档编号规则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 || (
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;
/**
* 确保存档目录存在
*/
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) {
var saveDir = path.join(SAVES_DIR, saveId);
if (!fs.existsSync(saveDir)) return false;
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;
}
}
/**
* 检查存档是否存在
*/
function exists(saveId) {
return fs.existsSync(path.join(SAVES_DIR, saveId));
}
module.exports = {
save,
load,
listSaves,
deleteSave,
exists,
generateSaveId,
encrypt,
decrypt
};