prompt-discovery-mcp
Version:
基于Nacos的动态提示词MCP服务器,支持智能推荐和实时热更新
391 lines (390 loc) • 13.6 kB
JavaScript
import { NacosConfigClient } from 'nacos-config';
import * as fs from 'fs';
import * as path from 'path';
import { EventEmitter } from 'events';
import JSON5 from 'json5';
/**
* Nacos服务类,处理与Nacos的连接和配置管理
*/
export class NacosService extends EventEmitter {
options;
client;
cacheFilePath;
configData = null;
refreshTimer = null;
reconnectTimer = null;
isConnected = false;
reconnectAttempts = 0;
maxReconnectAttempts = 10;
/**
* 构造函数
* @param options Nacos服务配置选项
*/
constructor(options) {
super();
this.options = {
cachePath: path.join(process.cwd(), '.cache'),
refreshInterval: 30000, // 默认30秒刷新一次
...options
};
this.client = new NacosConfigClient({
serverAddr: this.options.serverAddr,
namespace: this.options.namespace,
username: this.options.username,
password: this.options.password,
requestTimeout: this.options.requestTimeout || 6000,
});
// 确保缓存目录存在
if (!fs.existsSync(this.options.cachePath)) {
fs.mkdirSync(this.options.cachePath, { recursive: true });
}
this.cacheFilePath = path.join(this.options.cachePath, `${this.options.configId}.json`);
}
/**
* 初始化服务
* 连接Nacos并开始配置监听
*/
async init() {
try {
// 尝试从Nacos获取配置
await this.refreshConfig();
// 设置定时刷新
this.startConfigRefresh();
// 注册Nacos配置变更监听
await this.registerConfigListener();
this.isConnected = true;
this.reconnectAttempts = 0; // 重置重连尝试次数
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
this.emit('ready', true);
}
catch (error) {
// 连接失败时尝试加载本地缓存
this.isConnected = false;
this.loadFromCache();
const err = error;
const errorResponse = {
code: 'NACOS_CONNECTION_ERROR',
message: '无法连接到Nacos配置中心',
details: err.message
};
console.error('Nacos连接失败:', err.message);
this.emit('error', errorResponse);
// 启动重连机制
this.scheduleReconnect();
// 即使连接失败,也设置定时尝试重连
this.startConfigRefresh();
}
}
/**
* 开始配置定时刷新
*/
startConfigRefresh() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
}
this.refreshTimer = setInterval(async () => {
try {
await this.refreshConfig();
// 如果之前断开连接,现在恢复了,触发重新连接事件
if (!this.isConnected) {
this.isConnected = true;
this.reconnectAttempts = 0; // 重置重连尝试次数
this.emit('connected');
}
}
catch (error) {
const err = error;
console.error('刷新配置失败:', err.message);
if (this.isConnected) {
this.isConnected = false;
this.emit('disconnected', {
code: 'NACOS_REFRESH_ERROR',
message: '刷新Nacos配置失败',
details: err.message
});
// 启动重连机制
this.scheduleReconnect();
}
// 连接失败时使用缓存
this.loadFromCache();
}
}, this.options.refreshInterval);
}
/**
* 注册Nacos配置变更监听
*/
async registerConfigListener() {
this.client.subscribe({
dataId: this.options.configId,
group: this.options.group
}, (content) => {
try {
const newConfig = JSON5.parse(content);
this.configData = newConfig;
this.saveToCache(newConfig);
this.emit('configChanged', newConfig);
}
catch (error) {
const err = error;
this.emit('error', {
code: 'CONFIG_PARSE_ERROR',
message: '解析配置数据失败',
details: err.message
});
}
});
}
/**
* 刷新配置
*/
async refreshConfig() {
try {
const content = await this.client.getConfig(this.options.configId, this.options.group);
if (!content) {
throw new Error('从Nacos获取的配置为空');
}
try {
const config = JSON5.parse(content);
this.configData = config;
this.saveToCache(config);
if (!this.isConnected) {
this.isConnected = true;
this.emit('connected');
}
return;
}
catch (error) {
const err = error;
throw new Error(`解析配置失败: ${content} ${err.message}`);
}
}
catch (error) {
// 重新抛出异常,让上层处理
throw error;
}
}
/**
* 将配置保存到本地缓存
*/
saveToCache(config) {
try {
fs.writeFileSync(this.cacheFilePath, JSON.stringify(config, null, 2));
}
catch (error) {
const err = error;
console.error('保存缓存失败:', err.message);
}
}
/**
* 从本地缓存加载配置
*/
loadFromCache() {
try {
if (fs.existsSync(this.cacheFilePath)) {
const content = fs.readFileSync(this.cacheFilePath, 'utf-8');
// 尝试解析缓存内容
const parsed = JSON5.parse(content);
// 处理不同的缓存格式
if (Array.isArray(parsed)) {
// 如果缓存是数组格式,确保每个提示词对象都有 title 字段
const prompts = parsed.map((p) => {
if (!p.title) {
p.title = p.description || p.id || '未命名提示词';
}
if (!p.tags) {
p.tags = [];
}
return p;
});
this.configData = prompts;
}
else if (parsed && typeof parsed === 'object') {
if (Array.isArray(parsed.prompts)) {
// 如果缓存是旧的 PromptConfig 对象格式
// 同样确保每个提示词对象都有 title 字段
const prompts = parsed.prompts.map((p) => {
if (!p.title) {
p.title = p.description || p.id || '未命名提示词';
}
if (!p.tags) {
p.tags = [];
}
return p;
});
this.configData = prompts;
}
else {
// 如果缓存是其他对象格式,尝试转换
const prompt = parsed;
if (!prompt.title) {
prompt.title = prompt.description || prompt.id || '未命名提示词';
}
if (!prompt.tags) {
prompt.tags = [];
}
this.configData = [prompt];
}
}
else {
throw new Error('无法识别的缓存格式');
}
// 确保 configData 存在
if (!this.configData) {
this.configData = [];
}
this.emit('usingCache', this.configData);
}
else {
// 如果缓存文件不存在,初始化为空数组
this.configData = [];
}
}
catch (error) {
const err = error;
console.error('加载缓存失败:', err.message);
// 确保即使在错误情况下也有有效的配置数据
this.configData = [];
this.emit('error', {
code: 'CACHE_LOAD_ERROR',
message: '加载本地缓存失败',
details: err.message
});
}
}
/**
* 获取所有提示词
*/
getAllPrompts() {
if (!this.configData) {
return [];
}
// 确保所有提示词都有必要的字段
return this.configData.map((p) => {
if (!p.title) {
p.title = p.description || p.id || '未命名提示词';
}
if (!p.tags) {
p.tags = [];
}
return p;
});
}
/**
* 根据标签获取提示词
*/
getPromptsByTags(tags) {
const prompts = this.getAllPrompts();
if (tags.length === 0) {
return prompts;
}
return prompts.filter(prompt => tags.some(tag => prompt.tags.includes(tag)));
}
/**
* 根据ID获取提示词
*/
getPromptById(id) {
const prompts = this.getAllPrompts();
return prompts.find(prompt => prompt.id === id);
}
/**
* 搜索提示词
* @param keyword 搜索关键词
*/
searchPrompts(keyword) {
if (!keyword) {
return this.getAllPrompts();
}
const lowerKeyword = keyword.toLowerCase();
return this.getAllPrompts().filter(prompt => prompt.title.toLowerCase().includes(lowerKeyword) ||
prompt.description.toLowerCase().includes(lowerKeyword) ||
prompt.content.toLowerCase().includes(lowerKeyword) ||
prompt.tags.some(tag => tag.toLowerCase().includes(lowerKeyword)));
}
/**
* 记录提示词使用情况
* @param promptId 提示词ID
* @returns 更新后的提示词对象,如果未找到则返回undefined
*/
trackPromptUsage(promptId) {
if (!this.configData) {
return undefined;
}
const promptIndex = this.configData.findIndex(p => p.id === promptId);
if (promptIndex === -1) {
return undefined;
}
// 更新使用统计
const prompt = this.configData[promptIndex];
prompt.usageCount = (prompt.usageCount || 0) + 1;
prompt.lastUsed = new Date().toISOString();
// 更新配置数据
this.configData[promptIndex] = prompt;
// 保存到缓存
this.saveToCache(this.configData);
return prompt;
}
/**
* 安排重新连接
*/
scheduleReconnect() {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
}
// 如果超过最大重连次数,停止重连
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error(`已达到最大重连尝试次数(${this.maxReconnectAttempts}),停止重连`);
return;
}
// 使用指数退避策略计算下一次重连时间
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
this.reconnectAttempts++;
console.log(`计划在 ${delay}ms 后进行第 ${this.reconnectAttempts} 次重连尝试`);
this.reconnectTimer = setTimeout(async () => {
try {
console.log(`尝试第 ${this.reconnectAttempts} 次重新连接到 Nacos...`);
await this.refreshConfig();
this.isConnected = true;
this.reconnectAttempts = 0; // 重置重连计数
console.log('重新连接到 Nacos 成功');
this.emit('connected');
}
catch (error) {
const err = error;
console.error(`第 ${this.reconnectAttempts} 次重连失败:`, err.message);
// 安排下一次重连
this.scheduleReconnect();
}
}, delay);
}
/**
* 关闭服务
*/
async close() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
}
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
try {
this.client.unSubscribe({
dataId: this.options.configId,
group: this.options.group
}, null);
}
catch (error) {
console.error('取消订阅失败:', error);
}
}
/**
* 获取连接状态
*/
getConnectionStatus() {
return this.isConnected;
}
}