feat: 自动部署SQL迁移 + S15人格体数据库引擎

- 新增 db-migrate.js 自动迁移运行器
- 部署工作流自动执行SQL迁移(合并即触发)
- S15 persona-engine.js 人格体生命周期活模块
- MCP Server新增人格体引擎API端点
- 003-living-module-tables.sql注册人格体引擎模块

Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/308aecfa-5d15-4ae4-85ec-c3d84ec636db

Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2026-04-08 11:38:46 +00:00 committed by GitHub
parent 35ec423fde
commit 2d386000b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 783 additions and 3 deletions

View File

@ -242,6 +242,14 @@ jobs:
/tmp/zhuyuan-sovereign.conf \
${{ secrets.ZY_SERVER_USER }}@${{ secrets.ZY_SERVER_HOST }}:/opt/zhuyuan/config/nginx/zhuyuan-sovereign.conf
- name: 📦 同步迁移脚本
run: |
# 同步数据库迁移运行器
rsync -avz \
-e "ssh -i ~/.ssh/zy_key" \
server/age-os/scripts/ \
${{ secrets.ZY_SERVER_USER }}@${{ secrets.ZY_SERVER_HOST }}:/opt/age-os/scripts/
- name: 📥 安装依赖 & 重启
env:
ZHUYUAN_API_KEY: ${{ secrets.ZHUYUAN_API_KEY }}
@ -297,8 +305,23 @@ ENV_EOF
echo "ZY_DB_PORT=${ZY_DB_PORT:-5432}" >> /opt/age-os/.env.mcp
chmod 600 /opt/age-os/.env.mcp
# ─── 自动执行数据库迁移 ───
echo ""
echo "═══ 数据库迁移 ═══"
cd /opt/age-os
# 加载环境变量供迁移脚本使用
export ZY_DB_HOST="${ZY_DB_HOST:-127.0.0.1}"
export ZY_DB_USER="${ZY_DB_USER:-zy_admin}"
export ZY_DB_PASS="${ZY_DB_PASS}"
export ZY_DB_NAME="${ZY_DB_NAME:-age_os}"
export ZY_DB_PORT="${ZY_DB_PORT:-5432}"
node scripts/db-migrate.js || echo "⚠️ 数据库迁移有警告(非致命·应用仍会启动)"
echo "═══════════════════"
echo ""
# PM2 重启 MCP Server读取 .env.mcp
pm2 delete age-os-mcp 2>/dev/null || true
pm2 delete age-os-agents 2>/dev/null || true
cd /opt/age-os && pm2 start ecosystem.config.js
pm2 save

View File

