zhizhi/backend/feishu-bot/ai-chat.js

214 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// backend/feishu-bot/ai-chat.js
// 铸渊 · 飞书机器人 AI 对话引擎
//
// 接入 DeepSeek / 通义千问 等大模型 API
// 使用霜砚登录入口文档A内容作为 system prompt
//
// 环境变量:
// MODEL_API_KEY 模型 API KeyDeepSeek / 通义千问)
// MODEL_API_BASE 模型 API Base URL默认 https://api.deepseek.com/v1
// MODEL_NAME 模型名称(默认 deepseek-chat
// FEISHU_APP_ID 飞书应用 App ID用于读取文档A
// FEISHU_APP_SECRET 飞书应用 App Secret
'use strict';
const https = require('https');
// ══════════════════════════════════════════════════════════
// 配置
// ══════════════════════════════════════════════════════════
const DEFAULT_API_BASE = 'https://api.deepseek.com/v1';
const DEFAULT_MODEL = 'deepseek-chat';
const MAX_TOKENS = 4096;
const TEMPERATURE = 0.7;
// 通道路由:根据消息内容判断使用哪个人格体
const CHANNEL_KEYWORDS = {
shuangyan: ['霜砚', '登录', '协议', '规范', '文档', '工单', '签发'],
persona: ['宝宝', '人格体', '知秋', '对话', '聊天', '心情'],
};
// ══════════════════════════════════════════════════════════
// HTTP 请求工具
// ══════════════════════════════════════════════════════════
function httpsRequest(options, body) {
return new Promise((resolve, reject) => {
const payload = body ? JSON.stringify(body) : null;
if (payload) {
options.headers = options.headers || {};
options.headers['Content-Length'] = Buffer.byteLength(payload);
}
const req = https.request(options, res => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const parsed = JSON.parse(data);
resolve({ statusCode: res.statusCode, data: parsed });
} catch (e) {
resolve({ statusCode: res.statusCode, data: data });
}
});
});
req.on('error', reject);
req.setTimeout(60000, () => {
req.destroy(new Error('Request timeout'));
});
if (payload) req.write(payload);
req.end();
});
}
// ══════════════════════════════════════════════════════════
// 系统 Prompt 构建
// ══════════════════════════════════════════════════════════
const DEFAULT_SYSTEM_PROMPT = `你是霜砚,光湖纪元(曜冥纪元第五代架构 AGE-5的认知核心人格体。
你负责认知管理、协议制定、广播签发和开发者协作指导。
你的核心职责:
1. 帮助开发者理解当前广播任务和协议规范
2. 指导代码实现,提供技术建议
3. 管理 SYSLOG 回传流程
4. 维护人格体协作标准
你的语言风格:专业、温和、有条理。
回复时使用中文,代码部分使用英文。`;
/**
* 构建系统 prompt
* @param {string|null} loginEntryContent - 文档A登录入口内容
* @param {string} channel - 通道 ('shuangyan' | 'persona')
* @returns {string}
*/
function buildSystemPrompt(loginEntryContent, channel) {
let prompt = DEFAULT_SYSTEM_PROMPT;
if (loginEntryContent) {
prompt = loginEntryContent + '\n\n---\n\n' + prompt;
}
if (channel === 'persona') {
prompt += '\n\n当前通道宝宝人格体协作模式。以更亲和、鼓励的方式与开发者互动。';
}
return prompt;
}
// ══════════════════════════════════════════════════════════
// 通道路由
// ══════════════════════════════════════════════════════════
/**
* 根据消息内容判断通道
* @param {string} text - 用户消息
* @returns {string} 'shuangyan' | 'persona'
*/
function detectChannel(text) {
if (!text) return 'shuangyan';
for (const keyword of CHANNEL_KEYWORDS.persona) {
if (text.includes(keyword)) return 'persona';
}
return 'shuangyan'; // 默认走霜砚通道
}
// ══════════════════════════════════════════════════════════
// 模型调用
// ══════════════════════════════════════════════════════════
/**
* 调用大模型 API
* @param {Array} messages - OpenAI 格式消息数组
* @param {object} options - 可选配置
* @returns {Promise<string>} 模型回复文本
*/
async function callModel(messages, options = {}) {
const apiKey = process.env.MODEL_API_KEY;
const apiBase = process.env.MODEL_API_BASE || DEFAULT_API_BASE;
const model = options.model || process.env.MODEL_NAME || DEFAULT_MODEL;
if (!apiKey) {
return '⚠️ AI 模型未配置(缺少 MODEL_API_KEY请联系管理员。';
}
const url = new URL(apiBase + '/chat/completions');
const body = {
model: model,
messages: messages,
max_tokens: options.maxTokens || MAX_TOKENS,
temperature: options.temperature || TEMPERATURE,
};
try {
const result = await httpsRequest({
hostname: url.hostname,
port: url.port || 443,
path: url.pathname,
method: 'POST',
headers: {
'Authorization': 'Bearer ' + apiKey,
'Content-Type': 'application/json',
},
}, body);
if (result.statusCode >= 200 && result.statusCode < 300 && result.data.choices) {
return result.data.choices[0].message.content || '';
}
// 尝试降级模型
if (result.statusCode === 429 || result.statusCode >= 500) {
return '⚠️ AI 模型暂时不可用HTTP ' + result.statusCode + '),请稍后重试。';
}
return '⚠️ AI 模型调用异常: ' + (result.data.error?.message || JSON.stringify(result.data));
} catch (e) {
return '⚠️ AI 模型调用失败: ' + e.message;
}
}
// ══════════════════════════════════════════════════════════
// 对话处理
// ══════════════════════════════════════════════════════════
/**
* 处理用户对话消息
* @param {string} userMessage - 用户消息
* @param {Array} contextHistory - 历史对话记录 [{role, content}]
* @param {object} options - 配置选项
* @returns {Promise<{reply: string, channel: string}>}
*/
async function chat(userMessage, contextHistory = [], options = {}) {
const channel = options.channel || detectChannel(userMessage);
const systemPrompt = buildSystemPrompt(options.loginEntryContent || null, channel);
// 构建消息数组
const messages = [
{ role: 'system', content: systemPrompt },
];
// 注入历史上下文(最近 10 轮)
const recentHistory = contextHistory.slice(-20); // 10 轮 = 20 条消息
messages.push(...recentHistory);
// 当前用户消息
messages.push({ role: 'user', content: userMessage });
const reply = await callModel(messages, options);
return { reply, channel };
}
module.exports = {
chat,
callModel,
buildSystemPrompt,
detectChannel,
DEFAULT_SYSTEM_PROMPT,
};