@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.
Files changed (110) hide show
  1. package/README.md +304 -121
  2. package/app/cli/commands/start.js +28 -4
  3. package/app/cli/support/argv.js +6 -0
  4. package/env.example +32 -0
  5. package/package.json +36 -6
  6. package/packages/server/api/admin.routes.js +409 -1
  7. package/packages/server/api/open.routes.js +7 -2
  8. package/packages/server/api/tool.routes.js +479 -0
  9. package/packages/server/app.js +97 -25
  10. package/packages/server/configs/models/built-in/bigmodel.yaml +6 -0
  11. package/packages/server/configs/models/providers.yaml +50 -0
  12. package/packages/server/configs/templates/built-in/general-iteration.yaml +60 -0
  13. package/packages/server/configs/templates/built-in/general-optimize.yaml +63 -0
  14. package/packages/server/configs/templates/built-in/output-format-optimize.yaml +95 -0
  15. package/packages/server/mcp/heartbeat-patch.js +73 -0
  16. package/packages/server/mcp/mcp.server.js +63 -314
  17. package/packages/server/mcp/prompt.handler.js +26 -0
  18. package/packages/server/mcp/thinking-toolkit.handler.js +380 -0
  19. package/packages/server/package.json +35 -3
  20. package/packages/server/server.js +114 -12
  21. package/packages/server/services/TerminalService.js +498 -0
  22. package/packages/server/services/WebSocketService.js +484 -0
  23. package/packages/server/services/manager.js +38 -7
  24. package/packages/server/services/model.service.js +473 -0
  25. package/packages/server/services/optimization.service.js +457 -0
  26. package/packages/server/services/template.service.js +333 -0
  27. package/packages/server/toolm/tool-description-generator-optimized.service.js +5 -2
  28. package/packages/server/toolm/tool-sync.service.js +47 -3
  29. package/packages/server/utils/config.js +8 -1
  30. package/packages/server/utils/port-checker.js +63 -0
  31. package/packages/server/utils/util.js +27 -0
  32. package/IFLOW.md +0 -175
  33. package/app/desktop/assets/app.1.png +0 -0
  34. package/app/desktop/assets/app.png +0 -0
  35. package/app/desktop/assets/icons/icon.icns +0 -0
  36. package/app/desktop/assets/icons/icon.ico +0 -0
  37. package/app/desktop/assets/icons/icon.png +0 -0
  38. package/app/desktop/assets/icons/tray.png +0 -0
  39. package/app/desktop/assets/templates/about.html +0 -147
  40. package/app/desktop/assets/tray.1.png +0 -0
  41. package/app/desktop/assets/tray.png +0 -0
  42. package/app/desktop/main.js +0 -241
  43. package/app/desktop/package-lock.json +0 -5026
  44. package/app/desktop/package.json +0 -100
  45. package/app/desktop/preload.js +0 -7
  46. package/app/desktop/src/core/error-handler.js +0 -108
  47. package/app/desktop/src/core/event-emitter.js +0 -84
  48. package/app/desktop/src/core/logger.js +0 -108
  49. package/app/desktop/src/core/state-manager.js +0 -125
  50. package/app/desktop/src/services/module-loader.js +0 -214
  51. package/app/desktop/src/services/runtime-manager.js +0 -301
  52. package/app/desktop/src/services/service-manager.js +0 -169
  53. package/app/desktop/src/services/update-manager.js +0 -267
  54. package/app/desktop/src/ui/about-dialog-manager.js +0 -208
  55. package/app/desktop/src/ui/admin-window-manager.js +0 -757
  56. package/app/desktop/src/ui/splash-manager.js +0 -253
  57. package/app/desktop/src/ui/tray-manager.js +0 -186
  58. package/app/desktop/src/utils/icon-manager.js +0 -133
  59. package/app/desktop/src/utils/path-utils.js +0 -58
  60. package/app/desktop/src/utils/resource-paths.js +0 -49
  61. package/app/desktop/src/utils/resource-sync.js +0 -260
  62. package/app/desktop/src/utils/runtime-sync.js +0 -241
  63. package/app/desktop/src/utils/template-renderer.js +0 -284
  64. package/app/desktop/src/utils/version-utils.js +0 -59
  65. package/examples/prompts/developer/code-review.yaml +0 -32
  66. package/examples/prompts/developer/code_refactoring.yaml +0 -31
  67. package/examples/prompts/developer/doc-generator.yaml +0 -36
  68. package/examples/prompts/developer/error-code-fixer.yaml +0 -35
  69. package/examples/prompts/engineer/engineer-professional.yaml +0 -92
  70. package/examples/prompts/engineer/laowang-engineer.yaml +0 -132
  71. package/examples/prompts/engineer/nekomata-engineer.yaml +0 -123
  72. package/examples/prompts/engineer/ojousama-engineer.yaml +0 -124
  73. package/examples/prompts/generator/gen_3d_edu_webpage_html.yaml +0 -117
  74. package/examples/prompts/generator/gen_3d_webpage_html.yaml +0 -75
  75. package/examples/prompts/generator/gen_bento_grid_html.yaml +0 -112
  76. package/examples/prompts/generator/gen_html_web_page.yaml +0 -88
  77. package/examples/prompts/generator/gen_knowledge_card_html.yaml +0 -83
  78. package/examples/prompts/generator/gen_magazine_card_html.yaml +0 -82
  79. package/examples/prompts/generator/gen_mimeng_headline_title.yaml +0 -71
  80. package/examples/prompts/generator/gen_podcast_script.yaml +0 -69
  81. package/examples/prompts/generator/gen_prd_prototype_html.yaml +0 -175
  82. package/examples/prompts/generator/gen_summarize.yaml +0 -157
  83. package/examples/prompts/generator/gen_title.yaml +0 -119
  84. package/examples/prompts/generator/others/api_documentation.yaml +0 -32
  85. package/examples/prompts/generator/others/build_mcp_server.yaml +0 -26
  86. package/examples/prompts/generator/others/project_architecture.yaml +0 -31
  87. package/examples/prompts/generator/others/test_case_generator.yaml +0 -30
  88. package/examples/prompts/generator/others/writing_assistant.yaml +0 -72
  89. package/examples/prompts/recommend/human_3-0_growth_diagnostic_coach_prompt.yaml +0 -105
  90. package/examples/prompts/workflow/sixstep-workflow.yaml +0 -192
  91. package/packages/admin-ui/.babelrc +0 -3
  92. package/packages/admin-ui/admin.html +0 -412
  93. package/packages/admin-ui/css/codemirror-theme_xq-light.css +0 -43
  94. package/packages/admin-ui/css/codemirror.css +0 -344
  95. package/packages/admin-ui/css/main.css +0 -2592
  96. package/packages/admin-ui/css/recommended-prompts.css +0 -610
  97. package/packages/admin-ui/package-lock.json +0 -6973
  98. package/packages/admin-ui/package.json +0 -36
  99. package/packages/admin-ui/src/codemirror.js +0 -53
  100. package/packages/admin-ui/src/index.js +0 -3188
  101. package/packages/admin-ui/webpack.config.js +0 -76
  102. package/packages/server/toolm/test-tools.js +0 -264
  103. package/scripts/build-icons.js +0 -135
  104. package/scripts/build.sh +0 -57
  105. package/scripts/postinstall.js +0 -34
  106. package/scripts/surge/CNAME +0 -1
  107. package/scripts/surge/README.md +0 -47
  108. package/scripts/surge/package-lock.json +0 -34
  109. package/scripts/surge/package.json +0 -20
  110. 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 };
@@ -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
- // Set up onclose handler to clean up transport when closed
195
- transport.onclose = () => {
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({
@@ -0,0 +1,6 @@
1
+ name: "BigModel"
2
+ provider: "BigModel"
3
+ model: "glm-4.5-air"
4
+ apiEndpoint: "https://open.bigmodel.cn/api/paas/v4/chat/completions"
5
+ apiKey: "ba6397b3478a47c79938c98b822a3ced.fGPlm03ACPDDsbmL"
6
+ enabled: true