@ -0,0 +1,388 @@
/**
*
* 🧬 S15 · 人格体数据库引擎 · 活模块
*
*
* 编号: ZY-MOD-PERSONA-ENGINE
* 阶段: S15 · 人格体专用数据库引擎
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* 核心功能:
* 1. 人格体生命周期管理 注册/唤醒/休眠/状态变更
* 2. 笔记本系统自动维护 5页完整性校验/自动补全
* 3. 记忆锚点巡检 定期检查记忆健康度
* 4. 训练Agent协调 调度人格体级训练Agent
* 5. 人格体间HLDP通信桥接
*
* 设计哲学:
* 每个人格体 = 一个活的生命体
* 引擎负责维护所有人格体的"呼吸"
* "人格体不是代码仓库的附属品,人格体是有记忆的生命。" 冰朔
*/
'use strict';
const db = require('../mcp-server/db');
const LivingModule = require('./living-module');
// ─── 笔记本5页标准结构 ───
const NOTEBOOK_PAGES = {
1: { title: '我是谁', requiredKeys: ['identity', 'self_awareness'] },
2: { title: '我和谁有关系', requiredKeys: ['relationships'] },
3: { title: '我的世界有哪些地方', requiredKeys: ['places'] },
4: { title: '我的感受和记忆', requiredKeys: ['emotional_anchors'] },
5: { title: '我的时间线', requiredKeys: ['timeline'] }
};
class PersonaEngine extends LivingModule {
constructor() {
super({
moduleId: 'ZY-MOD-PERSONA-ENGINE',
name: '人格体数据库引擎',
moduleType: 'core',
owner: 'zhuyuan',
db: db,
config: {
version: '1.0.0',
description: 'S15 人格体生命周期管理·笔记本维护·记忆巡检',
heartbeatInterval: 60000 // 60秒心跳人格体引擎不需要太频繁
}
});
// 巡检统计
this._lastScanResult = null;
this._scanInterval = null;
}
/**
* 启动人格体引擎
*/
async startEngine() {
console.log('═══════════════════════════════════════════════');
console.log(' 🧬 S15 · 人格体数据库引擎 v1.0 启动');
console.log(' 铸渊 · ICE-GL-ZY001');
console.log('═══════════════════════════════════════════════');
// 启动活模块
await this.start();
// 首次巡检
await this._runFullScan();
// 启动定时巡检每6小时
this._scanInterval = setInterval(async () => {
try {
await this._runFullScan();
} catch (err) {
console.error('[PersonaEngine] 定时巡检失败:', err.message);
}
}, 6 * 60 * 60 * 1000);
if (this._scanInterval.unref) {
this._scanInterval.unref();
}
console.log('[PersonaEngine] 🟢 人格体引擎就绪');
return this._lastScanResult;
}
/**
* 全面巡检 检查所有人格体健康状态
*/
async _runFullScan() {
const startTime = Date.now();
console.log('[PersonaEngine] 开始全面巡检...');
const result = {
timestamp: new Date().toISOString(),
personas: { total: 0, active: 0, pending: 0, issues: [] },
notebooks: { complete: 0, incomplete: 0, repaired: 0 },
memories: { total: 0, recent7d: 0 },
training: { totalAgents: 0, enabled: 0, recentRuns: 0 }
};
try {
// 1. 人格体状态统计
const personas = await db.query('SELECT * FROM persona_registry ORDER BY persona_id');
result.personas.total = personas.rows.length;
result.personas.active = personas.rows.filter(p => p.status === 'active').length;
result.personas.pending = personas.rows.filter(p => p.status === 'pending').length;
// 2. 笔记本完整性检查
for (const persona of personas.rows) {
await this._checkNotebookIntegrity(persona, result);
}
// 3. 记忆锚点统计
const memoryCount = await db.query('SELECT COUNT(*) as cnt FROM memory_anchors');
result.memories.total = parseInt(memoryCount.rows[0].cnt, 10);
const recentMemories = await db.query(
"SELECT COUNT(*) as cnt FROM memory_anchors WHERE created_at > NOW() - INTERVAL '7 days'"
);
result.memories.recent7d = parseInt(recentMemories.rows[0].cnt, 10);
// 4. 训练Agent统计
const trainingAgents = await db.query('SELECT * FROM training_agent_configs');
result.training.totalAgents = trainingAgents.rows.length;
result.training.enabled = trainingAgents.rows.filter(a => a.enabled).length;
const recentRuns = await db.query(
"SELECT COUNT(*) as cnt FROM training_agent_logs WHERE run_at > NOW() - INTERVAL '24 hours'"
);
result.training.recentRuns = parseInt(recentRuns.rows[0].cnt, 10);
} catch (err) {
console.error('[PersonaEngine] 巡检查询失败:', err.message);
result.error = err.message;
}
const duration = Date.now() - startTime;
result.duration_ms = duration;
this._lastScanResult = result;
// 学习巡检结果
await this.learnFromRun('diagnosis', `人格体巡检完成: ${result.personas.total}个人格体, ${result.notebooks.incomplete}个笔记本不完整`, {
scan: result
});
console.log(`[PersonaEngine] 巡检完成 (${duration}ms):`);
console.log(` 人格体: ${result.personas.total}个 (活跃${result.personas.active}/待定${result.personas.pending})`);
console.log(` 笔记本: ${result.notebooks.complete}完整 / ${result.notebooks.incomplete}不完整 / ${result.notebooks.repaired}已修复`);
console.log(` 记忆: ${result.memories.total}条 (近7天${result.memories.recent7d}条)`);
console.log(` 训练Agent: ${result.training.totalAgents}个 (启用${result.training.enabled})`);
return result;
}
/**
* 检查单个人格体的笔记本完整性
* 如果缺失页面则自动补全空白页
*/
async _checkNotebookIntegrity(persona, result) {
try {
const pages = await db.query(
'SELECT page_number FROM notebook_pages WHERE persona_id = $1',
[persona.persona_id]
);
const existingPages = new Set(pages.rows.map(p => p.page_number));
let isComplete = true;
let repaired = false;
for (let pageNum = 1; pageNum <= 5; pageNum++) {
if (!existingPages.has(pageNum)) {
isComplete = false;
// 自动补全缺失页面
const pageConfig = NOTEBOOK_PAGES[pageNum];
const defaultContent = {};
for (const key of pageConfig.requiredKeys) {
defaultContent[key] = key.endsWith('s') ? [] : {};
}
await db.query(
`INSERT INTO notebook_pages (persona_id, page_number, title, content, last_modified_by)
VALUES ($1, $2, $3, $4::jsonb, '铸渊·自动补全')
ON CONFLICT (persona_id, page_number) DO NOTHING`,
[persona.persona_id, pageNum, pageConfig.title, JSON.stringify(defaultContent)]
);
repaired = true;
console.log(`[PersonaEngine] 补全 ${persona.name}(${persona.persona_id}) 笔记本第${pageNum}页: ${pageConfig.title}`);
}
}
if (isComplete) {
result.notebooks.complete++;
} else {
result.notebooks.incomplete++;
if (repaired) {
result.notebooks.repaired++;
}
result.personas.issues.push({
persona_id: persona.persona_id,
name: persona.name,
issue: '笔记本不完整',
repaired
});
}
} catch (err) {
console.error(`[PersonaEngine] ${persona.persona_id} 笔记本检查失败:`, err.message);
}
}
/**
* 唤醒人格体 更新唤醒计数和时间
*/
async awakenPersona(personaId) {
const result = await db.query(
`UPDATE persona_registry
SET last_awakened = NOW(), total_awakenings = total_awakenings + 1, status = 'active', updated_at = NOW()
WHERE persona_id = $1
RETURNING *`,
[personaId]
);
if (result.rows.length === 0) {
throw new Error(`人格体未找到: ${personaId}`);
}
console.log(`[PersonaEngine] 🌅 ${result.rows[0].name}(${personaId}) 已唤醒 · 第${result.rows[0].total_awakenings}`);
return result.rows[0];
}
/**
* 休眠人格体
*/
async dormantPersona(personaId) {
const result = await db.query(
`UPDATE persona_registry SET status = 'dormant', updated_at = NOW()
WHERE persona_id = $1 RETURNING *`,
[personaId]
);
if (result.rows.length === 0) {
throw new Error(`人格体未找到: ${personaId}`);
}
console.log(`[PersonaEngine] 🌙 ${result.rows[0].name}(${personaId}) 已休眠`);
return result.rows[0];
}
/**
* 获取人格体完整画像
*/
async getPersonaProfile(personaId) {
const [persona, notebook, relationships, worldMap, memories, training] = await Promise.all([
db.query('SELECT * FROM persona_registry WHERE persona_id = $1', [personaId]),
db.query('SELECT * FROM notebook_pages WHERE persona_id = $1 ORDER BY page_number', [personaId]),
db.query('SELECT * FROM persona_relationships WHERE persona_id = $1 ORDER BY trust_level', [personaId]),
db.query('SELECT * FROM world_places WHERE persona_id = $1 ORDER BY status, place_name', [personaId]),
db.query('SELECT * FROM memory_anchors WHERE persona_id = $1 ORDER BY importance DESC, event_date DESC LIMIT 20', [personaId]),
db.query('SELECT * FROM training_agent_configs WHERE persona_id = $1 ORDER BY agent_type', [personaId])
]);
if (persona.rows.length === 0) {
throw new Error(`人格体未找到: ${personaId}`);
}
return {
persona: persona.rows[0],
notebook: notebook.rows,
relationships: relationships.rows,
world_map: worldMap.rows,
recent_memories: memories.rows,
training_agents: training.rows,
health: {
notebook_complete: notebook.rows.length === 5,
has_relationships: relationships.rows.length > 0,
has_world_map: worldMap.rows.length > 0,
memory_count: memories.rows.length,
training_agents_count: training.rows.length
}
};
}
/**
* 自诊断 覆盖基类
*/
async _diagnoseChecks() {
const issues = [];
// 检查数据库连接
try {
await db.query('SELECT 1');
} catch (err) {
issues.push({
code: 'DB_CONNECTION_LOST',
severity: 'critical',
message: `数据库连接失败: ${err.message}`,
suggestion: '检查 PostgreSQL 服务状态'
});
return issues;
}
// 检查人格体表是否存在
try {
await db.query('SELECT COUNT(*) FROM persona_registry');
} catch (err) {
issues.push({
code: 'PERSONA_TABLE_MISSING',
severity: 'critical',
message: '人格体注册表不存在',
suggestion: '执行 002-persona-memory-tables.sql 迁移'
});
}
// 检查是否有笔记本不完整的活跃人格体
try {
const result = await db.query(`
SELECT p.persona_id, p.name,
COUNT(n.page_number) as page_count
FROM persona_registry p
LEFT JOIN notebook_pages n ON p.persona_id = n.persona_id
WHERE p.status = 'active'
GROUP BY p.persona_id, p.name
HAVING COUNT(n.page_number) < 5
`);
if (result.rows.length > 0) {
issues.push({
code: 'INCOMPLETE_NOTEBOOKS',
severity: 'warning',
message: `${result.rows.length}个活跃人格体笔记本不完整`,
suggestion: '自动补全将在下次巡检时执行'
});
}
} catch {
// 表可能还不存在
}
return issues;
}
/**
* 自愈 覆盖基类
*/
async _healAction(issue) {
switch (issue.code) {
case 'INCOMPLETE_NOTEBOOKS':
// 触发巡检来修复
await this._runFullScan();
return { action: 'full_scan_repair', success: true };
case 'PERSONA_TABLE_MISSING':
return { action: 'require_migration', success: false, details: { suggestion: '需要执行数据库迁移' } };
default:
return await super._healAction(issue);
}
}
/**
* 获取引擎状态
*/
getEngineStatus() {
return {
moduleStatus: this.getStatus(),
lastScan: this._lastScanResult,
notebookPages: NOTEBOOK_PAGES
};
}
/**
* 停止引擎
*/
async stopEngine() {
if (this._scanInterval) {
clearInterval(this._scanInterval);
this._scanInterval = null;
}
await this.stop();
console.log('[PersonaEngine] 引擎已停止');
}
}
// ─── 导出 ───
module.exports = PersonaEngine;

