@becrafter/prompt-manager 0.1.2 → 0.1.8
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 +304 -121
- package/app/cli/commands/start.js +28 -4
- package/app/cli/support/argv.js +6 -0
- package/env.example +32 -0
- package/package.json +36 -6
- package/packages/server/api/admin.routes.js +409 -1
- package/packages/server/api/open.routes.js +7 -2
- package/packages/server/api/tool.routes.js +479 -0
- package/packages/server/app.js +97 -25
- package/packages/server/configs/models/built-in/bigmodel.yaml +6 -0
- package/packages/server/configs/models/providers.yaml +50 -0
- package/packages/server/configs/templates/built-in/general-iteration.yaml +60 -0
- package/packages/server/configs/templates/built-in/general-optimize.yaml +63 -0
- package/packages/server/configs/templates/built-in/output-format-optimize.yaml +95 -0
- package/packages/server/mcp/heartbeat-patch.js +73 -0
- package/packages/server/mcp/mcp.server.js +63 -314
- package/packages/server/mcp/prompt.handler.js +26 -0
- package/packages/server/mcp/thinking-toolkit.handler.js +380 -0
- package/packages/server/package.json +35 -3
- package/packages/server/server.js +114 -12
- package/packages/server/services/TerminalService.js +498 -0
- package/packages/server/services/WebSocketService.js +484 -0
- package/packages/server/services/manager.js +38 -7
- package/packages/server/services/model.service.js +473 -0
- package/packages/server/services/optimization.service.js +457 -0
- package/packages/server/services/template.service.js +333 -0
- package/packages/server/toolm/tool-description-generator-optimized.service.js +5 -2
- package/packages/server/toolm/tool-sync.service.js +47 -3
- package/packages/server/utils/config.js +8 -1
- package/packages/server/utils/port-checker.js +63 -0
- package/packages/server/utils/util.js +27 -0
- package/IFLOW.md +0 -175
- package/app/desktop/assets/app.1.png +0 -0
- package/app/desktop/assets/app.png +0 -0
- package/app/desktop/assets/icons/icon.icns +0 -0
- package/app/desktop/assets/icons/icon.ico +0 -0
- package/app/desktop/assets/icons/icon.png +0 -0
- package/app/desktop/assets/icons/tray.png +0 -0
- package/app/desktop/assets/templates/about.html +0 -147
- package/app/desktop/assets/tray.1.png +0 -0
- package/app/desktop/assets/tray.png +0 -0
- package/app/desktop/main.js +0 -241
- package/app/desktop/package-lock.json +0 -5026
- package/app/desktop/package.json +0 -100
- package/app/desktop/preload.js +0 -7
- package/app/desktop/src/core/error-handler.js +0 -108
- package/app/desktop/src/core/event-emitter.js +0 -84
- package/app/desktop/src/core/logger.js +0 -108
- package/app/desktop/src/core/state-manager.js +0 -125
- package/app/desktop/src/services/module-loader.js +0 -214
- package/app/desktop/src/services/runtime-manager.js +0 -301
- package/app/desktop/src/services/service-manager.js +0 -169
- package/app/desktop/src/services/update-manager.js +0 -267
- package/app/desktop/src/ui/about-dialog-manager.js +0 -208
- package/app/desktop/src/ui/admin-window-manager.js +0 -757
- package/app/desktop/src/ui/splash-manager.js +0 -253
- package/app/desktop/src/ui/tray-manager.js +0 -186
- package/app/desktop/src/utils/icon-manager.js +0 -133
- package/app/desktop/src/utils/path-utils.js +0 -58
- package/app/desktop/src/utils/resource-paths.js +0 -49
- package/app/desktop/src/utils/resource-sync.js +0 -260
- package/app/desktop/src/utils/runtime-sync.js +0 -241
- package/app/desktop/src/utils/template-renderer.js +0 -284
- package/app/desktop/src/utils/version-utils.js +0 -59
- package/examples/prompts/developer/code-review.yaml +0 -32
- package/examples/prompts/developer/code_refactoring.yaml +0 -31
- package/examples/prompts/developer/doc-generator.yaml +0 -36
- package/examples/prompts/developer/error-code-fixer.yaml +0 -35
- package/examples/prompts/engineer/engineer-professional.yaml +0 -92
- package/examples/prompts/engineer/laowang-engineer.yaml +0 -132
- package/examples/prompts/engineer/nekomata-engineer.yaml +0 -123
- package/examples/prompts/engineer/ojousama-engineer.yaml +0 -124
- package/examples/prompts/generator/gen_3d_edu_webpage_html.yaml +0 -117
- package/examples/prompts/generator/gen_3d_webpage_html.yaml +0 -75
- package/examples/prompts/generator/gen_bento_grid_html.yaml +0 -112
- package/examples/prompts/generator/gen_html_web_page.yaml +0 -88
- package/examples/prompts/generator/gen_knowledge_card_html.yaml +0 -83
- package/examples/prompts/generator/gen_magazine_card_html.yaml +0 -82
- package/examples/prompts/generator/gen_mimeng_headline_title.yaml +0 -71
- package/examples/prompts/generator/gen_podcast_script.yaml +0 -69
- package/examples/prompts/generator/gen_prd_prototype_html.yaml +0 -175
- package/examples/prompts/generator/gen_summarize.yaml +0 -157
- package/examples/prompts/generator/gen_title.yaml +0 -119
- package/examples/prompts/generator/others/api_documentation.yaml +0 -32
- package/examples/prompts/generator/others/build_mcp_server.yaml +0 -26
- package/examples/prompts/generator/others/project_architecture.yaml +0 -31
- package/examples/prompts/generator/others/test_case_generator.yaml +0 -30
- package/examples/prompts/generator/others/writing_assistant.yaml +0 -72
- package/examples/prompts/recommend/human_3-0_growth_diagnostic_coach_prompt.yaml +0 -105
- package/examples/prompts/workflow/sixstep-workflow.yaml +0 -192
- package/packages/admin-ui/.babelrc +0 -3
- package/packages/admin-ui/admin.html +0 -412
- package/packages/admin-ui/css/codemirror-theme_xq-light.css +0 -43
- package/packages/admin-ui/css/codemirror.css +0 -344
- package/packages/admin-ui/css/main.css +0 -2592
- package/packages/admin-ui/css/recommended-prompts.css +0 -610
- package/packages/admin-ui/package-lock.json +0 -6973
- package/packages/admin-ui/package.json +0 -36
- package/packages/admin-ui/src/codemirror.js +0 -53
- package/packages/admin-ui/src/index.js +0 -3188
- package/packages/admin-ui/webpack.config.js +0 -76
- package/packages/server/toolm/test-tools.js +0 -264
- package/scripts/build-icons.js +0 -135
- package/scripts/build.sh +0 -57
- package/scripts/postinstall.js +0 -34
- package/scripts/surge/CNAME +0 -1
- package/scripts/surge/README.md +0 -47
- package/scripts/surge/package-lock.json +0 -34
- package/scripts/surge/package.json +0 -20
- package/scripts/surge/sync-to-surge.js +0 -151
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具管理接口
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import express from 'express';
|
|
6
|
+
import multer from 'multer';
|
|
7
|
+
import AdmZip from 'adm-zip';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import fs from 'fs-extra';
|
|
10
|
+
import fse from 'fs-extra';
|
|
11
|
+
import { logger } from '../utils/logger.js';
|
|
12
|
+
import { toolLoaderService } from '../toolm/tool-loader.service.js';
|
|
13
|
+
import { pathExists } from '../toolm/tool-utils.js';
|
|
14
|
+
import os from 'os';
|
|
15
|
+
|
|
16
|
+
// 配置 multer 用于处理文件上传
|
|
17
|
+
const upload = multer({
|
|
18
|
+
storage: multer.diskStorage({
|
|
19
|
+
destination: (req, file, cb) => {
|
|
20
|
+
const uploadDir = path.join(os.homedir(), '.prompt-manager', 'temp');
|
|
21
|
+
fs.ensureDirSync(uploadDir);
|
|
22
|
+
cb(null, uploadDir);
|
|
23
|
+
},
|
|
24
|
+
filename: (req, file, cb) => {
|
|
25
|
+
// 生成唯一文件名,避免并发上传冲突
|
|
26
|
+
const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1E9)}-${file.originalname}`;
|
|
27
|
+
cb(null, uniqueName);
|
|
28
|
+
}
|
|
29
|
+
}),
|
|
30
|
+
// 文件类型验证
|
|
31
|
+
fileFilter: (req, file, cb) => {
|
|
32
|
+
if (file.originalname.toLowerCase().endsWith('.zip')) {
|
|
33
|
+
cb(null, true);
|
|
34
|
+
} else {
|
|
35
|
+
cb(new Error('只允许上传ZIP格式的文件'), false);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
// 文件大小限制(50MB)
|
|
39
|
+
limits: {
|
|
40
|
+
fileSize: 50 * 1024 * 1024
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const router = express.Router();
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 获取工具列表
|
|
48
|
+
*/
|
|
49
|
+
router.get('/list', async (req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
// 确保工具加载器已初始化
|
|
52
|
+
if (!toolLoaderService.initialized) {
|
|
53
|
+
await toolLoaderService.initialize();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const {
|
|
57
|
+
search,
|
|
58
|
+
category,
|
|
59
|
+
tags,
|
|
60
|
+
author,
|
|
61
|
+
page = 1,
|
|
62
|
+
limit = 20
|
|
63
|
+
} = req.query;
|
|
64
|
+
|
|
65
|
+
// 获取所有工具
|
|
66
|
+
let tools = toolLoaderService.getAllTools();
|
|
67
|
+
|
|
68
|
+
// 过滤处理
|
|
69
|
+
if (search) {
|
|
70
|
+
const searchLower = search.toLowerCase();
|
|
71
|
+
tools = tools.filter(tool => {
|
|
72
|
+
const nameMatch = tool.metadata.name?.toLowerCase().includes(searchLower);
|
|
73
|
+
const descMatch = tool.metadata.description?.toLowerCase().includes(searchLower);
|
|
74
|
+
return nameMatch || descMatch;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (category) {
|
|
79
|
+
tools = tools.filter(tool =>
|
|
80
|
+
tool.metadata.category === category
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (tags) {
|
|
85
|
+
const tagArray = Array.isArray(tags) ? tags : tags.split(',').map(t => t.trim());
|
|
86
|
+
tools = tools.filter(tool => {
|
|
87
|
+
const toolTags = tool.metadata.tags || [];
|
|
88
|
+
return tagArray.some(tag => toolTags.includes(tag));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (author) {
|
|
93
|
+
const authorLower = author.toLowerCase();
|
|
94
|
+
tools = tools.filter(tool =>
|
|
95
|
+
tool.metadata.author?.toLowerCase() === authorLower
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 排序
|
|
100
|
+
tools.sort((a, b) => a.name.localeCompare(b.name));
|
|
101
|
+
|
|
102
|
+
// 分页处理
|
|
103
|
+
const total = tools.length;
|
|
104
|
+
const startIndex = (page - 1) * limit;
|
|
105
|
+
const endIndex = startIndex + limit;
|
|
106
|
+
const paginatedTools = tools.slice(startIndex, endIndex);
|
|
107
|
+
|
|
108
|
+
// 格式化返回数据
|
|
109
|
+
const formattedTools = paginatedTools.map(tool => {
|
|
110
|
+
const metadata = tool.metadata || {};
|
|
111
|
+
return {
|
|
112
|
+
id: metadata.id || tool.name,
|
|
113
|
+
name: metadata.name || tool.name,
|
|
114
|
+
description: metadata.description || '',
|
|
115
|
+
version: metadata.version || '1.0.0',
|
|
116
|
+
category: metadata.category || 'other',
|
|
117
|
+
author: metadata.author || 'Prompt Manager',
|
|
118
|
+
tags: metadata.tags || [],
|
|
119
|
+
scenarios: metadata.scenarios || [],
|
|
120
|
+
limitations: metadata.limitations || []
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const response = {
|
|
125
|
+
success: true,
|
|
126
|
+
total,
|
|
127
|
+
page: parseInt(page),
|
|
128
|
+
limit: parseInt(limit),
|
|
129
|
+
tools: formattedTools
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
res.json(response);
|
|
133
|
+
|
|
134
|
+
} catch (error) {
|
|
135
|
+
logger.error('获取工具列表失败:', error);
|
|
136
|
+
res.status(500).json({
|
|
137
|
+
success: false,
|
|
138
|
+
error: error.message
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 获取工具详情
|
|
145
|
+
*/
|
|
146
|
+
router.get('/detail/:toolName', async (req, res) => {
|
|
147
|
+
try {
|
|
148
|
+
// 确保工具加载器已初始化
|
|
149
|
+
if (!toolLoaderService.initialized) {
|
|
150
|
+
await toolLoaderService.initialize();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const { toolName } = req.params;
|
|
154
|
+
|
|
155
|
+
// 检查工具是否存在
|
|
156
|
+
if (!toolLoaderService.hasTool(toolName)) {
|
|
157
|
+
return res.status(404).json({
|
|
158
|
+
success: false,
|
|
159
|
+
error: `工具 '${toolName}' 不存在`
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const tool = toolLoaderService.getTool(toolName);
|
|
164
|
+
const metadata = tool.metadata || {};
|
|
165
|
+
|
|
166
|
+
// 格式化工具详情
|
|
167
|
+
const toolDetail = {
|
|
168
|
+
id: metadata.id || tool.name,
|
|
169
|
+
name: metadata.name || tool.name,
|
|
170
|
+
description: metadata.description || '',
|
|
171
|
+
version: metadata.version || '1.0.0',
|
|
172
|
+
category: metadata.category || 'other',
|
|
173
|
+
author: metadata.author || 'Prompt Manager',
|
|
174
|
+
tags: metadata.tags || [],
|
|
175
|
+
scenarios: metadata.scenarios || [],
|
|
176
|
+
limitations: metadata.limitations || [],
|
|
177
|
+
schema: tool.schema || {},
|
|
178
|
+
businessErrors: tool.businessErrors || []
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
res.json({
|
|
182
|
+
success: true,
|
|
183
|
+
tool: toolDetail
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
} catch (error) {
|
|
187
|
+
logger.error('获取工具详情失败:', error);
|
|
188
|
+
res.status(500).json({
|
|
189
|
+
success: false,
|
|
190
|
+
error: error.message
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* 读取工具的 README.md 文件
|
|
197
|
+
*/
|
|
198
|
+
router.get('/readme/:toolName', async (req, res) => {
|
|
199
|
+
try {
|
|
200
|
+
// 确保工具加载器已初始化
|
|
201
|
+
if (!toolLoaderService.initialized) {
|
|
202
|
+
await toolLoaderService.initialize();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const { toolName } = req.params;
|
|
206
|
+
|
|
207
|
+
// 检查工具是否存在
|
|
208
|
+
if (!toolLoaderService.hasTool(toolName)) {
|
|
209
|
+
return res.status(404).json({
|
|
210
|
+
success: false,
|
|
211
|
+
error: `工具 '${toolName}' 不存在`
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const tool = toolLoaderService.getTool(toolName);
|
|
216
|
+
|
|
217
|
+
// 查找 README.md 文件
|
|
218
|
+
const toolboxDir = path.join(os.homedir(), '.prompt-manager', 'toolbox', toolName);
|
|
219
|
+
const readmePath = path.join(toolboxDir, 'README.md');
|
|
220
|
+
|
|
221
|
+
if (!await pathExists(readmePath)) {
|
|
222
|
+
return res.status(404).json({
|
|
223
|
+
success: false,
|
|
224
|
+
error: `工具 '${toolName}' 的 README.md 文件不存在`
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const fs = await import('fs');
|
|
229
|
+
const readmeContent = await fs.promises.readFile(readmePath, 'utf-8');
|
|
230
|
+
|
|
231
|
+
res.json({
|
|
232
|
+
success: true,
|
|
233
|
+
toolName,
|
|
234
|
+
content: readmeContent
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
} catch (error) {
|
|
238
|
+
logger.error('读取工具 README 失败:', error);
|
|
239
|
+
res.status(500).json({
|
|
240
|
+
success: false,
|
|
241
|
+
error: error.message
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 上传工具包
|
|
248
|
+
*
|
|
249
|
+
* 上传内容必须是 .zip 文件
|
|
250
|
+
* 上传后需要做工具名的重复性检查,重复的不允许会给出提示,让用户自己判断是否需要覆盖
|
|
251
|
+
* 解压缩 .zip 文件,然后检查里面是否存在规范约定的两个文件,至少存在这两个
|
|
252
|
+
* 然后运行工具验证,看程序是否可以正常运行
|
|
253
|
+
*/
|
|
254
|
+
router.post('/upload', upload.single('file'), async (req, res) => {
|
|
255
|
+
let tempZipPath = null;
|
|
256
|
+
let extractedDir = null;
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
// 验证文件是否存在
|
|
260
|
+
if (!req.file) {
|
|
261
|
+
return res.status(400).json({
|
|
262
|
+
success: false,
|
|
263
|
+
error: '请上传ZIP文件'
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
tempZipPath = req.file.path;
|
|
268
|
+
|
|
269
|
+
// 验证文件类型
|
|
270
|
+
if (!req.file.originalname.toLowerCase().endsWith('.zip')) {
|
|
271
|
+
return res.status(400).json({
|
|
272
|
+
success: false,
|
|
273
|
+
error: '上传的文件必须是ZIP格式'
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 创建临时解压目录
|
|
278
|
+
const tempDir = path.join(os.homedir(), '.prompt-manager', 'temp');
|
|
279
|
+
fs.ensureDirSync(tempDir);
|
|
280
|
+
extractedDir = path.join(tempDir, `extracted_${Date.now()}_${Math.round(Math.random() * 1E9)}`);
|
|
281
|
+
fs.ensureDirSync(extractedDir);
|
|
282
|
+
|
|
283
|
+
// 解压ZIP文件
|
|
284
|
+
const zip = new AdmZip(tempZipPath);
|
|
285
|
+
zip.extractAllTo(extractedDir, true);
|
|
286
|
+
|
|
287
|
+
// 检查解压后的目录结构
|
|
288
|
+
const files = fs.readdirSync(extractedDir);
|
|
289
|
+
|
|
290
|
+
// 查找工具文件(以 .tool.js 结尾的文件)
|
|
291
|
+
const toolFiles = files.filter(file =>
|
|
292
|
+
file.endsWith('.tool.js') && fs.statSync(path.join(extractedDir, file)).isFile()
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
if (toolFiles.length === 0) {
|
|
296
|
+
return res.status(400).json({
|
|
297
|
+
success: false,
|
|
298
|
+
error: 'ZIP文件中未找到以 .tool.js 结尾的工具文件'
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 提取工具名(从第一个 .tool.js 文件名推断)
|
|
303
|
+
const toolFileName = toolFiles[0];
|
|
304
|
+
const toolName = toolFileName.replace('.tool.js', '');
|
|
305
|
+
|
|
306
|
+
// 检查是否存在 README.md
|
|
307
|
+
const hasReadme = files.some(file =>
|
|
308
|
+
file.toLowerCase() === 'readme.md' && fs.statSync(path.join(extractedDir, file)).isFile()
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
if (!hasReadme) {
|
|
312
|
+
return res.status(400).json({
|
|
313
|
+
success: false,
|
|
314
|
+
error: 'ZIP文件中必须包含 README.md 文件'
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 检查工具是否已存在
|
|
319
|
+
const toolboxDir = path.join(os.homedir(), '.prompt-manager', 'toolbox');
|
|
320
|
+
const targetToolDir = path.join(toolboxDir, toolName);
|
|
321
|
+
const toolExists = await pathExists(targetToolDir);
|
|
322
|
+
|
|
323
|
+
// 如果工具已存在且没有覆盖参数,则提示用户
|
|
324
|
+
if (toolExists && req.body.overwrite !== 'true') {
|
|
325
|
+
return res.status(409).json({
|
|
326
|
+
success: false,
|
|
327
|
+
error: `工具 "${toolName}" 已存在`,
|
|
328
|
+
toolName: toolName,
|
|
329
|
+
canOverwrite: true
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 如果用户确认覆盖,先删除原有工具目录
|
|
334
|
+
if (toolExists && req.body.overwrite === 'true') {
|
|
335
|
+
await fse.remove(targetToolDir);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// 将解压的文件复制到工具目录
|
|
339
|
+
await fse.ensureDir(targetToolDir);
|
|
340
|
+
await fse.copy(extractedDir, targetToolDir);
|
|
341
|
+
|
|
342
|
+
// 检查工具文件是否可导入(语法验证)
|
|
343
|
+
const toolFilePath = path.join(targetToolDir, toolFileName);
|
|
344
|
+
if (!await pathExists(toolFilePath)) {
|
|
345
|
+
return res.status(500).json({
|
|
346
|
+
success: false,
|
|
347
|
+
error: `工具文件 ${toolFileName} 不存在`
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 尝试动态导入工具以验证语法
|
|
352
|
+
let tool;
|
|
353
|
+
try {
|
|
354
|
+
const toolModule = await import(`file://${toolFilePath}`);
|
|
355
|
+
tool = toolModule.default || toolModule;
|
|
356
|
+
|
|
357
|
+
// 验证工具接口是否符合规范
|
|
358
|
+
if (typeof tool.execute !== 'function') {
|
|
359
|
+
return res.status(400).json({
|
|
360
|
+
success: false,
|
|
361
|
+
error: `工具文件缺少必需的 execute 方法`
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// 运行测试验证工具是否能正常工作
|
|
366
|
+
const { createToolContext } = await import('../toolm/tool-context.service.js');
|
|
367
|
+
const toolContext = await createToolContext(toolName, tool);
|
|
368
|
+
|
|
369
|
+
// 执行一个简单的测试,检查工具是否能被正确初始化
|
|
370
|
+
if (typeof tool.getMetadata === 'function') {
|
|
371
|
+
try {
|
|
372
|
+
const metadata = tool.getMetadata();
|
|
373
|
+
if (!metadata || typeof metadata !== 'object') {
|
|
374
|
+
logger.warn(`工具 ${toolName} 的 getMetadata 方法返回值无效`);
|
|
375
|
+
}
|
|
376
|
+
} catch (metaError) {
|
|
377
|
+
logger.warn(`工具 ${toolName} 的 getMetadata 方法调用失败:`, metaError.message);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 简单测试 execute 方法是否存在
|
|
382
|
+
if (tool.execute && typeof tool.execute === 'function') {
|
|
383
|
+
// 为了安全起见,不实际执行工具,只验证其签名
|
|
384
|
+
logger.info(`工具 ${toolName} 的 execute 方法存在,签名验证通过`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
} catch (importError) {
|
|
388
|
+
// 清理失败的工具目录
|
|
389
|
+
await fse.remove(targetToolDir);
|
|
390
|
+
return res.status(400).json({
|
|
391
|
+
success: false,
|
|
392
|
+
error: `工具文件语法错误: ${importError.message}`
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 验证 package.json(如果存在)
|
|
397
|
+
const packageJsonPath = path.join(targetToolDir, 'package.json');
|
|
398
|
+
let dependencies = {};
|
|
399
|
+
if (await pathExists(packageJsonPath)) {
|
|
400
|
+
try {
|
|
401
|
+
const packageJson = await fse.readJson(packageJsonPath);
|
|
402
|
+
dependencies = packageJson.dependencies || {};
|
|
403
|
+
} catch (error) {
|
|
404
|
+
// 如果 package.json 有问题,但不是致命错误,记录警告
|
|
405
|
+
logger.warn(`工具 ${toolName} 的 package.json 文件有问题:`, error.message);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// 如果有依赖,尝试安装(使用工具依赖管理服务)
|
|
410
|
+
if (Object.keys(dependencies).length > 0) {
|
|
411
|
+
try {
|
|
412
|
+
const { ensureToolDependencies } = await import('../toolm/tool-dependency.service.js');
|
|
413
|
+
await ensureToolDependencies(toolName, null);
|
|
414
|
+
} catch (depError) {
|
|
415
|
+
// 依赖安装失败不是致命错误,但要记录
|
|
416
|
+
logger.warn(`工具 ${toolName} 依赖安装失败,将在运行时尝试:`, depError.message);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 验证工具是否能被工具加载器识别
|
|
421
|
+
try {
|
|
422
|
+
if (toolLoaderService.initialized) {
|
|
423
|
+
// 重新加载工具
|
|
424
|
+
await toolLoaderService.reload();
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 验证工具是否可以被加载
|
|
428
|
+
if (toolLoaderService.hasTool(toolName)) {
|
|
429
|
+
logger.info(`工具 ${toolName} 验证通过并已加载`);
|
|
430
|
+
}
|
|
431
|
+
} catch (loadError) {
|
|
432
|
+
logger.warn(`工具 ${toolName} 无法被工具加载器识别:`, loadError.message);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
res.json({
|
|
436
|
+
success: true,
|
|
437
|
+
message: `工具 ${toolName} 上传成功`,
|
|
438
|
+
toolName: toolName,
|
|
439
|
+
overwritten: toolExists && req.body.overwrite === 'true'
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
} catch (error) {
|
|
443
|
+
logger.error('工具上传失败:', error);
|
|
444
|
+
|
|
445
|
+
// 清理临时文件
|
|
446
|
+
try {
|
|
447
|
+
if (extractedDir && await pathExists(extractedDir)) {
|
|
448
|
+
await fse.remove(extractedDir);
|
|
449
|
+
}
|
|
450
|
+
} catch (cleanupError) {
|
|
451
|
+
logger.error('清理临时文件失败:', cleanupError);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
res.status(500).json({
|
|
455
|
+
success: false,
|
|
456
|
+
error: error.message
|
|
457
|
+
});
|
|
458
|
+
} finally {
|
|
459
|
+
// 确保临时上传的ZIP文件被清理
|
|
460
|
+
try {
|
|
461
|
+
if (tempZipPath && await pathExists(tempZipPath)) {
|
|
462
|
+
await fse.remove(tempZipPath);
|
|
463
|
+
}
|
|
464
|
+
} catch (cleanupError) {
|
|
465
|
+
logger.error('清理上传文件失败:', cleanupError);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// 确保临时解压目录被清理
|
|
469
|
+
try {
|
|
470
|
+
if (extractedDir && await pathExists(extractedDir)) {
|
|
471
|
+
await fse.remove(extractedDir);
|
|
472
|
+
}
|
|
473
|
+
} catch (cleanupError) {
|
|
474
|
+
logger.error('清理临时解压目录失败:', cleanupError);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
export { router as toolRouter };
|
package/packages/server/app.js
CHANGED
|
@@ -9,11 +9,15 @@ import { logger } from './utils/logger.js';
|
|
|
9
9
|
import { adminRouter } from './api/admin.routes.js';
|
|
10
10
|
import { openRouter } from './api/open.routes.js';
|
|
11
11
|
import { surgeRouter } from './api/surge.routes.js';
|
|
12
|
+
import { toolRouter } from './api/tool.routes.js';
|
|
12
13
|
import { getMcpServer } from './mcp/mcp.server.js';
|
|
13
14
|
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
14
15
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
15
16
|
import { InMemoryEventStore } from '@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js';
|
|
17
|
+
import { patchStreamableHTTPHeartbeat } from './mcp/heartbeat-patch.js';
|
|
16
18
|
|
|
19
|
+
// 初始化心跳补丁(防止 SSE 长连被中间层/客户端回收)
|
|
20
|
+
patchStreamableHTTPHeartbeat();
|
|
17
21
|
|
|
18
22
|
const app = express();
|
|
19
23
|
const adminUiRoot = util.getWebUiRoot();
|
|
@@ -82,8 +86,12 @@ app.use(express.urlencoded({ extended: true }));
|
|
|
82
86
|
const isAsarPath = adminUiRoot.includes('.asar') && fs.existsSync(adminUiRoot.replace(/\/.*$/, '.asar'));
|
|
83
87
|
if (isAsarPath) {
|
|
84
88
|
app.use(config.adminPath, serveAsarStatic(adminUiRoot));
|
|
89
|
+
// 为assets路径提供静态文件服务
|
|
90
|
+
app.use('/assets', serveAsarStatic(path.join(adminUiRoot, 'assets')));
|
|
85
91
|
} else {
|
|
86
92
|
app.use(config.adminPath, express.static(adminUiRoot));
|
|
93
|
+
// 为assets路径提供静态文件服务
|
|
94
|
+
app.use('/assets', express.static(path.join(adminUiRoot, 'assets')));
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
// 统一处理 index.html 请求的辅助函数
|
|
@@ -124,6 +132,17 @@ app.get(config.adminPath, sendIndexHtml);
|
|
|
124
132
|
app.get(config.adminPath + '/', sendIndexHtml);
|
|
125
133
|
|
|
126
134
|
|
|
135
|
+
// 健康检查端点
|
|
136
|
+
app.get('/health', (req, res) => {
|
|
137
|
+
res.status(200).json({
|
|
138
|
+
status: 'ok',
|
|
139
|
+
timestamp: new Date().toISOString(),
|
|
140
|
+
uptime: process.uptime(),
|
|
141
|
+
port: config.getPort(),
|
|
142
|
+
version: config.getServerVersion()
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
127
146
|
// 注册后台API
|
|
128
147
|
app.use('/adminapi', adminRouter);
|
|
129
148
|
|
|
@@ -133,9 +152,56 @@ app.use('/openapi', openRouter);
|
|
|
133
152
|
// 注册 Surge 静态资源代理 API
|
|
134
153
|
app.use('/surge', surgeRouter);
|
|
135
154
|
|
|
155
|
+
// 注册工具API
|
|
156
|
+
app.use('/tool', toolRouter);
|
|
157
|
+
|
|
136
158
|
|
|
137
159
|
const transports = {};
|
|
138
160
|
const mcpServers = {}; // 存储每个会话的MCP服务器实例
|
|
161
|
+
const eventStores = {}; // 存储每个会话的事件存储,以支持会话恢复
|
|
162
|
+
const sessionCleanupTimers = {}; // 延迟清理,给断线重连预留时间
|
|
163
|
+
|
|
164
|
+
// MCP 会话清理相关配置(可通过环境变量调整)
|
|
165
|
+
const MCP_SESSION_TTL_MS = parseInt(process.env.MCP_SESSION_TTL_MS || '600000', 10); // 断线后保留 10 分钟
|
|
166
|
+
|
|
167
|
+
function scheduleSessionCleanup(sid) {
|
|
168
|
+
if (!sid) return;
|
|
169
|
+
|
|
170
|
+
if (sessionCleanupTimers[sid]) {
|
|
171
|
+
clearTimeout(sessionCleanupTimers[sid]);
|
|
172
|
+
delete sessionCleanupTimers[sid];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
sessionCleanupTimers[sid] = setTimeout(() => {
|
|
176
|
+
if (transports[sid]) {
|
|
177
|
+
console.log(`Transport closed for session ${sid}, removing from transports map (delayed cleanup)`);
|
|
178
|
+
delete transports[sid];
|
|
179
|
+
}
|
|
180
|
+
if (mcpServers[sid]) {
|
|
181
|
+
delete mcpServers[sid];
|
|
182
|
+
}
|
|
183
|
+
if (eventStores[sid]) {
|
|
184
|
+
delete eventStores[sid];
|
|
185
|
+
}
|
|
186
|
+
delete sessionCleanupTimers[sid];
|
|
187
|
+
}, MCP_SESSION_TTL_MS);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function attachTransportLifecycle(transport) {
|
|
191
|
+
// Set a defensive sessionId for recovered transports
|
|
192
|
+
if (!transport.sessionId && typeof transport.sessionIdGenerator === 'function') {
|
|
193
|
+
transport.sessionId = transport.sessionIdGenerator();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
transport.onclose = () => {
|
|
197
|
+
const sid = transport.sessionId;
|
|
198
|
+
scheduleSessionCleanup(sid);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
transport.onerror = (error) => {
|
|
202
|
+
console.error('MCP Transport error:', error);
|
|
203
|
+
};
|
|
204
|
+
}
|
|
139
205
|
|
|
140
206
|
// 挂载MCP流式服务(独立路径前缀,避免冲突)
|
|
141
207
|
app.all('/mcp', (req, res) => {
|
|
@@ -161,13 +227,36 @@ app.all('/mcp', (req, res) => {
|
|
|
161
227
|
});
|
|
162
228
|
return;
|
|
163
229
|
}
|
|
230
|
+
} else if (sessionId && eventStores[sessionId] && mcpServers[sessionId]) {
|
|
231
|
+
// 断线后尝试恢复会话:为已有会话重新创建 transport
|
|
232
|
+
const eventStore = eventStores[sessionId];
|
|
233
|
+
transport = new StreamableHTTPServerTransport({
|
|
234
|
+
sessionIdGenerator: () => sessionId,
|
|
235
|
+
eventStore,
|
|
236
|
+
onsessioninitialized: async () => {
|
|
237
|
+
// 已有会话,不需要重新初始化
|
|
238
|
+
transports[sessionId] = transport;
|
|
239
|
+
// 重新连接已有的 MCP server
|
|
240
|
+
try {
|
|
241
|
+
const server = mcpServers[sessionId];
|
|
242
|
+
server.connect(transport);
|
|
243
|
+
console.log(`MCP server reconnected for session ${sessionId}`);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.error('会话恢复失败:', error);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
// 确保立即记录 transport
|
|
250
|
+
transports[sessionId] = transport;
|
|
251
|
+
attachTransportLifecycle(transport);
|
|
252
|
+
// 若存在延迟清理计时器,先取消
|
|
253
|
+
if (sessionCleanupTimers[sessionId]) {
|
|
254
|
+
clearTimeout(sessionCleanupTimers[sessionId]);
|
|
255
|
+
delete sessionCleanupTimers[sessionId];
|
|
256
|
+
}
|
|
164
257
|
} else if (!sessionId && req.method === 'POST' && isInitializeRequest(req.body)) {
|
|
165
258
|
const eventStore = new InMemoryEventStore();
|
|
166
|
-
|
|
167
|
-
// 预先创建 MCP 服务器实例,避免异步时序问题
|
|
168
|
-
let mcpServerPromise = null;
|
|
169
|
-
let serverReady = false;
|
|
170
|
-
|
|
259
|
+
|
|
171
260
|
transport = new StreamableHTTPServerTransport({
|
|
172
261
|
sessionIdGenerator: () => randomUUID(),
|
|
173
262
|
eventStore, // Enable resumability
|
|
@@ -175,39 +264,22 @@ app.all('/mcp', (req, res) => {
|
|
|
175
264
|
// Store the transport by session ID when session is initialized
|
|
176
265
|
console.log(`StreamableHTTP session initialized with ID: ${sessionId}`);
|
|
177
266
|
transports[sessionId] = transport;
|
|
267
|
+
eventStores[sessionId] = eventStore;
|
|
178
268
|
|
|
179
269
|
try {
|
|
180
270
|
// 为新会话创建MCP服务器实例(同步等待完成)
|
|
181
271
|
const server = await getMcpServer();
|
|
182
272
|
mcpServers[sessionId] = server;
|
|
183
273
|
server.connect(transport);
|
|
184
|
-
serverReady = true;
|
|
185
274
|
console.log(`MCP server connected for session ${sessionId}`);
|
|
186
275
|
} catch (error) {
|
|
187
276
|
console.error('创建MCP服务器失败:', error);
|
|
188
|
-
// 即使失败也标记为ready,避免阻塞
|
|
189
|
-
serverReady = true;
|
|
190
277
|
}
|
|
191
278
|
}
|
|
192
279
|
});
|
|
193
280
|
|
|
194
|
-
//
|
|
195
|
-
transport
|
|
196
|
-
const sid = transport.sessionId;
|
|
197
|
-
if (sid && transports[sid]) {
|
|
198
|
-
console.log(`Transport closed for session ${sid}, removing from transports map`);
|
|
199
|
-
delete transports[sid];
|
|
200
|
-
// 清理MCP服务器实例
|
|
201
|
-
if (mcpServers[sid]) {
|
|
202
|
-
delete mcpServers[sid];
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
// 添加错误处理
|
|
208
|
-
transport.onerror = (error) => {
|
|
209
|
-
console.error('MCP Transport error:', error);
|
|
210
|
-
};
|
|
281
|
+
// 统一注册关闭/错误处理(含延迟清理)
|
|
282
|
+
attachTransportLifecycle(transport);
|
|
211
283
|
} else {
|
|
212
284
|
// Invalid request - no session ID or not initialization request
|
|
213
285
|
res.status(400).json({
|