@becrafter/prompt-manager 0.0.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 (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +326 -0
  3. package/app/cli/cli.js +17 -0
  4. package/app/cli/commands/start.js +20 -0
  5. package/app/cli/index.js +37 -0
  6. package/app/cli/support/argv.js +7 -0
  7. package/app/cli/support/signals.js +34 -0
  8. package/app/desktop/assets/icon.png +0 -0
  9. package/app/desktop/main.js +496 -0
  10. package/app/desktop/package-lock.json +4091 -0
  11. package/app/desktop/package.json +63 -0
  12. package/examples/prompts/developer/code-review.yaml +32 -0
  13. package/examples/prompts/developer/code_refactoring.yaml +31 -0
  14. package/examples/prompts/developer/doc-generator.yaml +36 -0
  15. package/examples/prompts/developer/error-code-fixer.yaml +35 -0
  16. package/examples/prompts/generator/gen_3d_edu_webpage_html.yaml +117 -0
  17. package/examples/prompts/generator/gen_3d_webpage_html.yaml +75 -0
  18. package/examples/prompts/generator/gen_bento_grid_html.yaml +112 -0
  19. package/examples/prompts/generator/gen_html_web_page.yaml +88 -0
  20. package/examples/prompts/generator/gen_knowledge_card_html.yaml +83 -0
  21. package/examples/prompts/generator/gen_magazine_card_html.yaml +82 -0
  22. package/examples/prompts/generator/gen_mimeng_headline_title.yaml +71 -0
  23. package/examples/prompts/generator/gen_podcast_script.yaml +69 -0
  24. package/examples/prompts/generator/gen_prd_prototype_html.yaml +175 -0
  25. package/examples/prompts/generator/gen_summarize.yaml +157 -0
  26. package/examples/prompts/generator/gen_title.yaml +119 -0
  27. package/examples/prompts/generator/others/api_documentation.yaml +32 -0
  28. package/examples/prompts/generator/others/build_mcp_server.yaml +26 -0
  29. package/examples/prompts/generator/others/project_architecture.yaml +31 -0
  30. package/examples/prompts/generator/others/test_case_generator.yaml +30 -0
  31. package/examples/prompts/generator/others/writing_assistant.yaml +72 -0
  32. package/package.json +54 -0
  33. package/packages/admin-ui/admin.html +4959 -0
  34. package/packages/admin-ui/css/codemirror-theme_xq-light.css +43 -0
  35. package/packages/admin-ui/css/codemirror.css +344 -0
  36. package/packages/admin-ui/js/closebrackets.min.js +8 -0
  37. package/packages/admin-ui/js/codemirror.min.js +8 -0
  38. package/packages/admin-ui/js/js-yaml.min.js +2 -0
  39. package/packages/admin-ui/js/markdown.min.js +8 -0
  40. package/packages/server/config.js +283 -0
  41. package/packages/server/logger.js +55 -0
  42. package/packages/server/manager.js +473 -0
  43. package/packages/server/mcp.js +234 -0
  44. package/packages/server/mcpManager.js +205 -0
  45. package/packages/server/server.js +1001 -0
  46. package/scripts/postinstall.js +34 -0
@@ -0,0 +1,1001 @@
1
+ import express from 'express';
2
+ import crypto from 'crypto';
3
+ import fs from 'fs';
4
+ import fse from 'fs-extra';
5
+ import path from 'path';
6
+ import yaml from 'js-yaml';
7
+ import { fileURLToPath, pathToFileURL } from 'url';
8
+ import { config } from './config.js';
9
+ import { logger } from './logger.js';
10
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
13
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
14
+ import { mcpManager } from './mcpManager.js';
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+
19
+ const app = express();
20
+ app.use(express.json());
21
+
22
+ const adminUiRoot = path.join(__dirname, '..', 'admin-ui');
23
+ const examplesPromptsRoot = path.join(__dirname, '..', '..', 'examples', 'prompts');
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
+ // 获取prompts目录路径(在启动时可能被覆盖)
34
+ let promptsDir = config.getPromptsDir();
35
+
36
+ async function seedPromptsIfEmpty() {
37
+ try {
38
+ const entries = await fse.readdir(promptsDir);
39
+ if (entries.length > 0) {
40
+ return;
41
+ }
42
+ } catch (error) {
43
+ logger.warn('读取Prompts目录失败,尝试同步示例数据:', error.message);
44
+ }
45
+
46
+ try {
47
+ const exists = await fse.pathExists(examplesPromptsRoot);
48
+ if (!exists) {
49
+ return;
50
+ }
51
+ await fse.copy(examplesPromptsRoot, promptsDir, {
52
+ overwrite: false,
53
+ errorOnExist: false,
54
+ recursive: true
55
+ });
56
+ logger.info(`已将示例Prompts同步到 ${promptsDir}`);
57
+ } catch (error) {
58
+ logger.warn('同步示例Prompts失败:', error.message);
59
+ }
60
+ }
61
+ const GROUP_META_FILENAME = '.groupmeta.json';
62
+ const GROUP_NAME_REGEX = /^[a-zA-Z0-9-_\u4e00-\u9fa5]{1,64}$/;
63
+
64
+ function generateUniqueId(relativePath) {
65
+ const hash = crypto.createHash('sha256');
66
+ hash.update(relativePath);
67
+ const hashHex = hash.digest('hex');
68
+ return hashHex.substring(0, 8);
69
+ }
70
+
71
+ function getPromptsFromFiles() {
72
+ const prompts = [];
73
+
74
+ function traverseDir(currentPath, relativeDir = '', inheritedEnabled = true) {
75
+ let currentEnabled = inheritedEnabled;
76
+ if (relativeDir) {
77
+ const meta = readGroupMeta(currentPath);
78
+ currentEnabled = currentEnabled && (meta.enabled !== false);
79
+ }
80
+
81
+ try {
82
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true });
83
+ for (const entry of entries) {
84
+ const fullPath = path.join(currentPath, entry.name);
85
+ if (entry.isDirectory()) {
86
+ const childRelativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
87
+ traverseDir(fullPath, childRelativePath, currentEnabled);
88
+ } else if (entry.isFile() && entry.name.endsWith('.yaml')) {
89
+ try {
90
+ const fileContent = fs.readFileSync(fullPath, 'utf8');
91
+ const prompt = yaml.load(fileContent);
92
+ if (prompt && prompt.name) {
93
+ const relativePath = path.relative(promptsDir, fullPath);
94
+ const normalizedRelativePath = relativePath.split(path.sep).join('/');
95
+ const relativeDirForFile = path.dirname(normalizedRelativePath);
96
+ const topLevelGroup = relativeDirForFile && relativeDirForFile !== '.' ? relativeDirForFile.split('/')[0] : (prompt.group || 'default');
97
+ const groupPath = relativeDirForFile && relativeDirForFile !== '.' ? relativeDirForFile : topLevelGroup;
98
+ prompts.push({
99
+ ...prompt,
100
+ uniqueId: generateUniqueId(prompt.name + '.yaml'),
101
+ fileName: entry.name,
102
+ relativePath: normalizedRelativePath,
103
+ group: topLevelGroup,
104
+ groupPath,
105
+ groupEnabled: currentEnabled
106
+ });
107
+ }
108
+ } catch (error) {
109
+ logger.error(`Error processing file ${fullPath}:`, error);
110
+ }
111
+ }
112
+ }
113
+ } catch (error) {
114
+ logger.error(`Error reading directory ${currentPath}:`, error);
115
+ }
116
+ }
117
+
118
+ traverseDir(promptsDir);
119
+ return prompts;
120
+ }
121
+
122
+ /**
123
+ * 计算搜索关键词与prompt的相似度得分
124
+ * @param {string} searchTerm - 搜索关键词
125
+ * @param {Object} prompt - prompt对象
126
+ * @returns {number} 相似度得分 (0-100)
127
+ */
128
+ function calculateSimilarityScore(searchTerm, prompt) {
129
+ const searchLower = searchTerm.toLowerCase();
130
+ let totalScore = 0;
131
+
132
+ // 搜索字段权重配置(专注于内容搜索,不包含ID检索)
133
+ const fieldWeights = {
134
+ name: 60, // 名称权重高,是主要匹配字段
135
+ description: 40 // 描述权重适中,是辅助匹配字段
136
+ };
137
+
138
+ // 计算name匹配得分
139
+ if (prompt.name) {
140
+ const nameScore = getStringMatchScore(searchLower, prompt.name.toLowerCase());
141
+ totalScore += nameScore * fieldWeights.name;
142
+ }
143
+
144
+ // 计算description匹配得分
145
+ if (prompt.description) {
146
+ const descScore = getStringMatchScore(searchLower, prompt.description.toLowerCase());
147
+ totalScore += descScore * fieldWeights.description;
148
+ }
149
+
150
+ // 标准化得分到0-100范围
151
+ const maxPossibleScore = Object.values(fieldWeights).reduce((sum, weight) => sum + weight, 0);
152
+ return Math.round((totalScore / maxPossibleScore) * 100);
153
+ }
154
+
155
+ /**
156
+ * 计算两个字符串的匹配得分
157
+ * @param {string} search - 搜索词 (已转小写)
158
+ * @param {string} target - 目标字符串 (已转小写)
159
+ * @returns {number} 匹配得分 (0-1)
160
+ */
161
+ function getStringMatchScore(search, target) {
162
+ if (!search || !target) return 0;
163
+
164
+ // 完全匹配得分最高
165
+ if (target === search) return 1.0;
166
+
167
+ // 完全包含得分较高
168
+ if (target.includes(search)) return 0.8;
169
+
170
+ // 部分词匹配
171
+ const searchWords = search.split(/\s+/).filter(word => word.length > 0);
172
+ const targetWords = target.split(/\s+/).filter(word => word.length > 0);
173
+
174
+ let matchedWords = 0;
175
+ for (const searchWord of searchWords) {
176
+ for (const targetWord of targetWords) {
177
+ if (targetWord.includes(searchWord) || searchWord.includes(targetWord)) {
178
+ matchedWords++;
179
+ break;
180
+ }
181
+ }
182
+ }
183
+
184
+ if (searchWords.length > 0) {
185
+ const wordMatchRatio = matchedWords / searchWords.length;
186
+ return wordMatchRatio * 0.6; // 部分词匹配得分
187
+ }
188
+
189
+ return 0;
190
+ }
191
+
192
+ // 获取服务器信息
193
+ app.get('/', (req, res) => {
194
+ res.status(404).send('404 Not Found');
195
+ });
196
+
197
+ app.get('/prompts', (req, res) => {
198
+ try {
199
+ const prompts = getPromptsFromFiles();
200
+
201
+ // 过滤出启用的提示词
202
+ const filtered = prompts.filter(prompt => {
203
+ const groupActive = prompt.groupEnabled !== false;
204
+ const promptActive = prompt.enabled === true;
205
+ return groupActive && promptActive;
206
+ });
207
+ logger.debug(`filtered prompts: ${JSON.stringify(filtered)}`);
208
+
209
+ // 判断是否有搜索参数,且搜索参数名为search
210
+ if (req.query.search) {
211
+ const search = req.query.search;
212
+
213
+ // 实现相似度匹配算法
214
+ const searchResults = filtered.map(prompt => {
215
+ const score = calculateSimilarityScore(search, prompt);
216
+ return {
217
+ prompt: {
218
+ id: prompt.uniqueId,
219
+ name: prompt.name,
220
+ description: prompt.description || `Prompt: ${prompt.name}`,
221
+ metadata: {
222
+ // fileName: prompt.fileName,
223
+ fullPath: prompt.relativePath
224
+ }
225
+ },
226
+ score: score
227
+ };
228
+ })
229
+ .filter(result => result.score > 0) // 只返回有匹配的结果
230
+ .sort((a, b) => b.score - a.score); // 按相似度得分降序排列
231
+
232
+ // 只返回prompt字段
233
+ res.json(searchResults.map(result => result.prompt));
234
+ } else {
235
+ // 无搜索参数时,返回精简信息
236
+ const simplifiedPrompts = filtered.map(prompt => ({
237
+ id: prompt.uniqueId,
238
+ name: prompt.name,
239
+ description: prompt.description || `Prompt: ${prompt.name}`,
240
+ metadata: {
241
+ // fileName: prompt.fileName,
242
+ fullPath: prompt.relativePath
243
+ }
244
+ }));
245
+
246
+ res.json(simplifiedPrompts);
247
+ }
248
+ } catch (error) {
249
+ res.status(500).json({ error: error.message });
250
+ }
251
+ });
252
+
253
+ async function processPromptContent(prompt, args) {
254
+ let content = '';
255
+
256
+ if (prompt.messages && Array.isArray(prompt.messages)) {
257
+ const userMessages = prompt.messages.filter(msg => msg.role === 'user');
258
+
259
+ for (const message of userMessages) {
260
+ if (message.content && typeof message.content.text === 'string') {
261
+ let text = message.content.text;
262
+
263
+ // Replace arguments
264
+ if (args) {
265
+ for (const [key, value] of Object.entries(args)) {
266
+ const placeholder = new RegExp(`{{${key}}}`, 'g');
267
+ text = text.replace(placeholder, String(value));
268
+ }
269
+ }
270
+
271
+ content += text + '\n\n';
272
+ }
273
+ }
274
+ }
275
+
276
+ return content.trim();
277
+ }
278
+
279
+ // 管理员API中间件
280
+ function adminAuthMiddleware(req, res, next) {
281
+ // 检查是否启用了管理员功能
282
+ if (!config.adminEnable) {
283
+ return res.status(404).json({ error: 'Admin功能未启用' });
284
+ }
285
+
286
+ const authHeader = req.headers.authorization;
287
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
288
+ return res.status(401).json({ error: '未提供认证令牌' });
289
+ }
290
+
291
+ const token = authHeader.substring(7);
292
+
293
+ // 验证令牌
294
+ const admin = config.admins.find(a => a.token === token);
295
+ if (!admin) {
296
+ return res.status(401).json({ error: '无效的认证令牌' });
297
+ }
298
+
299
+ req.admin = admin;
300
+ next();
301
+ }
302
+
303
+ // 登录端点
304
+ app.post('/api/login', (req, res) => {
305
+ // 检查是否启用了管理员功能
306
+ if (!config.adminEnable) {
307
+ return res.status(404).json({ error: 'Admin功能未启用' });
308
+ }
309
+
310
+ const { username, password } = req.body;
311
+
312
+ if (!username || !password) {
313
+ return res.status(400).json({ error: '用户名和密码是必需的' });
314
+ }
315
+
316
+ // 验证凭据
317
+ const admin = config.admins.find(a => a.username === username && a.password === password);
318
+ if (!admin) {
319
+ return res.status(401).json({ error: '无效的用户名或密码' });
320
+ }
321
+
322
+ res.json({ token: admin.token });
323
+ });
324
+
325
+ function isValidGroupName(name) {
326
+ return GROUP_NAME_REGEX.test(name);
327
+ }
328
+
329
+ function validateGroupPath(relativePath) {
330
+ if (!relativePath || typeof relativePath !== 'string') {
331
+ return null;
332
+ }
333
+ const segments = relativePath.split('/').filter(Boolean);
334
+ if (!segments.length) {
335
+ return null;
336
+ }
337
+ for (const segment of segments) {
338
+ if (!isValidGroupName(segment)) {
339
+ return null;
340
+ }
341
+ }
342
+ return segments;
343
+ }
344
+
345
+ function resolveGroupDir(relativePath) {
346
+ const segments = validateGroupPath(relativePath);
347
+ if (!segments) return null;
348
+ const targetPath = path.resolve(promptsDir, ...segments);
349
+ const normalizedPromptsDir = path.resolve(promptsDir);
350
+ if (!targetPath.startsWith(normalizedPromptsDir)) {
351
+ return null;
352
+ }
353
+ return { dir: targetPath, segments };
354
+ }
355
+
356
+ function getGroupMetaPath(dir) {
357
+ return path.join(dir, GROUP_META_FILENAME);
358
+ }
359
+
360
+ function readGroupMeta(dir) {
361
+ try {
362
+ const metaPath = getGroupMetaPath(dir);
363
+ if (!fs.existsSync(metaPath)) {
364
+ return { enabled: true };
365
+ }
366
+ const raw = fs.readFileSync(metaPath, 'utf8');
367
+ const data = JSON.parse(raw);
368
+ return {
369
+ enabled: data.enabled !== false
370
+ };
371
+ } catch (error) {
372
+ logger.warn('读取类目元数据失败:', error);
373
+ return { enabled: true };
374
+ }
375
+ }
376
+
377
+ function writeGroupMeta(dir, meta = {}) {
378
+ const metaPath = getGroupMetaPath(dir);
379
+ const data = {
380
+ enabled: meta.enabled !== false
381
+ };
382
+ fs.writeFileSync(metaPath, JSON.stringify(data, null, 2), 'utf8');
383
+ }
384
+
385
+ // 获取所有分组(直接从目录读取)
386
+ function buildGroupTree(baseDir, relativePath = '') {
387
+ const nodes = [];
388
+ try {
389
+ const entries = fs.readdirSync(baseDir, { withFileTypes: true });
390
+ for (const entry of entries) {
391
+ if (!entry.isDirectory()) continue;
392
+ if (entry.name.startsWith('.')) continue;
393
+ const childRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
394
+ const childDir = path.join(baseDir, entry.name);
395
+ const children = buildGroupTree(childDir, childRelativePath);
396
+ const meta = readGroupMeta(childDir);
397
+ nodes.push({
398
+ name: entry.name,
399
+ path: childRelativePath,
400
+ children,
401
+ enabled: meta.enabled !== false
402
+ });
403
+ }
404
+ } catch (error) {
405
+ logger.error('读取分组目录失败:', error);
406
+ }
407
+ nodes.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
408
+ return nodes;
409
+ }
410
+
411
+ app.get('/api/groups', adminAuthMiddleware, (req, res) => {
412
+ try {
413
+ const tree = buildGroupTree(promptsDir);
414
+ const hasDefault = tree.some(node => node.path === 'default');
415
+ if (!hasDefault) {
416
+ tree.unshift({ name: 'default', path: 'default', children: [], enabled: true });
417
+ }
418
+ res.json(tree);
419
+ } catch (error) {
420
+ res.status(500).json({ error: error.message });
421
+ }
422
+ });
423
+
424
+ // 获取所有提示词(支持搜索、过滤和分组)
425
+ app.get('/api/prompts', adminAuthMiddleware, (req, res) => {
426
+ try {
427
+ const prompts = getPromptsFromFiles();
428
+
429
+ // 处理搜索参数
430
+ const search = req.query.search;
431
+ const enabled = req.query.enabled === 'true';
432
+ const groupPathFilter = req.query.groupPath;
433
+ const group = req.query.group;
434
+
435
+ let filteredPrompts = prompts;
436
+
437
+ // 应用分组过滤
438
+ if (groupPathFilter) {
439
+ filteredPrompts = filteredPrompts.filter(prompt => (prompt.groupPath || prompt.group || 'default') === groupPathFilter);
440
+ } else if (group) {
441
+ filteredPrompts = filteredPrompts.filter(prompt => (prompt.group || 'default') === group);
442
+ }
443
+
444
+ // 应用搜索过滤
445
+ if (search) {
446
+ filteredPrompts = filteredPrompts.filter(prompt =>
447
+ prompt.name.includes(search) ||
448
+ (prompt.description && prompt.description.includes(search))
449
+ );
450
+ }
451
+
452
+ // 应用启用状态过滤
453
+ if (enabled) {
454
+ filteredPrompts = filteredPrompts.filter(prompt => prompt.enabled);
455
+ }
456
+
457
+ filteredPrompts.sort((a, b) => (a.name || '').localeCompare(b.name || '', 'zh-CN'));
458
+
459
+ res.json(filteredPrompts);
460
+ } catch (error) {
461
+ res.status(500).json({ error: error.message });
462
+ }
463
+ });
464
+
465
+ // 获取单个提示词
466
+ app.get('/api/prompts/:name', adminAuthMiddleware, (req, res) => {
467
+ try {
468
+ const prompts = getPromptsFromFiles();
469
+ const targetPath = req.query.path;
470
+ let prompt;
471
+ if (targetPath) {
472
+ prompt = prompts.find(p => p.relativePath === targetPath);
473
+ }
474
+ if (!prompt) {
475
+ prompt = prompts.find(p => p.name === req.params.name);
476
+ }
477
+
478
+ if (!prompt) {
479
+ return res.status(404).json({ error: `Prompt "${req.params.name}" 未找到` });
480
+ }
481
+
482
+ // 读取原始YAML文件内容
483
+ const promptPath = path.join(promptsDir, prompt.relativePath);
484
+ const yamlContent = fs.readFileSync(promptPath, 'utf8');
485
+
486
+ res.json({
487
+ ...prompt,
488
+ yaml: yamlContent
489
+ });
490
+ } catch (error) {
491
+ res.status(500).json({ error: error.message });
492
+ }
493
+ });
494
+
495
+ // 保存提示词
496
+ app.post('/api/prompts', adminAuthMiddleware, (req, res) => {
497
+ try {
498
+ const { name, group, yaml: yamlContent, relativePath: originalRelativePath } = req.body;
499
+
500
+ if (!name || !yamlContent) {
501
+ return res.status(400).json({ error: '名称和YAML内容是必需的' });
502
+ }
503
+
504
+ // 验证名称格式
505
+ if (!/^[a-zA-Z0-9-_]{1,64}$/.test(name)) {
506
+ return res.status(400).json({ error: '名称格式无效' });
507
+ }
508
+
509
+ // 计算目标路径
510
+ const groupName = group || 'default';
511
+ const normalizedOriginalPath = originalRelativePath ? path.normalize(originalRelativePath).replace(/\\/g, '/') : null;
512
+ const originalDirParts = normalizedOriginalPath ? path.posix.dirname(normalizedOriginalPath).split('/').filter(Boolean) : [];
513
+ let subPathSegments = [];
514
+
515
+ if (normalizedOriginalPath && originalDirParts.length > 1) {
516
+ subPathSegments = originalDirParts.slice(1);
517
+ }
518
+
519
+ const targetSegments = [];
520
+ if (groupName) {
521
+ targetSegments.push(groupName);
522
+ }
523
+ if (subPathSegments.length) {
524
+ targetSegments.push(...subPathSegments);
525
+ }
526
+
527
+ const finalFileName = `${name}.yaml`;
528
+ targetSegments.push(finalFileName);
529
+
530
+ const targetRelativePath = path.posix.join(...targetSegments);
531
+ const targetDir = path.join(promptsDir, path.posix.dirname(targetRelativePath));
532
+ const filePath = path.join(promptsDir, targetRelativePath);
533
+
534
+ fse.ensureDirSync(targetDir);
535
+
536
+ // 检查是否重名(同目录下)
537
+ const prompts = getPromptsFromFiles();
538
+ const existingPrompt = prompts.find(p => {
539
+ if (p.name !== name) return false;
540
+ const isOriginalFile = normalizedOriginalPath && p.relativePath === normalizedOriginalPath;
541
+ if (isOriginalFile) return false;
542
+ const sameRelativePath = p.relativePath === targetRelativePath;
543
+ if (sameRelativePath) return false;
544
+ const sameDirectory = path.posix.dirname(p.relativePath || '') === path.posix.dirname(targetRelativePath);
545
+ return sameDirectory;
546
+ });
547
+
548
+ if (existingPrompt) {
549
+ return res.status(400).json({ error: '名称已存在' });
550
+ }
551
+
552
+ // 保存文件
553
+ fs.writeFileSync(filePath, yamlContent, 'utf8');
554
+
555
+ // 如果目标路径与原始路径不同,删除旧文件
556
+ if (normalizedOriginalPath && normalizedOriginalPath !== targetRelativePath) {
557
+ const originalFilePath = path.join(promptsDir, normalizedOriginalPath);
558
+ if (fs.existsSync(originalFilePath)) {
559
+ fs.unlinkSync(originalFilePath);
560
+ }
561
+ }
562
+
563
+ res.json({ message: '保存成功', relativePath: targetRelativePath, group: groupName });
564
+ } catch (error) {
565
+ res.status(500).json({ error: error.message });
566
+ }
567
+ });
568
+
569
+ // 创建新分组目录
570
+ app.post('/api/groups', adminAuthMiddleware, (req, res) => {
571
+ try {
572
+ const { name } = req.body;
573
+
574
+ if (!name) {
575
+ return res.status(400).json({ error: '分组名称是必需的' });
576
+ }
577
+
578
+ // 验证名称格式
579
+ if (!isValidGroupName(name)) {
580
+ return res.status(400).json({ error: '名称格式无效,只能包含字母、数字、中划线、下划线和中文' });
581
+ }
582
+
583
+ const groupDir = path.join(promptsDir, name);
584
+
585
+ // 检查目录是否已存在
586
+ if (fs.existsSync(groupDir)) {
587
+ return res.status(400).json({ error: '分组已存在' });
588
+ }
589
+
590
+ // 创建目录
591
+ fs.mkdirSync(groupDir, { recursive: true });
592
+
593
+ res.json({ message: '分组创建成功' });
594
+ } catch (error) {
595
+ res.status(500).json({ error: error.message });
596
+ }
597
+ });
598
+
599
+ app.patch('/api/groups/rename', adminAuthMiddleware, (req, res) => {
600
+ try {
601
+ const { path: groupPath, newName } = req.body || {};
602
+ if (!groupPath || !newName) {
603
+ return res.status(400).json({ error: '分组路径和新名称是必需的' });
604
+ }
605
+ if (!isValidGroupName(newName)) {
606
+ return res.status(400).json({ error: '名称格式无效,只能包含字母、数字、中划线、下划线和中文' });
607
+ }
608
+ if (groupPath === 'default') {
609
+ return res.status(400).json({ error: '默认分组不允许重命名' });
610
+ }
611
+
612
+ const resolved = resolveGroupDir(groupPath);
613
+ if (!resolved) {
614
+ return res.status(400).json({ error: '无效的分组路径' });
615
+ }
616
+ const { dir: oldDir, segments } = resolved;
617
+ if (!fs.existsSync(oldDir) || !fs.lstatSync(oldDir).isDirectory()) {
618
+ return res.status(404).json({ error: '分组不存在' });
619
+ }
620
+
621
+ const parentSegments = segments.slice(0, -1);
622
+ const oldName = segments[segments.length - 1];
623
+ if (newName === oldName) {
624
+ return res.json({ message: '分组名称未变更', path: groupPath });
625
+ }
626
+ const newSegments = [...parentSegments, newName];
627
+ const newDir = path.resolve(promptsDir, ...newSegments);
628
+ if (fs.existsSync(newDir)) {
629
+ return res.status(400).json({ error: '目标名称已存在,请选择其他名称' });
630
+ }
631
+
632
+ fse.moveSync(oldDir, newDir);
633
+
634
+ res.json({ message: '分组重命名成功', path: newSegments.join('/') });
635
+ } catch (error) {
636
+ logger.error('分组重命名失败:', error);
637
+ res.status(500).json({ error: error.message });
638
+ }
639
+ });
640
+
641
+ app.patch('/api/groups/status', adminAuthMiddleware, (req, res) => {
642
+ try {
643
+ const { path: groupPath, enabled } = req.body || {};
644
+ if (typeof enabled !== 'boolean') {
645
+ return res.status(400).json({ error: '状态值无效' });
646
+ }
647
+ if (!groupPath) {
648
+ return res.status(400).json({ error: '分组路径是必需的' });
649
+ }
650
+
651
+ const resolved = resolveGroupDir(groupPath);
652
+ if (!resolved) {
653
+ return res.status(400).json({ error: '无效的分组路径' });
654
+ }
655
+ const { dir } = resolved;
656
+ if (!fs.existsSync(dir) || !fs.lstatSync(dir).isDirectory()) {
657
+ return res.status(404).json({ error: '分组不存在' });
658
+ }
659
+
660
+ writeGroupMeta(dir, { enabled });
661
+
662
+ res.json({ message: '分组状态已更新', enabled });
663
+ } catch (error) {
664
+ logger.error('更新分组状态失败:', error);
665
+ res.status(500).json({ error: error.message });
666
+ }
667
+ });
668
+
669
+ app.delete('/api/groups', adminAuthMiddleware, (req, res) => {
670
+ try {
671
+ const groupPath = req.query.path;
672
+ if (!groupPath) {
673
+ return res.status(400).json({ error: '分组路径是必需的' });
674
+ }
675
+ if (groupPath === 'default') {
676
+ return res.status(400).json({ error: '默认分组不允许删除' });
677
+ }
678
+
679
+ const resolved = resolveGroupDir(groupPath);
680
+ if (!resolved) {
681
+ return res.status(400).json({ error: '无效的分组路径' });
682
+ }
683
+ const { dir } = resolved;
684
+ if (!fs.existsSync(dir) || !fs.lstatSync(dir).isDirectory()) {
685
+ return res.status(404).json({ error: '分组不存在' });
686
+ }
687
+
688
+ const entries = fs.readdirSync(dir, { withFileTypes: true })
689
+ .filter(entry => entry.name !== GROUP_META_FILENAME && !entry.name.startsWith('.'));
690
+
691
+ if (entries.length > 0) {
692
+ return res.status(400).json({ error: '目录非空,请先移除其下的Prompt或子目录' });
693
+ }
694
+
695
+ fse.removeSync(dir);
696
+ res.json({ message: '分组删除成功' });
697
+ } catch (error) {
698
+ logger.error('删除分组失败:', error);
699
+ res.status(500).json({ error: error.message });
700
+ }
701
+ });
702
+
703
+ // 切换提示词启用状态
704
+ app.post('/api/prompts/:name/toggle', adminAuthMiddleware, (req, res) => {
705
+ try {
706
+ const prompts = getPromptsFromFiles();
707
+ const targetPath = req.query.path;
708
+ let prompt;
709
+ if (targetPath) {
710
+ prompt = prompts.find(p => p.relativePath === targetPath);
711
+ }
712
+ if (!prompt) {
713
+ prompt = prompts.find(p => p.name === req.params.name);
714
+ }
715
+
716
+ if (!prompt) {
717
+ return res.status(404).json({ error: `Prompt "${req.params.name}" 未找到` });
718
+ }
719
+
720
+ // 读取原始YAML文件内容
721
+ const promptPath = path.join(promptsDir, prompt.relativePath);
722
+ const yamlContent = fs.readFileSync(promptPath, 'utf8');
723
+
724
+ // 解析YAML
725
+ const promptData = yaml.load(yamlContent);
726
+
727
+ // 切换启用状态
728
+ promptData.enabled = !promptData.enabled;
729
+
730
+ // 保存更新后的YAML
731
+ const newYamlContent = yaml.dump(promptData);
732
+ fs.writeFileSync(promptPath, newYamlContent, 'utf8');
733
+
734
+ res.json({ message: '状态切换成功', enabled: promptData.enabled });
735
+ } catch (error) {
736
+ res.status(500).json({ error: error.message });
737
+ }
738
+ });
739
+
740
+ // 删除提示词(软删)
741
+ app.delete('/api/prompts/:name', adminAuthMiddleware, (req, res) => {
742
+ try {
743
+ const prompts = getPromptsFromFiles();
744
+ const targetPath = req.query.path;
745
+ let prompt;
746
+ if (targetPath) {
747
+ prompt = prompts.find(p => p.relativePath === targetPath);
748
+ }
749
+ if (!prompt) {
750
+ prompt = prompts.find(p => p.name === req.params.name);
751
+ }
752
+
753
+ if (!prompt) {
754
+ return res.status(404).json({ error: `Prompt "${req.params.name}" 未找到` });
755
+ }
756
+
757
+ // 读取原始文件路径
758
+ const promptPath = path.join(promptsDir, prompt.relativePath);
759
+
760
+ // 直接删除文件
761
+ fse.unlinkSync(promptPath);
762
+
763
+ res.json({ message: '删除成功' });
764
+ } catch (error) {
765
+ res.status(500).json({ error: error.message });
766
+ }
767
+ });
768
+
769
+ // Markdown预览
770
+ app.post('/api/md-preview', adminAuthMiddleware, (req, res) => {
771
+ try {
772
+ const { yaml: yamlContent, vars } = req.body;
773
+
774
+ if (!yamlContent) {
775
+ return res.status(400).json({ error: 'YAML内容是必需的' });
776
+ }
777
+
778
+ // 解析YAML
779
+ const promptData = yaml.load(yamlContent);
780
+
781
+ // 处理变量替换
782
+ let content = '';
783
+ if (promptData.messages && Array.isArray(promptData.messages)) {
784
+ const userMessages = promptData.messages.filter(msg => msg.role === 'user');
785
+
786
+ for (const message of userMessages) {
787
+ if (message.content && typeof message.content.text === 'string') {
788
+ let text = message.content.text;
789
+
790
+ // 替换变量
791
+ if (vars) {
792
+ for (const [key, value] of Object.entries(vars)) {
793
+ const placeholder = new RegExp(`{{${key}}}`, 'g');
794
+ text = text.replace(placeholder, String(value));
795
+ }
796
+ }
797
+
798
+ content += text + '\n\n';
799
+ }
800
+ }
801
+ }
802
+
803
+ // 简单的Markdown转HTML(实际应用中可以使用专门的库)
804
+ const html = content
805
+ .replace(/&/g, '&')
806
+ .replace(/</g, '&lt;')
807
+ .replace(/>/g, '&gt;')
808
+ .replace(/\n/g, '<br>')
809
+ .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
810
+ .replace(/\*(.*?)\*/g, '<em>$1</em>')
811
+ .replace(/`(.*?)`/g, '<code>$1</code>')
812
+ .replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>')
813
+ .replace(/### (.*?)(<br>|$)/g, '<h3>$1</h3>')
814
+ .replace(/## (.*?)(<br>|$)/g, '<h2>$1</h2>')
815
+ .replace(/# (.*?)(<br>|$)/g, '<h1>$1</h1>');
816
+
817
+ res.json({ html });
818
+ } catch (error) {
819
+ res.status(500).json({ error: error.message });
820
+ }
821
+ });
822
+
823
+ app.post('/process', async (req, res) => {
824
+ try {
825
+ const { promptName, arguments: args } = req.body;
826
+
827
+ if (!promptName) {
828
+ return res.status(400).json({ error: 'Missing promptName' });
829
+ }
830
+
831
+ const prompts = getPromptsFromFiles();
832
+ const prompt = prompts.find(p => p.name === promptName);
833
+
834
+ if (!prompt) {
835
+ return res.status(404).json({ error: `Prompt "${promptName}" not found` });
836
+ }
837
+
838
+ const processedPrompt = await processPromptContent(prompt, args);
839
+ res.json({ processedText: processedPrompt });
840
+ } catch (error) {
841
+ res.status(500).json({ error: error.message });
842
+ }
843
+ });
844
+
845
+ let serverInstance = null;
846
+ let serverStartingPromise = null;
847
+ let mcpManagerInstance = null;
848
+
849
+ export function getServerAddress() {
850
+ return `http://127.0.0.1:${config.getPort()}`;
851
+ }
852
+
853
+ export function isServerRunning() {
854
+ return Boolean(serverInstance);
855
+ }
856
+
857
+ export async function startServer(options = {}) {
858
+ if (serverInstance) {
859
+ return serverInstance;
860
+ }
861
+ if (serverStartingPromise) {
862
+ return serverStartingPromise;
863
+ }
864
+
865
+ const { configOverrides } = options;
866
+ if (configOverrides) {
867
+ config.applyOverrides(configOverrides);
868
+ }
869
+ promptsDir = config.getPromptsDir();
870
+
871
+ serverStartingPromise = (async () => {
872
+ try {
873
+ await config.ensurePromptsDir();
874
+ promptsDir = config.getPromptsDir();
875
+ await seedPromptsIfEmpty();
876
+ await config.validate();
877
+ config.showConfig();
878
+
879
+ return await new Promise((resolve, reject) => {
880
+ const server = app.listen(config.getPort(), () => {
881
+ logger.info(`服务器已启动,监听端口 ${config.getPort()}`);
882
+ if (config.adminEnable) {
883
+ logger.info(`管理员界面可通过 http://localhost:${config.getPort()}${config.adminPath} 访问`);
884
+ }
885
+ resolve(server);
886
+ });
887
+
888
+ server.on('error', (err) => {
889
+ logger.error('服务器启动失败:', err.message);
890
+ reject(err);
891
+ });
892
+ });
893
+ } catch (error) {
894
+ logger.error('服务器启动失败:', error.message);
895
+ throw error;
896
+ }
897
+ })();
898
+
899
+ try {
900
+ serverInstance = await serverStartingPromise;
901
+
902
+ // 启动MCP服务器
903
+ startMCPServer();
904
+
905
+ return serverInstance;
906
+ } finally {
907
+ serverStartingPromise = null;
908
+ }
909
+ }
910
+
911
+ export async function stopServer() {
912
+ if (serverStartingPromise) {
913
+ try {
914
+ await serverStartingPromise;
915
+ } catch (error) {
916
+ // ignore failing start when stopping
917
+ }
918
+ }
919
+
920
+ if (!serverInstance) {
921
+ return;
922
+ }
923
+
924
+ await new Promise((resolve, reject) => {
925
+ serverInstance.close((err) => {
926
+ if (err) {
927
+ logger.error('停止服务器失败:', err.message);
928
+ reject(err);
929
+ } else {
930
+ logger.info('服务器已停止');
931
+ resolve();
932
+ }
933
+ });
934
+ });
935
+
936
+ serverInstance = null;
937
+
938
+ // 停止MCP服务器
939
+ if (mcpManagerInstance) {
940
+ await mcpManagerInstance.close();
941
+ mcpManagerInstance = null;
942
+ }
943
+ }
944
+
945
+ export function getServerState() {
946
+ return {
947
+ running: Boolean(serverInstance),
948
+ port: config.getPort(),
949
+ address: getServerAddress(),
950
+ adminPath: config.adminPath
951
+ };
952
+ }
953
+
954
+ // 启动MCP服务器
955
+ async function startMCPServer() {
956
+ try {
957
+ // 初始化MCP管理器
958
+ mcpManagerInstance = mcpManager;
959
+ await mcpManagerInstance.initialize();
960
+
961
+ // 添加MCP HTTP端点
962
+ app.post('/mcp', express.json(), async (req, res) => {
963
+ try {
964
+ await mcpManagerInstance.handleHTTPRequest(req, res);
965
+ } catch (error) {
966
+ logger.error('MCP HTTP请求处理失败:', error.message);
967
+ if (!res.headersSent) {
968
+ res.status(500).json({
969
+ jsonrpc: '2.0',
970
+ error: {
971
+ code: -32603,
972
+ message: 'Internal error'
973
+ }
974
+ });
975
+ }
976
+ }
977
+ });
978
+
979
+ logger.info(`MCP服务器已启动 http://localhost:${config.getPort()}/mcp 端点`);
980
+ } catch (error) {
981
+ logger.error('启动MCP服务器失败:', error.message);
982
+ mcpManagerInstance = null;
983
+ }
984
+ }
985
+
986
+ const isDirectRun = (() => {
987
+ try {
988
+ const executed = process.argv[1];
989
+ if (!executed) return false;
990
+ return pathToFileURL(executed).href === import.meta.url;
991
+ } catch (error) {
992
+ return false;
993
+ }
994
+ })();
995
+
996
+ if (isDirectRun) {
997
+ startServer().catch((error) => {
998
+ logger.error('服务器启动失败:', error.message);
999
+ process.exit(1);
1000
+ });
1001
+ }