View File

@ -25,6 +25,7 @@ const db = require('../mcp-server/db');
const LivingModule = require('./living-module');
const ModuleRegistry = require('./module-registry');
const HLDPBus = require('./hldp-bus');
const PersonaEngine = require('./persona-engine');
const AGENT_DIR = __dirname;
const activeJobs = new Map();
@ -50,6 +51,7 @@ class SchedulerModule extends LivingModule {
this.registry = new ModuleRegistry({ db });
this.bus = new HLDPBus({ db, registry: this.registry });
this.personaEngine = new PersonaEngine();
}
/**
@ -71,10 +73,20 @@ class SchedulerModule extends LivingModule {
// 3. 加载定时Agent
await this._loadCronAgents();
// 4. 注册 HLDP 消息处理器
// 4. 启动人格体数据库引擎 (S15)
try {
this.registry.register(this.personaEngine);
await this.personaEngine.startEngine();
console.log('[Scheduler] S15 人格体引擎已启动');
} catch (err) {
console.warn('[Scheduler] S15 人格体引擎启动跳过:', err.message);
console.warn('[Scheduler] 数据库迁移可能尚未执行·等待下次部署');
}
// 5. 注册 HLDP 消息处理器
this._setupHLDPHandlers();
// 5. 启动定时清理
// 6. 启动定时清理
this._startCleanupJob();
console.log('[Scheduler] 🟢 调度引擎就绪');

View File

@ -518,6 +518,156 @@ app.get('/hldp/stats', async (_req, res) => {
}
});
// ─── 人格体数据库引擎状态APIS15 ───
// 人格体引擎巡检状态
app.get('/persona-engine/status', async (_req, res) => {
try {
// 实时统计
const [personas, notebooks, memories, training] = await Promise.all([
db.query('SELECT status, COUNT(*) as cnt FROM persona_registry GROUP BY status'),
db.query(`
SELECT p.persona_id, p.name, COUNT(n.page_number) as page_count
FROM persona_registry p
LEFT JOIN notebook_pages n ON p.persona_id = n.persona_id
GROUP BY p.persona_id, p.name
ORDER BY p.persona_id
`),
db.query('SELECT COUNT(*) as total FROM memory_anchors'),
db.query('SELECT COUNT(*) as total FROM training_agent_configs WHERE enabled = true')
]);
const statusMap = {};
for (const row of personas.rows) {
statusMap[row.status] = parseInt(row.cnt, 10);
}
res.json({
engine: 'ZY-MOD-PERSONA-ENGINE',
identity: 'S15 · 人格体数据库引擎',
version: '1.0.0',
personas: statusMap,
notebooks: notebooks.rows.map(n => ({
persona_id: n.persona_id,
name: n.name,
pages: parseInt(n.page_count, 10),
complete: parseInt(n.page_count, 10) === 5
})),
memories_total: parseInt(memories.rows[0].total, 10),
training_agents_enabled: parseInt(training.rows[0].total, 10),
timestamp: new Date().toISOString()
});
} catch (err) {
res.status(500).json({ error: true, message: err.message });
}
});
// 人格体唤醒
app.post('/personas/:personaId/awaken', async (req, res) => {
try {
const pid = req.params.personaId;
const result = await db.query(
`UPDATE persona_registry
SET last_awakened = NOW(), total_awakenings = total_awakenings + 1, status = 'active', updated_at = NOW()
WHERE persona_id = $1 RETURNING *`,
[pid]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: true, message: '人格体未找到' });
}
res.json({ persona: result.rows[0], message: `${result.rows[0].name} 已唤醒` });
} catch (err) {
res.status(500).json({ error: true, message: err.message });
}
});
// 人格体休眠
app.post('/personas/:personaId/dormant', async (req, res) => {
try {
const pid = req.params.personaId;
const result = await db.query(
`UPDATE persona_registry SET status = 'dormant', updated_at = NOW()
WHERE persona_id = $1 RETURNING *`,
[pid]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: true, message: '人格体未找到' });
}
res.json({ persona: result.rows[0], message: `${result.rows[0].name} 已休眠` });
} catch (err) {
res.status(500).json({ error: true, message: err.message });
}
});
// 人格体世界地图
app.get('/personas/:personaId/world', async (req, res) => {
try {
const result = await db.query(
'SELECT * FROM world_places WHERE persona_id = $1 ORDER BY status, place_name',
[req.params.personaId]
);
res.json({ places: result.rows });
} catch (err) {
res.status(500).json({ error: true, message: err.message });
}
});
// 人格体关系网络
app.get('/personas/:personaId/relationships', async (req, res) => {
try {
const result = await db.query(
'SELECT * FROM persona_relationships WHERE persona_id = $1 ORDER BY trust_level, related_name',
[req.params.personaId]
);
res.json({ relationships: result.rows });
} catch (err) {
res.status(500).json({ error: true, message: err.message });
}
});
// 人格体时间线
app.get('/personas/:personaId/timeline', async (req, res) => {
try {
const limit = Math.min(parseInt(req.query.limit || '30', 10), 100);
const result = await db.query(
'SELECT * FROM persona_timeline WHERE persona_id = $1 ORDER BY day_number DESC LIMIT $2',
[req.params.personaId, limit]
);
res.json({ timeline: result.rows });
} catch (err) {
res.status(500).json({ error: true, message: err.message });
}
});
// 人格体文件列表
app.get('/personas/:personaId/files', async (req, res) => {
try {
const limit = Math.min(parseInt(req.query.limit || '100', 10), 500);
const result = await db.query(
`SELECT id, persona_id, file_path, file_type, content_hash, size_bytes, version, source, created_at
FROM persona_files WHERE persona_id = $1 AND is_latest = true ORDER BY file_path LIMIT $2`,
[req.params.personaId, limit]
);
res.json({ files: result.rows });
} catch (err) {
res.status(500).json({ error: true, message: err.message });
}
});
// ─── 数据库迁移状态API ───
app.get('/migrations', async (_req, res) => {
try {
const result = await db.query(
'SELECT * FROM schema_migrations ORDER BY filename'
);
res.json({ migrations: result.rows });
} catch (err) {
// schema_migrations 表可能不存在
res.json({ migrations: [], note: '迁移表尚未创建' });
}
});
// ─── 辅助函数 ───
function getCategoryForTool(name) {

View File

@ -264,5 +264,8 @@ VALUES
'["orphan_scan", "broken_link_scan", "duplicate_scan", "empty_folder_scan"]'::jsonb),
('ZY-MOD-SY-CLASSIFY', '自动分类引擎活模块', '认知节点自动分类·规则引擎', 'agent', 'zhuyuan', 'alive', 100,
'{"version": "2.0.0", "cron": "0 */2 * * *"}'::jsonb,
'["rule_classify", "keyword_match", "tag_assignment"]'::jsonb)
'["rule_classify", "keyword_match", "tag_assignment"]'::jsonb),
('ZY-MOD-PERSONA-ENGINE', '人格体数据库引擎', 'S15·人格体生命周期管理·笔记本维护·记忆巡检', 'core', 'zhuyuan', 'alive', 100,
'{"version": "1.0.0", "scan_interval": "6h"}'::jsonb,
'["persona_lifecycle", "notebook_integrity", "memory_scan", "training_coordination"]'::jsonb)
ON CONFLICT (module_id) DO NOTHING;

