@becrafter/prompt-manager 0.0.15 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -80
- package/app/desktop/main.js +321 -31
- package/app/desktop/package-lock.json +784 -79
- package/app/desktop/package.json +11 -3
- package/package.json +2 -1
- package/packages/admin-ui/admin.html +1 -1
- package/packages/server/api/admin.routes.js +450 -0
- package/packages/server/api/open.routes.js +89 -0
- package/packages/server/app.js +163 -0
- package/packages/server/{mcp.js → mcp/mcp.handler.js} +11 -79
- package/packages/server/mcp/mcp.server.js +82 -0
- package/packages/server/middlewares/auth.middleware.js +34 -0
- package/packages/server/server.js +42 -908
- package/packages/server/{manager.js → services/manager.js} +13 -5
- package/packages/server/{config.js → utils/config.js} +27 -27
- package/packages/server/utils/util.js +356 -0
- package/packages/server/mcpManager.js +0 -205
- /package/packages/server/{logger.js → utils/logger.js} +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { util } from './utils/util.js';
|
|
6
|
+
import { config } from './utils/config.js';
|
|
7
|
+
import { logger } from './utils/logger.js';
|
|
8
|
+
import { adminRouter } from './api/admin.routes.js';
|
|
9
|
+
import { openRouter } from './api/open.routes.js';
|
|
10
|
+
import { getMcpServer } from './mcp/mcp.server.js';
|
|
11
|
+
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
12
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
13
|
+
import { InMemoryEventStore } from '@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js';
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
const app = express();
|
|
17
|
+
const adminUiRoot = util.getAdminUiRoot();
|
|
18
|
+
|
|
19
|
+
// 全局中间件
|
|
20
|
+
app.use(cors());
|
|
21
|
+
app.use(express.json({ limit: '8mb' }));
|
|
22
|
+
app.use(express.urlencoded({ extended: true }));
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
// 为管理员界面提供静态文件服务 - 根路径
|
|
26
|
+
app.use(config.adminPath, express.static(adminUiRoot));
|
|
27
|
+
|
|
28
|
+
// 为管理员界面提供根路径访问(当用户访问 /admin 时显示 admin.html)
|
|
29
|
+
app.get(config.adminPath, (req, res) => {
|
|
30
|
+
res.sendFile(path.join(adminUiRoot, 'admin.html'));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// 为管理员界面提供根路径访问(当用户访问 /admin/ 时显示 admin.html)
|
|
34
|
+
app.get(config.adminPath + '/', (req, res) => {
|
|
35
|
+
res.sendFile(path.join(adminUiRoot, 'admin.html'));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
// 注册后台API
|
|
40
|
+
app.use('/adminapi', adminRouter);
|
|
41
|
+
|
|
42
|
+
// 注册开放API
|
|
43
|
+
app.use('/openapi', openRouter);
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
const transports = {};
|
|
47
|
+
const mcpServers = {}; // 存储每个会话的MCP服务器实例
|
|
48
|
+
|
|
49
|
+
// 挂载MCP流式服务(独立路径前缀,避免冲突)
|
|
50
|
+
app.all('/mcp', (req, res) => {
|
|
51
|
+
try {
|
|
52
|
+
let transport;
|
|
53
|
+
const sessionId = req.headers['mcp-session-id'] || '';
|
|
54
|
+
|
|
55
|
+
if (sessionId && transports[sessionId]) {
|
|
56
|
+
const existingTransport = transports[sessionId];
|
|
57
|
+
// Check if the transport is of the correct type
|
|
58
|
+
if (existingTransport instanceof StreamableHTTPServerTransport) {
|
|
59
|
+
// Reuse existing transport
|
|
60
|
+
transport = existingTransport;
|
|
61
|
+
} else {
|
|
62
|
+
// Transport exists but is not a StreamableHTTPServerTransport (could be SSEServerTransport)
|
|
63
|
+
res.status(400).json({
|
|
64
|
+
jsonrpc: '2.0',
|
|
65
|
+
error: {
|
|
66
|
+
code: -32000,
|
|
67
|
+
message: 'Bad Request: Session exists but uses a different transport protocol'
|
|
68
|
+
},
|
|
69
|
+
id: null
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
} else if (!sessionId && req.method === 'POST' && isInitializeRequest(req.body)) {
|
|
74
|
+
const eventStore = new InMemoryEventStore();
|
|
75
|
+
transport = new StreamableHTTPServerTransport({
|
|
76
|
+
sessionIdGenerator: () => randomUUID(),
|
|
77
|
+
eventStore, // Enable resumability
|
|
78
|
+
onsessioninitialized: sessionId => {
|
|
79
|
+
// Store the transport by session ID when session is initialized
|
|
80
|
+
console.log(`StreamableHTTP session initialized with ID: ${sessionId}`);
|
|
81
|
+
transports[sessionId] = transport;
|
|
82
|
+
|
|
83
|
+
// 为新会话创建MCP服务器实例
|
|
84
|
+
mcpServers[sessionId] = getMcpServer();
|
|
85
|
+
mcpServers[sessionId].connect(transport);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Set up onclose handler to clean up transport when closed
|
|
90
|
+
transport.onclose = () => {
|
|
91
|
+
const sid = transport.sessionId;
|
|
92
|
+
if (sid && transports[sid]) {
|
|
93
|
+
console.log(`Transport closed for session ${sid}, removing from transports map`);
|
|
94
|
+
delete transports[sid];
|
|
95
|
+
// 清理MCP服务器实例
|
|
96
|
+
if (mcpServers[sid]) {
|
|
97
|
+
delete mcpServers[sid];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// 添加错误处理
|
|
103
|
+
transport.onerror = (error) => {
|
|
104
|
+
console.error('MCP Transport error:', error);
|
|
105
|
+
};
|
|
106
|
+
} else {
|
|
107
|
+
// Invalid request - no session ID or not initialization request
|
|
108
|
+
res.status(400).json({
|
|
109
|
+
jsonrpc: '2.0',
|
|
110
|
+
error: {
|
|
111
|
+
code: -32000,
|
|
112
|
+
message: 'Bad Request: No valid session ID provided'
|
|
113
|
+
},
|
|
114
|
+
id: null
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Handle the request with the transport
|
|
120
|
+
transport.handleRequest(req, res, req.body);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
logger.error('Error handling MCP request: ' + error.message);
|
|
123
|
+
return res.status(500).json({
|
|
124
|
+
jsonrpc: '2.0',
|
|
125
|
+
error: {
|
|
126
|
+
code: -32603,
|
|
127
|
+
message: 'Internal server error'
|
|
128
|
+
},
|
|
129
|
+
id: null
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// 错误处理中间件
|
|
135
|
+
app.use((err, req, res, next) => {
|
|
136
|
+
console.error(`[服务器错误]: ${err.message}`);
|
|
137
|
+
res.status(500).send('Internal Server Error')
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// 404处理
|
|
141
|
+
app.use((req, res) => {
|
|
142
|
+
res.status(404).send('Not Found')
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// 导出清理函数
|
|
146
|
+
export function cleanupMcpSessions() {
|
|
147
|
+
console.log('Cleaning up MCP sessions');
|
|
148
|
+
// 清理所有活动的传输和服务器实例
|
|
149
|
+
for (const sessionId in transports) {
|
|
150
|
+
try {
|
|
151
|
+
if (transports[sessionId] && typeof transports[sessionId].close === 'function') {
|
|
152
|
+
transports[sessionId].close();
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error(`Error closing transport for session ${sessionId}:`, error);
|
|
156
|
+
}
|
|
157
|
+
delete transports[sessionId];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 注意:mcpServers 对象在 app.js 中不可访问,需要在 server.js 中处理
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export default app;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// 导入自定义模块
|
|
2
|
-
import { config } from '
|
|
3
|
-
import { logger } from '
|
|
2
|
+
import { config } from '../utils/config.js';
|
|
3
|
+
import { logger } from '../utils/logger.js';
|
|
4
|
+
import { util } from '../utils/util.js';
|
|
4
5
|
|
|
5
6
|
// 处理 get_prompt 工具调用
|
|
6
|
-
export async function handleGetPrompt(args
|
|
7
|
+
export async function handleGetPrompt(args) {
|
|
7
8
|
// 注意:这里为了兼容性,我们同时支持prompt_id和name参数
|
|
8
9
|
const promptId = args.prompt_id || args.name;
|
|
9
10
|
|
|
@@ -11,6 +12,7 @@ export async function handleGetPrompt(args, promptManager) {
|
|
|
11
12
|
throw new Error("缺少必需参数: prompt_id 或 name");
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
const promptManager = await util.getPromptManager();
|
|
14
16
|
const prompt = promptManager.getPrompt(promptId);
|
|
15
17
|
if (!prompt) {
|
|
16
18
|
throw new Error(`未找到ID为 "${promptId}" 的prompt`);
|
|
@@ -41,12 +43,13 @@ export async function handleGetPrompt(args, promptManager) {
|
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
// 处理 search_prompts 工具调用
|
|
44
|
-
export async function handleSearchPrompts(args
|
|
46
|
+
export async function handleSearchPrompts(args) {
|
|
45
47
|
// 注意:这里为了兼容性,我们同时支持title和name参数
|
|
46
48
|
const searchTerm = args.title || args.name;
|
|
47
49
|
|
|
48
50
|
const logLevel = config.getLogLevel();
|
|
49
|
-
const
|
|
51
|
+
const promptManager = await util.getPromptManager();
|
|
52
|
+
let allPrompts = (await promptManager.loadPrompts()).prompts || [];
|
|
50
53
|
|
|
51
54
|
// 如果搜索词为空,则返回所有提示词
|
|
52
55
|
if (!searchTerm) {
|
|
@@ -67,7 +70,7 @@ export async function handleSearchPrompts(args, promptManager) {
|
|
|
67
70
|
prompt.hasArguments = prompt.arguments && prompt.arguments.length > 0;
|
|
68
71
|
return {
|
|
69
72
|
prompt: prompt,
|
|
70
|
-
score: calculateSimilarityScore(searchTerm, prompt),
|
|
73
|
+
score: util.calculateSimilarityScore(searchTerm, prompt),
|
|
71
74
|
};
|
|
72
75
|
})
|
|
73
76
|
.filter(result => result.score > 0) // 只返回有匹配的结果
|
|
@@ -98,9 +101,10 @@ export async function handleSearchPrompts(args, promptManager) {
|
|
|
98
101
|
/**
|
|
99
102
|
* 处理 reload_prompts 工具调用
|
|
100
103
|
*/
|
|
101
|
-
export async function handleReloadPrompts(
|
|
104
|
+
export async function handleReloadPrompts(args) {
|
|
102
105
|
logger.info('重新加载prompts...');
|
|
103
106
|
|
|
107
|
+
const promptManager = await util.getPromptManager();
|
|
104
108
|
const result = await promptManager.reloadPrompts();
|
|
105
109
|
|
|
106
110
|
return {
|
|
@@ -125,8 +129,6 @@ export async function handleReloadPrompts(promptManager) {
|
|
|
125
129
|
function formatResults(results = []) {
|
|
126
130
|
if (!Array.isArray(results)) return [];
|
|
127
131
|
|
|
128
|
-
// console.log(results);
|
|
129
|
-
|
|
130
132
|
return results.map(result => {
|
|
131
133
|
const prompt = result.prompt ? result.prompt : result;
|
|
132
134
|
const baseItem = {
|
|
@@ -161,74 +163,4 @@ function convertToText(result) {
|
|
|
161
163
|
}
|
|
162
164
|
]
|
|
163
165
|
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* 计算搜索关键词与prompt的相似度得分
|
|
168
|
-
* @param {string} searchTerm - 搜索关键词
|
|
169
|
-
* @param {Object} prompt - prompt对象
|
|
170
|
-
* @returns {number} 相似度得分 (0-100)
|
|
171
|
-
*/
|
|
172
|
-
function calculateSimilarityScore(searchTerm, prompt) {
|
|
173
|
-
let totalScore = 0;
|
|
174
|
-
const searchLower = searchTerm ? searchTerm.toLowerCase() : '';
|
|
175
|
-
|
|
176
|
-
// 搜索字段权重配置(专注于内容搜索,不包含ID检索)
|
|
177
|
-
const fieldWeights = {
|
|
178
|
-
name: 60, // 名称权重高,是主要匹配字段
|
|
179
|
-
description: 40 // 描述权重适中,是辅助匹配字段
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
// 计算name匹配得分
|
|
183
|
-
if (prompt && prompt.name && typeof prompt.name === 'string') {
|
|
184
|
-
const nameScore = getStringMatchScore(searchLower, prompt.name.toLowerCase());
|
|
185
|
-
totalScore += nameScore * fieldWeights.name;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// 计算description匹配得分
|
|
189
|
-
if (prompt.description) {
|
|
190
|
-
const descScore = getStringMatchScore(searchLower, prompt.description.toLowerCase());
|
|
191
|
-
totalScore += descScore * fieldWeights.description;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// 标准化得分到0-100范围
|
|
195
|
-
const maxPossibleScore = Object.values(fieldWeights).reduce((sum, weight) => sum + weight, 0);
|
|
196
|
-
return Math.round((totalScore / maxPossibleScore) * 100);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* 计算两个字符串的匹配得分
|
|
201
|
-
* @param {string} search - 搜索词 (已转小写)
|
|
202
|
-
* @param {string} target - 目标字符串 (已转小写)
|
|
203
|
-
* @returns {number} 匹配得分 (0-1)
|
|
204
|
-
*/
|
|
205
|
-
function getStringMatchScore(search, target) {
|
|
206
|
-
if (!search || !target) return 0;
|
|
207
|
-
|
|
208
|
-
// 完全匹配得分最高
|
|
209
|
-
if (target === search) return 1.0;
|
|
210
|
-
|
|
211
|
-
// 完全包含得分较高
|
|
212
|
-
if (target.includes(search)) return 0.8;
|
|
213
|
-
|
|
214
|
-
// 部分词匹配
|
|
215
|
-
const searchWords = search.split(/\s+/).filter(word => word.length > 0);
|
|
216
|
-
const targetWords = target.split(/\s+/).filter(word => word.length > 0);
|
|
217
|
-
|
|
218
|
-
let matchedWords = 0;
|
|
219
|
-
for (const searchWord of searchWords) {
|
|
220
|
-
for (const targetWord of targetWords) {
|
|
221
|
-
if (targetWord.includes(searchWord) || searchWord.includes(targetWord)) {
|
|
222
|
-
matchedWords++;
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (searchWords.length > 0) {
|
|
229
|
-
const wordMatchRatio = matchedWords / searchWords.length;
|
|
230
|
-
return wordMatchRatio * 0.6; // 部分词匹配得分
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return 0;
|
|
234
166
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { config } from '../utils/config.js';
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import {
|
|
4
|
+
handleGetPrompt,
|
|
5
|
+
handleSearchPrompts,
|
|
6
|
+
handleReloadPrompts
|
|
7
|
+
} from './mcp.handler.js';
|
|
8
|
+
|
|
9
|
+
class Server {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.server = new McpServer(
|
|
12
|
+
{
|
|
13
|
+
name: 'Prompt Management Server',
|
|
14
|
+
version: config.getServerVersion()
|
|
15
|
+
},
|
|
16
|
+
{ capabilities: { logging: {} } }
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
registerTools(tools) {
|
|
21
|
+
for (const tool of tools) {
|
|
22
|
+
this.server.tool(tool.name, tool.description, tool.inputSchema, tool.handler);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getServer() {
|
|
27
|
+
return this.server;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const getMcpServer = () => {
|
|
32
|
+
const mcpServer = new Server();
|
|
33
|
+
mcpServer.registerTools([
|
|
34
|
+
{
|
|
35
|
+
name: 'get_prompt',
|
|
36
|
+
description: 'Retrieve the complete content of a specific prompt by its ID, including all messages, arguments, and metadata.',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
prompt_id: {
|
|
41
|
+
type: 'string',
|
|
42
|
+
description: 'the unique identifier of the prompt to retrieve'
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
required: ['prompt_id']
|
|
46
|
+
},
|
|
47
|
+
handler: async (args) => {
|
|
48
|
+
return handleGetPrompt(args);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'search_prompts',
|
|
53
|
+
description: 'For keyword search matching, retrieve or return all prompts. When a corresponding prompt word is matched, utilize the prompt ID to invoke the get_prompt tool to query the complete content of the prompt word.',
|
|
54
|
+
inputSchema: {
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
name: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
description: 'Optional name filter for prompts'
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
required: []
|
|
63
|
+
},
|
|
64
|
+
handler: async (args) => {
|
|
65
|
+
return handleSearchPrompts(args);
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
// {
|
|
69
|
+
// name: 'reload_prompts',
|
|
70
|
+
// description: 'Force a reload of all preset prompts to overwrite the cache.',
|
|
71
|
+
// inputSchema: {
|
|
72
|
+
// type: 'object',
|
|
73
|
+
// properties: {},
|
|
74
|
+
// required: []
|
|
75
|
+
// },
|
|
76
|
+
// handler: async (args) => {
|
|
77
|
+
// return handleReloadPrompts(args);
|
|
78
|
+
// }
|
|
79
|
+
// }
|
|
80
|
+
]);
|
|
81
|
+
return mcpServer.getServer();
|
|
82
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { config } from '../utils/config.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 管理员API中间件
|
|
5
|
+
* @param {*} req
|
|
6
|
+
* @param {*} res
|
|
7
|
+
* @param {*} next
|
|
8
|
+
* @returns
|
|
9
|
+
*/
|
|
10
|
+
export function adminAuthMiddleware(req, res, next) {
|
|
11
|
+
// 检查是否启用了管理员功能
|
|
12
|
+
if (!config.adminEnable) {
|
|
13
|
+
return res.status(404).json({ error: 'Admin功能未启用' });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 检查Authorization请求头是否存在且格式正确
|
|
17
|
+
const authHeader = req.headers.authorization;
|
|
18
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
19
|
+
return res.status(401).json({ error: '未提供认证令牌' });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 提取Bearer token
|
|
23
|
+
const token = authHeader.substring(7);
|
|
24
|
+
|
|
25
|
+
// 验证令牌是否在配置的管理员列表中
|
|
26
|
+
const admin = config.admins.find(a => a.token === token);
|
|
27
|
+
if (!admin) {
|
|
28
|
+
return res.status(401).json({ error: '无效的认证令牌' });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 将管理员信息附加到请求对象,供后续中间件使用
|
|
32
|
+
req.admin = admin;
|
|
33
|
+
next();
|
|
34
|
+
}
|