ziwei-mcp
Version:
紫微斗数 MCP (Ziwei Doushu Model Context Protocol) - 基于 MCP 协议的紫微斗数命理分析工具
659 lines (567 loc) • 17.3 kB
JavaScript
/**
* SQLite数据持久化管理器
* 专门为SQLite优化的数据持久化解决方案
*/
const sqlite3 = require("sqlite3").verbose();
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
class SQLitePersistenceManager {
constructor(config = {}) {
this.config = {
dbPath: config.dbPath || "./data/charts.db",
backupPath: config.backupPath || "./data/backups",
enableWAL: config.enableWAL !== false,
enableCache: config.enableCache !== false,
cacheSize: config.cacheSize || 1000,
enableEncryption: config.enableEncryption || false,
encryptionKey: config.encryptionKey || null,
autoBackup: config.autoBackup || false,
backupInterval: config.backupInterval || 24 * 60 * 60 * 1000, // 24小时
...config,
};
this.db = null;
this.cache = new Map();
this.isInitialized = false;
this.backupTimer = null;
// 确保数据目录存在
this.ensureDirectories();
}
/**
* 确保必要的目录存在
*/
ensureDirectories() {
const dbDir = path.dirname(this.config.dbPath);
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
if (!fs.existsSync(this.config.backupPath)) {
fs.mkdirSync(this.config.backupPath, { recursive: true });
}
}
/**
* 初始化数据库
*/
async initialize() {
if (this.isInitialized) return;
return new Promise((resolve, reject) => {
this.db = new sqlite3.Database(this.config.dbPath, (err) => {
if (err) {
reject(new Error(`数据库连接失败: ${err.message}`));
return;
}
// 启用WAL模式
if (this.config.enableWAL) {
this.db.run("PRAGMA journal_mode=WAL");
}
// 设置其他优化参数
this.db.run("PRAGMA synchronous=NORMAL");
this.db.run("PRAGMA cache_size=10000");
this.db.run("PRAGMA temp_store=memory");
this.db.run("PRAGMA mmap_size=268435456"); // 256MB
this.createTables()
.then(() => {
this.isInitialized = true;
// 启动自动备份
if (this.config.autoBackup) {
this.startAutoBackup();
}
resolve();
})
.catch(reject);
});
});
}
/**
* 创建数据表
*/
async createTables() {
const tables = [
// 命盘表
`CREATE TABLE IF NOT EXISTS charts (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
birth_info TEXT NOT NULL,
chart_data TEXT NOT NULL,
svg_content TEXT,
chart_type TEXT DEFAULT 'traditional',
theme TEXT DEFAULT 'classic',
tags TEXT,
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
// 分析记录表
`CREATE TABLE IF NOT EXISTS analyses (
id TEXT PRIMARY KEY,
chart_id TEXT NOT NULL,
analysis_type TEXT NOT NULL,
analysis_data TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (chart_id) REFERENCES charts (id) ON DELETE CASCADE
)`,
// 比较记录表
`CREATE TABLE IF NOT EXISTS comparisons (
id TEXT PRIMARY KEY,
chart1_id TEXT NOT NULL,
chart2_id TEXT NOT NULL,
comparison_type TEXT NOT NULL,
comparison_data TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (chart1_id) REFERENCES charts (id) ON DELETE CASCADE,
FOREIGN KEY (chart2_id) REFERENCES charts (id) ON DELETE CASCADE
)`,
// 备份记录表
`CREATE TABLE IF NOT EXISTS backups (
id TEXT PRIMARY KEY,
backup_path TEXT NOT NULL,
backup_size INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
];
const indexes = [
"CREATE INDEX IF NOT EXISTS idx_charts_name ON charts (name)",
"CREATE INDEX IF NOT EXISTS idx_charts_created_at ON charts (created_at)",
"CREATE INDEX IF NOT EXISTS idx_charts_chart_type ON charts (chart_type)",
"CREATE INDEX IF NOT EXISTS idx_analyses_chart_id ON analyses (chart_id)",
"CREATE INDEX IF NOT EXISTS idx_comparisons_chart1_id ON comparisons (chart1_id)",
"CREATE INDEX IF NOT EXISTS idx_comparisons_chart2_id ON comparisons (chart2_id)",
];
return new Promise((resolve, reject) => {
// 先创建所有表
const createTables = () => {
return new Promise((resolveTable, rejectTable) => {
let tableCompleted = 0;
if (tables.length === 0) {
resolveTable();
return;
}
tables.forEach((sql) => {
this.db.run(sql, (err) => {
if (err) {
rejectTable(new Error(`创建表失败: ${err.message}`));
return;
}
tableCompleted++;
if (tableCompleted === tables.length) {
resolveTable();
}
});
});
});
};
// 再创建所有索引
const createIndexes = () => {
return new Promise((resolveIndex, rejectIndex) => {
let indexCompleted = 0;
if (indexes.length === 0) {
resolveIndex();
return;
}
indexes.forEach((sql) => {
this.db.run(sql, (err) => {
if (err) {
rejectIndex(new Error(`创建索引失败: ${err.message}`));
return;
}
indexCompleted++;
if (indexCompleted === indexes.length) {
resolveIndex();
}
});
});
});
};
// 按顺序执行
createTables()
.then(() => createIndexes())
.then(() => resolve())
.catch(reject);
});
}
/**
* 保存命盘
*/
async saveChart(chartData) {
await this.ensureInitialized();
const id = chartData.id || this.generateId();
const now = new Date().toISOString();
// 加密敏感数据
const encryptedData = this.config.enableEncryption
? this.encrypt(JSON.stringify(chartData.chartData))
: JSON.stringify(chartData.chartData);
const data = {
id,
name: chartData.name,
birth_info: JSON.stringify(chartData.birthInfo),
chart_data: encryptedData,
svg_content: chartData.svgContent || null,
chart_type: chartData.chartType || "traditional",
theme: chartData.theme || "classic",
tags: chartData.tags ? JSON.stringify(chartData.tags) : null,
notes: chartData.notes || null,
updated_at: now,
};
return new Promise((resolve, reject) => {
const sql = `INSERT OR REPLACE INTO charts
(id, name, birth_info, chart_data, svg_content, chart_type, theme, tags, notes, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,
COALESCE((SELECT created_at FROM charts WHERE id = ?), ?), ?)`;
this.db.run(
sql,
[
data.id,
data.name,
data.birth_info,
data.chart_data,
data.svg_content,
data.chart_type,
data.theme,
data.tags,
data.notes,
data.id,
now,
data.updated_at,
],
function (err) {
if (err) {
reject(new Error(`保存命盘失败: ${err.message}`));
return;
}
// 更新缓存
if (this.config.enableCache) {
this.cache.set(id, { ...data, created_at: now });
}
resolve({ id, changes: this.changes });
}.bind(this),
);
});
}
/**
* 获取命盘
*/
async getChart(id) {
await this.ensureInitialized();
// 检查缓存
if (this.config.enableCache && this.cache.has(id)) {
return this.processChartData(this.cache.get(id));
}
return new Promise((resolve, reject) => {
const sql = "SELECT * FROM charts WHERE id = ?";
this.db.get(sql, [id], (err, row) => {
if (err) {
reject(new Error(`获取命盘失败: ${err.message}`));
return;
}
if (!row) {
resolve(null);
return;
}
const result = this.processChartData(row);
// 更新缓存
if (this.config.enableCache) {
this.cache.set(id, row);
}
resolve(result);
});
});
}
/**
* 列出命盘
*/
async listCharts(options = {}) {
await this.ensureInitialized();
const {
limit = 50,
offset = 0,
sortBy = "created_at",
sortOrder = "DESC",
chartType = null,
theme = null,
} = options;
let sql =
"SELECT id, name, chart_type, theme, created_at, updated_at FROM charts";
const params = [];
const conditions = [];
if (chartType) {
conditions.push("chart_type = ?");
params.push(chartType);
}
if (theme) {
conditions.push("theme = ?");
params.push(theme);
}
if (conditions.length > 0) {
sql += " WHERE " + conditions.join(" AND ");
}
sql += ` ORDER BY ${sortBy} ${sortOrder} LIMIT ? OFFSET ?`;
params.push(limit, offset);
return new Promise((resolve, reject) => {
this.db.all(sql, params, (err, rows) => {
if (err) {
reject(new Error(`列出命盘失败: ${err.message}`));
return;
}
resolve(
rows.map((row) => ({
id: row.id,
name: row.name,
chartType: row.chart_type,
theme: row.theme,
createdAt: row.created_at,
updatedAt: row.updated_at,
})),
);
});
});
}
/**
* 搜索命盘
*/
async searchCharts(query, options = {}) {
await this.ensureInitialized();
const { limit = 50, offset = 0 } = options;
const sql = `SELECT id, name, chart_type, theme, created_at, updated_at
FROM charts
WHERE name LIKE ? OR notes LIKE ?
ORDER BY created_at DESC
LIMIT ? OFFSET ?`;
const searchTerm = `%${query}%`;
return new Promise((resolve, reject) => {
this.db.all(sql, [searchTerm, searchTerm, limit, offset], (err, rows) => {
if (err) {
reject(new Error(`搜索命盘失败: ${err.message}`));
return;
}
resolve(
rows.map((row) => ({
id: row.id,
name: row.name,
chartType: row.chart_type,
theme: row.theme,
createdAt: row.created_at,
updatedAt: row.updated_at,
})),
);
});
});
}
/**
* 删除命盘
*/
async deleteChart(id) {
await this.ensureInitialized();
return new Promise((resolve, reject) => {
this.db.run(
"DELETE FROM charts WHERE id = ?",
[id],
function (err) {
if (err) {
reject(new Error(`删除命盘失败: ${err.message}`));
return;
}
// 清除缓存
if (this.config.enableCache) {
this.cache.delete(id);
}
resolve({ deleted: this.changes > 0, changes: this.changes });
}.bind(this),
);
});
}
/**
* 获取统计信息
*/
async getStats() {
await this.ensureInitialized();
const queries = [
"SELECT COUNT(*) as total FROM charts",
"SELECT chart_type, COUNT(*) as count FROM charts GROUP BY chart_type",
"SELECT theme, COUNT(*) as count FROM charts GROUP BY theme",
"SELECT DATE(created_at) as date, COUNT(*) as count FROM charts GROUP BY DATE(created_at) ORDER BY date DESC LIMIT 30",
];
const results = await Promise.all(
queries.map(
(sql) =>
new Promise((resolve, reject) => {
this.db.all(sql, [], (err, rows) => {
if (err) reject(err);
else resolve(rows);
});
}),
),
);
return {
total: results[0][0].total,
byType: results[1],
byTheme: results[2],
dailyStats: results[3],
};
}
/**
* 备份数据库
*/
async backup(customPath = null) {
await this.ensureInitialized();
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const backupFileName = `charts_backup_${timestamp}.db`;
const backupPath =
customPath || path.join(this.config.backupPath, backupFileName);
return new Promise((resolve, reject) => {
// 使用SQLite的VACUUM INTO命令创建备份
this.db.run(`VACUUM INTO '${backupPath}'`, (err) => {
if (err) {
reject(new Error(`备份失败: ${err.message}`));
return;
}
// 获取备份文件大小
const stats = fs.statSync(backupPath);
const backupId = this.generateId();
// 记录备份信息
this.db.run(
"INSERT INTO backups (id, backup_path, backup_size) VALUES (?, ?, ?)",
[backupId, backupPath, stats.size],
(err) => {
if (err) {
console.warn("备份记录保存失败:", err.message);
}
resolve({
id: backupId,
path: backupPath,
size: stats.size,
timestamp,
});
},
);
});
});
}
/**
* 恢复数据库
*/
async restore(backupPath) {
if (!fs.existsSync(backupPath)) {
throw new Error("备份文件不存在");
}
// 关闭当前连接
await this.close();
// 备份当前数据库
const currentBackup = this.config.dbPath + ".before_restore";
if (fs.existsSync(this.config.dbPath)) {
fs.copyFileSync(this.config.dbPath, currentBackup);
}
try {
// 复制备份文件
fs.copyFileSync(backupPath, this.config.dbPath);
// 重新初始化
this.isInitialized = false;
await this.initialize();
return { success: true, restoredFrom: backupPath };
} catch (error) {
// 恢复失败,回滚
if (fs.existsSync(currentBackup)) {
fs.copyFileSync(currentBackup, this.config.dbPath);
this.isInitialized = false;
await this.initialize();
}
throw new Error(`恢复失败: ${error.message}`);
} finally {
// 清理临时文件
if (fs.existsSync(currentBackup)) {
fs.unlinkSync(currentBackup);
}
}
}
/**
* 启动自动备份
*/
startAutoBackup() {
if (this.backupTimer) {
clearInterval(this.backupTimer);
}
this.backupTimer = setInterval(async () => {
try {
await this.backup();
console.log("自动备份完成");
} catch (error) {
console.error("自动备份失败:", error.message);
}
}, this.config.backupInterval);
}
/**
* 关闭数据库连接
*/
async close() {
if (this.backupTimer) {
clearInterval(this.backupTimer);
this.backupTimer = null;
}
if (this.db) {
return new Promise((resolve) => {
this.db.close((err) => {
if (err) {
console.error("关闭数据库失败:", err.message);
}
this.db = null;
this.isInitialized = false;
resolve();
});
});
}
}
/**
* 辅助方法
*/
async ensureInitialized() {
if (!this.isInitialized) {
await this.initialize();
}
}
generateId() {
return crypto.randomBytes(16).toString("hex");
}
encrypt(text) {
if (!this.config.encryptionKey) return text;
const cipher = crypto.createCipher(
"aes-256-cbc",
this.config.encryptionKey,
);
let encrypted = cipher.update(text, "utf8", "hex");
encrypted += cipher.final("hex");
return encrypted;
}
decrypt(encryptedText) {
if (!this.config.encryptionKey) return encryptedText;
const decipher = crypto.createDecipher(
"aes-256-cbc",
this.config.encryptionKey,
);
let decrypted = decipher.update(encryptedText, "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
}
processChartData(row) {
if (!row) return null;
try {
const chartData = this.config.enableEncryption
? JSON.parse(this.decrypt(row.chart_data))
: JSON.parse(row.chart_data);
return {
id: row.id,
name: row.name,
birthInfo: JSON.parse(row.birth_info),
chartData,
svgContent: row.svg_content,
chartType: row.chart_type,
theme: row.theme,
tags: row.tags ? JSON.parse(row.tags) : null,
notes: row.notes,
createdAt: row.created_at,
updatedAt: row.updated_at,
};
} catch (error) {
throw new Error(`数据解析失败: ${error.message}`);
}
}
}
module.exports = SQLitePersistenceManager;