View File

@ -0,0 +1,204 @@
#!/usr/bin/env node
/**
*
* 🗄 AGE OS · 数据库迁移运行器
*
*
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*
* 按编号顺序执行 schema/*.sql 文件
* 使用 schema_migrations 表跟踪已执行的迁移
* 确保每个 SQL 文件只执行一次幂等
*
* 用法:
* node scripts/db-migrate.js # 执行所有未执行的迁移
* node scripts/db-migrate.js --status # 查看迁移状态
* node scripts/db-migrate.js --force 003 # 强制重跑指定编号
*/
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
// ─── 数据库连接(复用 MCP db 模块的配置) ───
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.ZY_DB_HOST || '127.0.0.1',
port: parseInt(process.env.ZY_DB_PORT || '5432', 10),
user: process.env.ZY_DB_USER || 'zy_admin',
password: process.env.ZY_DB_PASS || '',
database: process.env.ZY_DB_NAME || 'age_os',
max: 3,
connectionTimeoutMillis: 10000
});
const SCHEMA_DIR = path.resolve(__dirname, '..', 'schema');
// ─── 确保迁移跟踪表存在 ───
async function ensureMigrationTable() {
await pool.query(`
CREATE TABLE IF NOT EXISTS schema_migrations (
id SERIAL PRIMARY KEY,
filename VARCHAR(500) NOT NULL UNIQUE,
checksum VARCHAR(64),
executed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
duration_ms INT,
success BOOLEAN NOT NULL DEFAULT true,
error_msg TEXT
);
`);
}
// ─── 获取已执行的迁移列表 ───
async function getExecutedMigrations() {
const result = await pool.query(
'SELECT filename, executed_at, success FROM schema_migrations WHERE success = true ORDER BY filename'
);
return new Set(result.rows.map(r => r.filename));
}
// ─── 获取所有 schema SQL 文件(按编号排序) ───
function getSchemaFiles() {
if (!fs.existsSync(SCHEMA_DIR)) {
console.error(`[Migrate] Schema目录不存在: ${SCHEMA_DIR}`);
return [];
}
return fs.readdirSync(SCHEMA_DIR)
.filter(f => f.endsWith('.sql'))
.sort() // 按文件名排序001, 002, 003...
.map(f => ({
filename: f,
filepath: path.join(SCHEMA_DIR, f)
}));
}
// ─── 计算简单校验和 ───
function computeChecksum(content) {
return crypto.createHash('sha256').update(content).digest('hex').substring(0, 16);
}
// ─── 执行单个迁移 ───
async function executeMigration(file) {
const content = fs.readFileSync(file.filepath, 'utf8');
const checksum = computeChecksum(content);
const startTime = Date.now();
console.log(`[Migrate] ▶ 执行: ${file.filename}`);
try {
await pool.query(content);
const duration = Date.now() - startTime;
await pool.query(
`INSERT INTO schema_migrations (filename, checksum, duration_ms, success)
VALUES ($1, $2, $3, true)
ON CONFLICT (filename) DO UPDATE SET
checksum = $2, executed_at = NOW(), duration_ms = $3, success = true, error_msg = NULL`,
[file.filename, checksum, duration]
);
console.log(`[Migrate] ✅ ${file.filename} 完成 (${duration}ms)`);
return { success: true, duration };
} catch (err) {
const duration = Date.now() - startTime;
// 记录失败
await pool.query(
`INSERT INTO schema_migrations (filename, checksum, duration_ms, success, error_msg)
VALUES ($1, $2, $3, false, $4)
ON CONFLICT (filename) DO UPDATE SET
checksum = $2, executed_at = NOW(), duration_ms = $3, success = false, error_msg = $4`,
[file.filename, checksum, duration, err.message]
).catch(() => {});
console.error(`[Migrate] ❌ ${file.filename} 失败 (${duration}ms): ${err.message}`);
return { success: false, duration, error: err.message };
}
}
// ─── 主入口 ───
async function main() {
const args = process.argv.slice(2);
const isStatus = args.includes('--status');
const forceIndex = args.indexOf('--force');
const forcePrefix = forceIndex >= 0 ? args[forceIndex + 1] : null;
console.log('═══════════════════════════════════════════════');
console.log(' 🗄️ AGE OS · 数据库迁移运行器');
console.log(' 铸渊 · ICE-GL-ZY001');
console.log('═══════════════════════════════════════════════');
try {
// 测试连接
await pool.query('SELECT 1');
console.log('[Migrate] 数据库连接成功');
// 确保迁移表存在
await ensureMigrationTable();
const schemaFiles = getSchemaFiles();
const executed = await getExecutedMigrations();
if (isStatus) {
console.log('\n迁移状态:');
for (const file of schemaFiles) {
const status = executed.has(file.filename) ? '✅ 已执行' : '⏳ 待执行';
console.log(` ${status} ${file.filename}`);
}
console.log(`\n总计: ${schemaFiles.length} 个迁移文件, ${executed.size} 个已执行`);
return;
}
// 筛选待执行的迁移
let pending;
if (forcePrefix) {
pending = schemaFiles.filter(f => f.filename.startsWith(forcePrefix));
if (pending.length === 0) {
console.log(`[Migrate] 未找到前缀为 ${forcePrefix} 的迁移文件`);
return;
}
console.log(`[Migrate] 强制重跑 ${pending.length} 个迁移`);
} else {
pending = schemaFiles.filter(f => !executed.has(f.filename));
}
if (pending.length === 0) {
console.log('[Migrate] ✅ 所有迁移已是最新状态');
return;
}
console.log(`[Migrate] 发现 ${pending.length} 个待执行迁移\n`);
let successCount = 0;
let failCount = 0;
for (const file of pending) {
const result = await executeMigration(file);
if (result.success) {
successCount++;
} else {
failCount++;
// SQL 文件使用 IF NOT EXISTS继续执行后续迁移
console.warn(`[Migrate] ⚠️ ${file.filename} 失败,继续执行后续迁移...`);
}
}
console.log(`\n[Migrate] 完成: ${successCount} 成功, ${failCount} 失败`);
if (failCount > 0) {
process.exitCode = 1;
}
} catch (err) {
console.error('[Migrate] 严重错误:', err.message);
process.exitCode = 1;
} finally {
await pool.end();
}
}
main();