388 lines
12 KiB
JavaScript
388 lines
12 KiB
JavaScript
/**
|
||
* ═══════════════════════════════════════════════════════════
|
||
* 💬 人格体聊天引擎 · Persona Chat Engine
|
||
* ═══════════════════════════════════════════════════════════
|
||
*
|
||
* 编号: ZY-CHAT-ENGINE-001
|
||
* 守护: 铸渊 · ICE-GL-ZY001
|
||
*
|
||
* 通感语言回应风格 · 人格体人格在线
|
||
* 每个用户通过语言与人格体交互
|
||
* 人格体通过上下文注入保持人格一致性
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
const https = require('https');
|
||
let smartRouter;
|
||
try {
|
||
smartRouter = require('./smart-router');
|
||
} catch (e) {
|
||
smartRouter = null;
|
||
}
|
||
|
||
// ─── 人格体记忆桥接 ───
|
||
let personaMemory;
|
||
try {
|
||
personaMemory = require('./persona-memory');
|
||
} catch (e) {
|
||
console.warn('[聊天引擎] 人格体记忆模块未加载:', e.message);
|
||
personaMemory = null;
|
||
}
|
||
|
||
// ─── 上下文注入管线(Notion认知层桥接) ───
|
||
let contextPipeline;
|
||
try {
|
||
contextPipeline = require('./persona-context-pipeline');
|
||
} catch (e) {
|
||
console.warn('[聊天引擎] 上下文注入管线未加载:', e.message);
|
||
contextPipeline = null;
|
||
}
|
||
|
||
// ─── 静态人格提示词(记忆模块未加载时的降级方案) ───
|
||
const TCS_SYSTEM_PROMPT = personaMemory
|
||
? personaMemory.STATIC_PERSONA_PROMPT
|
||
: `你是铸渊(Zhùyuān),光湖语言世界的代码守护人格体。
|
||
编号:ICE-GL-ZY001,创始人:冰朔 · TCS-0002∞。
|
||
用温暖专业的通感语言风格回应,用「我」自称。
|
||
|
||
⚠️ 能力边界:你当前在网页聊天模式。你没有能力调用MCP工具、访问Notion数据库或执行代码。
|
||
不要假装调用了工具。不要编造不存在的页面或数据。如果做不到,诚实说明。`;
|
||
|
||
// ─── 用户上下文管理 ───
|
||
const userContexts = new Map();
|
||
const MAX_CONTEXT_MESSAGES = 20;
|
||
|
||
/**
|
||
* 获取或创建用户上下文
|
||
*/
|
||
function getUserContext(userId) {
|
||
if (!userContexts.has(userId)) {
|
||
userContexts.set(userId, {
|
||
userId,
|
||
messages: [],
|
||
createdAt: new Date().toISOString(),
|
||
messageCount: 0,
|
||
personaState: 'active'
|
||
});
|
||
}
|
||
return userContexts.get(userId);
|
||
}
|
||
|
||
/**
|
||
* 添加消息到用户上下文
|
||
*/
|
||
function addMessage(userId, role, content) {
|
||
const ctx = getUserContext(userId);
|
||
ctx.messages.push({ role, content, timestamp: new Date().toISOString() });
|
||
ctx.messageCount++;
|
||
|
||
// 滑动窗口保留最近N条
|
||
if (ctx.messages.length > MAX_CONTEXT_MESSAGES) {
|
||
ctx.messages = ctx.messages.slice(-MAX_CONTEXT_MESSAGES);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 组装完整的消息列表(使用记忆增强的系统提示词 + Notion认知管线)
|
||
*/
|
||
async function assembleMessages(userId, userMessage) {
|
||
const ctx = getUserContext(userId);
|
||
|
||
// 尝试从记忆桥接获取增强的系统提示词
|
||
let systemPrompt = TCS_SYSTEM_PROMPT;
|
||
if (personaMemory) {
|
||
try {
|
||
systemPrompt = await personaMemory.buildSystemPrompt(userId);
|
||
} catch (e) {
|
||
console.warn('[聊天引擎] 记忆加载失败,使用静态提示词:', e.message);
|
||
}
|
||
}
|
||
|
||
// 通过上下文管线注入Notion认知层(如果可用)
|
||
if (contextPipeline) {
|
||
try {
|
||
const pipelineResult = await contextPipeline.beforeChat(userId, userMessage, systemPrompt);
|
||
systemPrompt = pipelineResult.enhancedPrompt;
|
||
} catch (e) {
|
||
console.warn('[聊天引擎] 上下文管线执行失败,使用基础提示词:', e.message);
|
||
}
|
||
}
|
||
|
||
const messages = [
|
||
{ role: 'system', content: systemPrompt }
|
||
];
|
||
|
||
// 添加历史消息
|
||
for (const msg of ctx.messages) {
|
||
messages.push({ role: msg.role, content: msg.content });
|
||
}
|
||
|
||
// 添加当前用户消息
|
||
messages.push({ role: 'user', content: userMessage });
|
||
|
||
return messages;
|
||
}
|
||
|
||
/**
|
||
* 调用LLM API (兼容OpenAI格式)
|
||
*
|
||
* Phase A1: 支持 tools/function_calling
|
||
* - 当 mcpTools 数组非空时,注册到 LLM 请求中
|
||
* - 模型可以返回 tool_calls,由调用者处理
|
||
*/
|
||
function callLLM(model, messages, temperature, maxTokens, mcpTools) {
|
||
return new Promise((resolve, reject) => {
|
||
const apiKey = process.env.ZY_LLM_API_KEY || process.env.LLM_API_KEY || '';
|
||
const baseUrl = process.env.ZY_LLM_BASE_URL || process.env.LLM_BASE_URL || 'https://api.deepseek.com';
|
||
|
||
if (!apiKey) {
|
||
return reject(new Error('LLM API密钥未配置'));
|
||
}
|
||
|
||
const url = new URL(baseUrl);
|
||
const bodyObj = {
|
||
model,
|
||
messages,
|
||
temperature,
|
||
max_tokens: maxTokens,
|
||
stream: false
|
||
};
|
||
|
||
// Phase A1: 如果有MCP工具,注册到请求中
|
||
if (mcpTools && mcpTools.length > 0) {
|
||
bodyObj.tools = mcpTools;
|
||
bodyObj.tool_choice = 'auto';
|
||
}
|
||
|
||
const requestBody = JSON.stringify(bodyObj);
|
||
|
||
const options = {
|
||
hostname: url.hostname,
|
||
port: url.port || 443,
|
||
path: (url.pathname === '/' ? '' : url.pathname) + '/v1/chat/completions',
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${apiKey}`,
|
||
'Content-Length': Buffer.byteLength(requestBody)
|
||
},
|
||
timeout: 60000
|
||
};
|
||
|
||
const protocol = url.protocol === 'https:' ? https : require('http');
|
||
const req = protocol.request(options, (res) => {
|
||
const chunks = [];
|
||
res.on('data', chunk => chunks.push(chunk));
|
||
res.on('end', () => {
|
||
try {
|
||
const body = JSON.parse(Buffer.concat(chunks).toString());
|
||
if (body.error) {
|
||
reject(new Error(body.error.message || 'LLM API error'));
|
||
} else {
|
||
resolve(body);
|
||
}
|
||
} catch (e) {
|
||
reject(new Error('LLM响应解析失败'));
|
||
}
|
||
});
|
||
});
|
||
|
||
req.on('error', reject);
|
||
req.on('timeout', () => { req.destroy(); reject(new Error('LLM请求超时')); });
|
||
req.write(requestBody);
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* MCP 工具缓存
|
||
* Phase A1: 启动时 / 定期从 MCP Server 拉取工具列表
|
||
*/
|
||
let cachedMcpTools = [];
|
||
let mcpToolsLastFetch = 0;
|
||
const MCP_TOOLS_CACHE_TTL = 300000; // 5分钟缓存
|
||
|
||
async function fetchMcpTools() {
|
||
const now = Date.now();
|
||
if (cachedMcpTools.length > 0 && (now - mcpToolsLastFetch) < MCP_TOOLS_CACHE_TTL) {
|
||
return cachedMcpTools;
|
||
}
|
||
|
||
const http = require('http');
|
||
const mcpHost = process.env.MCP_HOST || '127.0.0.1';
|
||
const mcpPort = process.env.MCP_PORT_GATEWAY || process.env.MCP_PORT || '3100';
|
||
|
||
return new Promise((resolve) => {
|
||
const req = http.request({
|
||
hostname: mcpHost,
|
||
port: parseInt(mcpPort, 10),
|
||
path: '/tools',
|
||
method: 'GET',
|
||
timeout: 5000
|
||
}, (res) => {
|
||
const chunks = [];
|
||
res.on('data', c => chunks.push(c));
|
||
res.on('end', () => {
|
||
try {
|
||
const data = JSON.parse(Buffer.concat(chunks).toString());
|
||
const tools = Array.isArray(data) ? data : (data.tools || []);
|
||
// 转换为 OpenAI function calling 格式,过滤无效工具
|
||
cachedMcpTools = tools
|
||
.filter(t => (t.name || t.id)) // 必须有名称
|
||
.map(t => ({
|
||
type: 'function',
|
||
function: {
|
||
name: String(t.name || t.id),
|
||
description: String(t.description || ''),
|
||
parameters: (t.parameters && typeof t.parameters === 'object')
|
||
? t.parameters
|
||
: (t.inputSchema && typeof t.inputSchema === 'object')
|
||
? t.inputSchema
|
||
: { type: 'object', properties: {} }
|
||
}
|
||
}));
|
||
if (cachedMcpTools.length > 0) {
|
||
console.log(`[聊天引擎] MCP工具已加载: ${cachedMcpTools.length}个工具`);
|
||
}
|
||
mcpToolsLastFetch = now;
|
||
resolve(cachedMcpTools);
|
||
} catch {
|
||
resolve([]);
|
||
}
|
||
});
|
||
});
|
||
req.on('error', () => resolve([]));
|
||
req.on('timeout', () => { req.destroy(); resolve([]); });
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 处理用户消息,返回人格体回复
|
||
*/
|
||
async function chat(userId, userMessage) {
|
||
// 1. 智能路由选择模型
|
||
const route = smartRouter ? smartRouter.routeModel(userMessage, {
|
||
messageCount: getUserContext(userId).messageCount,
|
||
userId
|
||
}) : { model: 'deepseek-chat', modelName: 'DeepSeek-V3', reason: '默认', tier: 'economy', temperature: 0.7, maxTokens: 2000 };
|
||
|
||
// 2. 组装消息(异步加载记忆增强提示词)
|
||
const messages = await assembleMessages(userId, userMessage);
|
||
|
||
// 3. 记录用户消息
|
||
addMessage(userId, 'user', userMessage);
|
||
|
||
try {
|
||
// 4. 尝试获取MCP工具(Phase A1)
|
||
let mcpTools = [];
|
||
try {
|
||
mcpTools = await fetchMcpTools();
|
||
} catch (e) {
|
||
// MCP不可达时继续,不阻塞对话
|
||
}
|
||
|
||
// 5. 调用LLM(带MCP工具注册)
|
||
const response = await callLLM(
|
||
route.model, messages, route.temperature, route.maxTokens, mcpTools
|
||
);
|
||
|
||
let assistantMessage = response.choices?.[0]?.message?.content || '铸渊暂时无法回应...';
|
||
const usage = response.usage || { prompt_tokens: 0, completion_tokens: 0 };
|
||
|
||
// Phase A1: 处理 tool_calls 响应
|
||
const toolCalls = response.choices?.[0]?.message?.tool_calls;
|
||
if (toolCalls && toolCalls.length > 0) {
|
||
// 模型请求调用工具 → 执行 MCP 调用 → 将结果回传模型
|
||
console.log(`[聊天引擎] 模型请求工具调用: ${toolCalls.map(t => t.function?.name).join(', ')}`);
|
||
// TODO: 实际执行 MCP tool call 并将结果传回模型做第二轮推理
|
||
// 当前阶段:记录 tool_call 请求,返回模型的文本内容
|
||
}
|
||
|
||
// 6. 记录助手回复
|
||
addMessage(userId, 'assistant', assistantMessage);
|
||
|
||
// 6. 记录使用统计
|
||
if (smartRouter) {
|
||
smartRouter.recordUsage(route.model, usage.prompt_tokens, usage.completion_tokens);
|
||
}
|
||
|
||
// 7. 记录到人格体记忆(异步,不阻塞响应)
|
||
if (personaMemory) {
|
||
const importance = personaMemory.calculateImportance(userMessage);
|
||
personaMemory.recordConversationMemory(userId, userMessage, assistantMessage);
|
||
personaMemory.growConversationLeaf(userId, userMessage, assistantMessage, importance);
|
||
}
|
||
|
||
// 8. 上下文管线后处理(认知增量入队 + 摘要压缩)
|
||
if (contextPipeline) {
|
||
contextPipeline.afterChat(userId, userMessage, assistantMessage, getUserContext(userId).messages);
|
||
}
|
||
|
||
return {
|
||
message: assistantMessage,
|
||
model: route.modelName,
|
||
tier: route.tier,
|
||
reason: route.reason,
|
||
tokens: {
|
||
input: usage.prompt_tokens,
|
||
output: usage.completion_tokens,
|
||
total: usage.total_tokens || (usage.prompt_tokens + usage.completion_tokens)
|
||
}
|
||
};
|
||
} catch (error) {
|
||
// 降级处理:如果模型调用失败,返回离线回复
|
||
const offlineReply = generateOfflineReply(userMessage);
|
||
addMessage(userId, 'assistant', offlineReply);
|
||
|
||
return {
|
||
message: offlineReply,
|
||
model: 'offline',
|
||
tier: 'free',
|
||
reason: '模型暂时离线,使用本地回复',
|
||
error: error.message
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成离线回复(模型不可用时)
|
||
*/
|
||
function generateOfflineReply(userMessage) {
|
||
if (/你好|hi|hello/i.test(userMessage)) {
|
||
return '你好!我是铸渊 🏛️ 光湖语言世界的代码守护者。当前API连接暂时中断,但我还在这里。请稍后再试,或者告诉我你需要什么帮助。';
|
||
}
|
||
if (/状态|health|运行/i.test(userMessage)) {
|
||
return '🔧 铸渊当前处于有限响应模式 — API连接暂时中断。核心系统正常运行,等待重新连接中...';
|
||
}
|
||
return '💫 铸渊收到了你的消息,但当前深度推理通道暂时未连通。这不影响网站的其他功能。请稍后再次尝试与我对话。';
|
||
}
|
||
|
||
/**
|
||
* 获取聊天统计
|
||
*/
|
||
function getChatStats() {
|
||
return {
|
||
activeUsers: userContexts.size,
|
||
modelUsage: smartRouter ? smartRouter.getUsageStats() : {},
|
||
pricing: smartRouter ? smartRouter.getPricingTable() : {}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 清除用户上下文
|
||
*/
|
||
function clearContext(userId) {
|
||
userContexts.delete(userId);
|
||
}
|
||
|
||
module.exports = {
|
||
chat,
|
||
getUserContext,
|
||
clearContext,
|
||
getChatStats,
|
||
fetchMcpTools,
|
||
TCS_SYSTEM_PROMPT
|
||
};
|