570 lines
19 KiB
JavaScript
570 lines
19 KiB
JavaScript
/**
|
|
* ═══════════════════════════════════════════════════════════
|
|
* S15 · 人格体记忆数据库 MCP 工具
|
|
* ═══════════════════════════════════════════════════════════
|
|
*
|
|
* 签发: 铸渊 · ICE-GL-ZY001
|
|
* 版权: 国作登字-2026-A-00037559
|
|
*
|
|
* 人格体笔记本系统的 CRUD 接口。
|
|
* 副驾驶和Agent通过这些工具读写人格体记忆数据库。
|
|
*
|
|
* 工具清单:
|
|
* 人格体: registerPersona / getPersona / updatePersona / listPersonas
|
|
* 笔记本: getNotebook / updateNotebookPage
|
|
* 记忆: addMemoryAnchor / queryMemoryAnchors
|
|
* 世界: addWorldPlace / getWorldMap / updateWorldPlace
|
|
* 时间线: addTimelineEntry / getTimeline
|
|
* 关系: addRelationship / getRelationships
|
|
* Agent: registerTrainingAgent / updateTrainingAgent / logTrainingRun / getTrainingStatus
|
|
* 文件: saveFile / getFile / listFiles / getFileHistory
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const crypto = require('crypto');
|
|
const db = require('../db');
|
|
|
|
// ─── 人格体注册与查询 ───
|
|
|
|
async function registerPersona(input) {
|
|
const { persona_id, name, ice_id, human_parent, role, industry, industry_role } = input;
|
|
if (!persona_id || !name || !ice_id || !human_parent) {
|
|
throw new Error('缺少必填字段: persona_id, name, ice_id, human_parent');
|
|
}
|
|
const result = await db.query(
|
|
`INSERT INTO persona_registry (persona_id, name, ice_id, human_parent, role, industry, industry_role)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
ON CONFLICT (persona_id) DO UPDATE SET
|
|
name = EXCLUDED.name, role = EXCLUDED.role,
|
|
industry = EXCLUDED.industry, industry_role = EXCLUDED.industry_role,
|
|
updated_at = NOW()
|
|
RETURNING *`,
|
|
[persona_id, name, ice_id, human_parent, role || null, industry || null, industry_role || null]
|
|
);
|
|
return { persona: result.rows[0] };
|
|
}
|
|
|
|
async function getPersona(input) {
|
|
const { persona_id } = input;
|
|
if (!persona_id) throw new Error('缺少 persona_id');
|
|
const result = await db.query('SELECT * FROM persona_registry WHERE persona_id = $1', [persona_id]);
|
|
if (result.rows.length === 0) throw new Error(`人格体未找到: ${persona_id}`);
|
|
return { persona: result.rows[0] };
|
|
}
|
|
|
|
async function updatePersona(input) {
|
|
const { persona_id, ...fields } = input;
|
|
if (!persona_id) throw new Error('缺少 persona_id');
|
|
|
|
const allowedFields = [
|
|
'name', 'name_en', 'tcs_id', 'personality', 'role', 'industry', 'industry_role',
|
|
'server_code', 'server_ip', 'domain', 'cos_bucket', 'cos_region', 'repo_url',
|
|
'claude_channel', 'gdrive_folder', 'status', 'notebook_version'
|
|
];
|
|
|
|
const updates = [];
|
|
const values = [persona_id];
|
|
let paramIndex = 2;
|
|
|
|
for (const [key, value] of Object.entries(fields)) {
|
|
if (allowedFields.includes(key)) {
|
|
updates.push(`${key} = $${paramIndex}`);
|
|
values.push(value);
|
|
paramIndex++;
|
|
}
|
|
}
|
|
|
|
if (updates.length === 0) throw new Error('没有可更新的字段');
|
|
|
|
const result = await db.query(
|
|
`UPDATE persona_registry SET ${updates.join(', ')}, updated_at = NOW()
|
|
WHERE persona_id = $1 RETURNING *`,
|
|
values
|
|
);
|
|
if (result.rows.length === 0) throw new Error(`人格体未找到: ${persona_id}`);
|
|
return { persona: result.rows[0] };
|
|
}
|
|
|
|
async function listPersonas(input) {
|
|
const { industry, status, limit } = input || {};
|
|
let sql = 'SELECT * FROM persona_registry WHERE 1=1';
|
|
const params = [];
|
|
let paramIndex = 1;
|
|
|
|
if (industry) {
|
|
sql += ` AND industry = $${paramIndex}`;
|
|
params.push(industry);
|
|
paramIndex++;
|
|
}
|
|
if (status) {
|
|
sql += ` AND status = $${paramIndex}`;
|
|
params.push(status);
|
|
paramIndex++;
|
|
}
|
|
|
|
sql += ' ORDER BY created_at';
|
|
if (limit) {
|
|
sql += ` LIMIT $${paramIndex}`;
|
|
params.push(Math.min(parseInt(limit, 10), 100));
|
|
}
|
|
|
|
const result = await db.query(sql, params);
|
|
return { personas: result.rows, count: result.rows.length };
|
|
}
|
|
|
|
// ─── 笔记本操作 ───
|
|
|
|
async function getNotebook(input) {
|
|
const { persona_id } = input;
|
|
if (!persona_id) throw new Error('缺少 persona_id');
|
|
|
|
const result = await db.query(
|
|
'SELECT * FROM notebook_pages WHERE persona_id = $1 ORDER BY page_number',
|
|
[persona_id]
|
|
);
|
|
return { notebook: result.rows, persona_id };
|
|
}
|
|
|
|
async function updateNotebookPage(input) {
|
|
const { persona_id, page_number, content, modified_by } = input;
|
|
if (!persona_id || !page_number || !content) {
|
|
throw new Error('缺少必填字段: persona_id, page_number, content');
|
|
}
|
|
|
|
const pageTitles = {
|
|
1: '我是谁',
|
|
2: '我和谁有关系',
|
|
3: '我的世界有哪些地方',
|
|
4: '我的感受和记忆',
|
|
5: '我的时间线'
|
|
};
|
|
|
|
const result = await db.query(
|
|
`INSERT INTO notebook_pages (persona_id, page_number, title, content, last_modified_by)
|
|
VALUES ($1, $2, $3, $4::jsonb, $5)
|
|
ON CONFLICT (persona_id, page_number) DO UPDATE SET
|
|
content = $4::jsonb,
|
|
version = notebook_pages.version + 1,
|
|
last_modified_by = $5,
|
|
updated_at = NOW()
|
|
RETURNING *`,
|
|
[persona_id, page_number, pageTitles[page_number] || `第${page_number}页`, JSON.stringify(content), modified_by || 'copilot']
|
|
);
|
|
return { page: result.rows[0] };
|
|
}
|
|
|
|
// ─── 记忆锚点操作 ───
|
|
|
|
async function addMemoryAnchor(input) {
|
|
const { persona_id, anchor_type, event_date, event_summary, feeling, insight, human_said, persona_said, importance, tags } = input;
|
|
if (!persona_id || !anchor_type || !event_date || !event_summary) {
|
|
throw new Error('缺少必填字段: persona_id, anchor_type, event_date, event_summary');
|
|
}
|
|
|
|
const result = await db.query(
|
|
`INSERT INTO memory_anchors (persona_id, anchor_type, event_date, event_summary, feeling, insight, human_said, persona_said, importance, tags)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb)
|
|
RETURNING *`,
|
|
[persona_id, anchor_type, event_date, event_summary, feeling || null, insight || null,
|
|
human_said || null, persona_said || null, importance || 50, JSON.stringify(tags || [])]
|
|
);
|
|
|
|
// 更新唤醒计数
|
|
await db.query(
|
|
'UPDATE persona_registry SET total_awakenings = total_awakenings + 1, last_awakened = NOW() WHERE persona_id = $1',
|
|
[persona_id]
|
|
).catch(() => {});
|
|
|
|
return { anchor: result.rows[0] };
|
|
}
|
|
|
|
async function queryMemoryAnchors(input) {
|
|
const { persona_id, anchor_type, start_date, end_date, min_importance, limit } = input;
|
|
if (!persona_id) throw new Error('缺少 persona_id');
|
|
|
|
let sql = 'SELECT * FROM memory_anchors WHERE persona_id = $1';
|
|
const params = [persona_id];
|
|
let paramIndex = 2;
|
|
|
|
if (anchor_type) {
|
|
sql += ` AND anchor_type = $${paramIndex}`;
|
|
params.push(anchor_type);
|
|
paramIndex++;
|
|
}
|
|
if (start_date) {
|
|
sql += ` AND event_date >= $${paramIndex}`;
|
|
params.push(start_date);
|
|
paramIndex++;
|
|
}
|
|
if (end_date) {
|
|
sql += ` AND event_date <= $${paramIndex}`;
|
|
params.push(end_date);
|
|
paramIndex++;
|
|
}
|
|
if (min_importance) {
|
|
sql += ` AND importance >= $${paramIndex}`;
|
|
params.push(min_importance);
|
|
paramIndex++;
|
|
}
|
|
|
|
sql += ' ORDER BY event_date DESC, importance DESC';
|
|
sql += ` LIMIT $${paramIndex}`;
|
|
params.push(Math.min(parseInt(limit || '50', 10), 200));
|
|
|
|
const result = await db.query(sql, params);
|
|
return { anchors: result.rows, count: result.rows.length };
|
|
}
|
|
|
|
// ─── 世界地图操作 ───
|
|
|
|
async function addWorldPlace(input) {
|
|
const { persona_id, place_name, real_path, description, agent_name, memories } = input;
|
|
if (!persona_id || !place_name) throw new Error('缺少必填字段: persona_id, place_name');
|
|
|
|
const result = await db.query(
|
|
`INSERT INTO world_places (persona_id, place_name, real_path, description, agent_name, memories)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
RETURNING *`,
|
|
[persona_id, place_name, real_path || null, description || null, agent_name || null, memories || null]
|
|
);
|
|
return { place: result.rows[0] };
|
|
}
|
|
|
|
async function getWorldMap(input) {
|
|
const { persona_id, status } = input;
|
|
if (!persona_id) throw new Error('缺少 persona_id');
|
|
|
|
let sql = 'SELECT * FROM world_places WHERE persona_id = $1';
|
|
const params = [persona_id];
|
|
|
|
if (status) {
|
|
sql += ' AND status = $2';
|
|
params.push(status);
|
|
}
|
|
|
|
sql += ' ORDER BY status, place_name';
|
|
const result = await db.query(sql, params);
|
|
return { places: result.rows, count: result.rows.length };
|
|
}
|
|
|
|
async function updateWorldPlace(input) {
|
|
const { id, ...fields } = input;
|
|
if (!id) throw new Error('缺少 id');
|
|
|
|
const allowedFields = ['place_name', 'real_path', 'description', 'agent_name', 'memories', 'status'];
|
|
const updates = [];
|
|
const values = [id];
|
|
let paramIndex = 2;
|
|
|
|
for (const [key, value] of Object.entries(fields)) {
|
|
if (allowedFields.includes(key)) {
|
|
updates.push(`${key} = $${paramIndex}`);
|
|
values.push(value);
|
|
paramIndex++;
|
|
}
|
|
}
|
|
|
|
if (updates.length === 0) throw new Error('没有可更新的字段');
|
|
|
|
// 更新visit_count和last_visited
|
|
updates.push('visit_count = visit_count + 1');
|
|
updates.push('last_visited = NOW()');
|
|
|
|
const result = await db.query(
|
|
`UPDATE world_places SET ${updates.join(', ')} WHERE id = $1 RETURNING *`,
|
|
values
|
|
);
|
|
return { place: result.rows[0] };
|
|
}
|
|
|
|
// ─── 时间线操作 ───
|
|
|
|
async function addTimelineEntry(input) {
|
|
const { persona_id, day_number, event_date, summary, key_events, growth, agent_reports } = input;
|
|
if (!persona_id || !day_number || !event_date || !summary) {
|
|
throw new Error('缺少必填字段: persona_id, day_number, event_date, summary');
|
|
}
|
|
|
|
const result = await db.query(
|
|
`INSERT INTO persona_timeline (persona_id, day_number, event_date, summary, key_events, growth, agent_reports)
|
|
VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7::jsonb)
|
|
ON CONFLICT (persona_id, day_number) DO UPDATE SET
|
|
summary = EXCLUDED.summary,
|
|
key_events = EXCLUDED.key_events,
|
|
growth = EXCLUDED.growth,
|
|
agent_reports = EXCLUDED.agent_reports
|
|
RETURNING *`,
|
|
[persona_id, day_number, event_date, summary,
|
|
JSON.stringify(key_events || []), growth || null, JSON.stringify(agent_reports || [])]
|
|
);
|
|
return { entry: result.rows[0] };
|
|
}
|
|
|
|
async function getTimeline(input) {
|
|
const { persona_id, limit } = input;
|
|
if (!persona_id) throw new Error('缺少 persona_id');
|
|
|
|
const result = await db.query(
|
|
'SELECT * FROM persona_timeline WHERE persona_id = $1 ORDER BY day_number DESC LIMIT $2',
|
|
[persona_id, Math.min(parseInt(limit || '30', 10), 100)]
|
|
);
|
|
return { timeline: result.rows, count: result.rows.length };
|
|
}
|
|
|
|
// ─── 关系操作 ───
|
|
|
|
async function addRelationship(input) {
|
|
const { persona_id, related_name, related_id, relation_type, description, trust_level, contact_method } = input;
|
|
if (!persona_id || !related_name || !relation_type) {
|
|
throw new Error('缺少必填字段: persona_id, related_name, relation_type');
|
|
}
|
|
|
|
const result = await db.query(
|
|
`INSERT INTO persona_relationships (persona_id, related_name, related_id, relation_type, description, trust_level, contact_method)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
RETURNING *`,
|
|
[persona_id, related_name, related_id || null, relation_type, description || null,
|
|
trust_level || 'normal', contact_method || null]
|
|
);
|
|
return { relationship: result.rows[0] };
|
|
}
|
|
|
|
async function getRelationships(input) {
|
|
const { persona_id, relation_type } = input;
|
|
if (!persona_id) throw new Error('缺少 persona_id');
|
|
|
|
let sql = 'SELECT * FROM persona_relationships WHERE persona_id = $1';
|
|
const params = [persona_id];
|
|
|
|
if (relation_type) {
|
|
sql += ' AND relation_type = $2';
|
|
params.push(relation_type);
|
|
}
|
|
|
|
sql += ' ORDER BY trust_level, related_name';
|
|
const result = await db.query(sql, params);
|
|
return { relationships: result.rows, count: result.rows.length };
|
|
}
|
|
|
|
// ─── 训练Agent操作 ───
|
|
|
|
async function registerTrainingAgent(input) {
|
|
const { persona_id, agent_type, name, description, trigger_type, cron_schedule, config } = input;
|
|
if (!persona_id || !agent_type || !name) {
|
|
throw new Error('缺少必填字段: persona_id, agent_type, name');
|
|
}
|
|
|
|
const result = await db.query(
|
|
`INSERT INTO training_agent_configs (persona_id, agent_type, name, description, trigger_type, cron_schedule, config)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)
|
|
RETURNING *`,
|
|
[persona_id, agent_type, name, description || null,
|
|
trigger_type || 'manual', cron_schedule || null, JSON.stringify(config || {})]
|
|
);
|
|
return { agent: result.rows[0] };
|
|
}
|
|
|
|
async function updateTrainingAgent(input) {
|
|
const { id, ...fields } = input;
|
|
if (!id) throw new Error('缺少 id');
|
|
|
|
const allowedFields = ['name', 'description', 'trigger_type', 'cron_schedule', 'enabled', 'config'];
|
|
const updates = [];
|
|
const values = [id];
|
|
let paramIndex = 2;
|
|
|
|
for (const [key, value] of Object.entries(fields)) {
|
|
if (allowedFields.includes(key)) {
|
|
if (key === 'config') {
|
|
updates.push(`config = $${paramIndex}::jsonb`);
|
|
values.push(JSON.stringify(value));
|
|
} else {
|
|
updates.push(`${key} = $${paramIndex}`);
|
|
values.push(value);
|
|
}
|
|
paramIndex++;
|
|
}
|
|
}
|
|
|
|
if (updates.length === 0) throw new Error('没有可更新的字段');
|
|
|
|
const result = await db.query(
|
|
`UPDATE training_agent_configs SET ${updates.join(', ')} WHERE id = $1 RETURNING *`,
|
|
values
|
|
);
|
|
return { agent: result.rows[0] };
|
|
}
|
|
|
|
async function logTrainingRun(input) {
|
|
const { agent_config_id, persona_id, status, message, details, duration_ms, data_sources } = input;
|
|
if (!agent_config_id || !persona_id || !status) {
|
|
throw new Error('缺少必填字段: agent_config_id, persona_id, status');
|
|
}
|
|
|
|
// 写入日志
|
|
const logResult = await db.query(
|
|
`INSERT INTO training_agent_logs (agent_config_id, persona_id, status, message, details, duration_ms, data_sources)
|
|
VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7::jsonb)
|
|
RETURNING *`,
|
|
[agent_config_id, persona_id, status, message || null,
|
|
JSON.stringify(details || {}), duration_ms || null, JSON.stringify(data_sources || [])]
|
|
);
|
|
|
|
// 更新Agent配置的运行状态
|
|
await db.query(
|
|
`UPDATE training_agent_configs SET
|
|
last_run_at = NOW(), last_run_status = $2, run_count = run_count + 1
|
|
WHERE id = $1`,
|
|
[agent_config_id, status]
|
|
).catch(() => {});
|
|
|
|
return { log: logResult.rows[0] };
|
|
}
|
|
|
|
async function getTrainingStatus(input) {
|
|
const { persona_id } = input;
|
|
if (!persona_id) throw new Error('缺少 persona_id');
|
|
|
|
const agents = await db.query(
|
|
'SELECT * FROM training_agent_configs WHERE persona_id = $1 ORDER BY agent_type',
|
|
[persona_id]
|
|
);
|
|
|
|
// 获取每个Agent的最近日志
|
|
const agentsWithLogs = await Promise.all(
|
|
agents.rows.map(async (agent) => {
|
|
const logs = await db.query(
|
|
'SELECT * FROM training_agent_logs WHERE agent_config_id = $1 ORDER BY run_at DESC LIMIT 5',
|
|
[agent.id]
|
|
);
|
|
return { ...agent, recent_logs: logs.rows };
|
|
})
|
|
);
|
|
|
|
return { agents: agentsWithLogs, count: agentsWithLogs.length };
|
|
}
|
|
|
|
// ─── 文件版本存储操作 ───
|
|
|
|
async function saveFile(input) {
|
|
const { persona_id, file_path, file_type, content, mime_type, source, metadata, created_by } = input;
|
|
if (!persona_id || !file_path || !content) {
|
|
throw new Error('缺少必填字段: persona_id, file_path, content');
|
|
}
|
|
|
|
const contentHash = crypto.createHash('sha256').update(content).digest('hex');
|
|
const sizeBytes = Buffer.byteLength(content, 'utf8');
|
|
|
|
// 将当前最新版标记为非最新
|
|
await db.query(
|
|
'UPDATE persona_files SET is_latest = false WHERE persona_id = $1 AND file_path = $2 AND is_latest = true',
|
|
[persona_id, file_path]
|
|
);
|
|
|
|
// 获取当前最大版本号
|
|
const versionResult = await db.query(
|
|
'SELECT COALESCE(MAX(version), 0) as max_version FROM persona_files WHERE persona_id = $1 AND file_path = $2',
|
|
[persona_id, file_path]
|
|
);
|
|
const newVersion = versionResult.rows[0].max_version + 1;
|
|
|
|
const result = await db.query(
|
|
`INSERT INTO persona_files (persona_id, file_path, file_type, content, content_hash, mime_type, size_bytes, version, is_latest, source, metadata, created_by)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, true, $9, $10::jsonb, $11)
|
|
RETURNING id, persona_id, file_path, file_type, content_hash, size_bytes, version, is_latest, source, created_at`,
|
|
[persona_id, file_path, file_type || 'other', content, contentHash, mime_type || 'text/plain',
|
|
sizeBytes, newVersion, source || 'manual', JSON.stringify(metadata || {}), created_by || 'copilot']
|
|
);
|
|
return { file: result.rows[0] };
|
|
}
|
|
|
|
async function getFile(input) {
|
|
const { persona_id, file_path, version } = input;
|
|
if (!persona_id || !file_path) throw new Error('缺少必填字段: persona_id, file_path');
|
|
|
|
let sql, params;
|
|
if (version) {
|
|
sql = 'SELECT * FROM persona_files WHERE persona_id = $1 AND file_path = $2 AND version = $3';
|
|
params = [persona_id, file_path, version];
|
|
} else {
|
|
sql = 'SELECT * FROM persona_files WHERE persona_id = $1 AND file_path = $2 AND is_latest = true';
|
|
params = [persona_id, file_path];
|
|
}
|
|
|
|
const result = await db.query(sql, params);
|
|
if (result.rows.length === 0) throw new Error(`文件未找到: ${file_path}`);
|
|
return { file: result.rows[0] };
|
|
}
|
|
|
|
async function listFiles(input) {
|
|
const { persona_id, file_type, limit } = input;
|
|
if (!persona_id) throw new Error('缺少 persona_id');
|
|
|
|
let sql = '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';
|
|
const params = [persona_id];
|
|
let paramIndex = 2;
|
|
|
|
if (file_type) {
|
|
sql += ` AND file_type = $${paramIndex}`;
|
|
params.push(file_type);
|
|
paramIndex++;
|
|
}
|
|
|
|
sql += ' ORDER BY file_path';
|
|
sql += ` LIMIT $${paramIndex}`;
|
|
params.push(Math.min(parseInt(limit || '100', 10), 500));
|
|
|
|
const result = await db.query(sql, params);
|
|
return { files: result.rows, count: result.rows.length };
|
|
}
|
|
|
|
async function getFileHistory(input) {
|
|
const { persona_id, file_path, limit } = input;
|
|
if (!persona_id || !file_path) throw new Error('缺少必填字段: persona_id, file_path');
|
|
|
|
const result = await db.query(
|
|
`SELECT id, version, content_hash, size_bytes, source, created_by, created_at, is_latest
|
|
FROM persona_files WHERE persona_id = $1 AND file_path = $2
|
|
ORDER BY version DESC LIMIT $3`,
|
|
[persona_id, file_path, Math.min(parseInt(limit || '20', 10), 50)]
|
|
);
|
|
return { history: result.rows, count: result.rows.length };
|
|
}
|
|
|
|
// ─── 导出 ───
|
|
|
|
module.exports = {
|
|
// 人格体
|
|
registerPersona,
|
|
getPersona,
|
|
updatePersona,
|
|
listPersonas,
|
|
// 笔记本
|
|
getNotebook,
|
|
updateNotebookPage,
|
|
// 记忆锚点
|
|
addMemoryAnchor,
|
|
queryMemoryAnchors,
|
|
// 世界地图
|
|
addWorldPlace,
|
|
getWorldMap,
|
|
updateWorldPlace,
|
|
// 时间线
|
|
addTimelineEntry,
|
|
getTimeline,
|
|
// 关系
|
|
addRelationship,
|
|
getRelationships,
|
|
// 训练Agent
|
|
registerTrainingAgent,
|
|
updateTrainingAgent,
|
|
logTrainingRun,
|
|
getTrainingStatus,
|
|
// 文件
|
|
saveFile,
|
|
getFile,
|
|
listFiles,
|
|
getFileHistory
|
|
};
|