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:
parent
35ec423fde
commit
2d386000b8
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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] 🟢 调度引擎就绪');
|
||||
|
|
|
|||
|
|
@ -518,6 +518,156 @@ app.get('/hldp/stats', async (_req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
// ─── 人格体数据库引擎状态API(S15) ───
|
||||
|
||||
// 人格体引擎巡检状态
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
Loading…
Reference in New Issue