@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
|
@@ -3,8 +3,9 @@ import path from 'path';
|
|
|
3
3
|
import YAML from 'yaml';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import crypto from 'crypto';
|
|
6
|
-
import { logger } from '
|
|
7
|
-
import { config } from '
|
|
6
|
+
import { logger } from '../utils/logger.js';
|
|
7
|
+
import { config } from '../utils/config.js';
|
|
8
|
+
import { GROUP_META_FILENAME } from '../utils/util.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Prompt数据结构验证schema
|
|
@@ -34,7 +35,7 @@ const PromptSchema = z.object({
|
|
|
34
35
|
/**
|
|
35
36
|
* Prompt管理器类
|
|
36
37
|
*/
|
|
37
|
-
|
|
38
|
+
class PromptManager {
|
|
38
39
|
constructor(promptsDir) {
|
|
39
40
|
this.promptsDir = promptsDir;
|
|
40
41
|
this.loadedPrompts = new Map();
|
|
@@ -92,7 +93,6 @@ export class PromptManager {
|
|
|
92
93
|
* @returns {Object} 组元数据
|
|
93
94
|
*/
|
|
94
95
|
readGroupMeta(dir) {
|
|
95
|
-
const GROUP_META_FILENAME = '.groupmeta.json';
|
|
96
96
|
try {
|
|
97
97
|
const metaPath = path.join(dir, GROUP_META_FILENAME);
|
|
98
98
|
if (!fs.existsSync(metaPath)) {
|
|
@@ -322,7 +322,7 @@ export class PromptManager {
|
|
|
322
322
|
}
|
|
323
323
|
});
|
|
324
324
|
|
|
325
|
-
logger.info(`本地Prompt加载完成: 启用 ${loadedCount} 个, 禁用 ${disabledCount} 个, 失败 ${errorCount}
|
|
325
|
+
logger.info(`本地Prompt加载完成: 启用 ${loadedCount} 个, 禁用 ${disabledCount} 个, 失败 ${errorCount} 个\n`);
|
|
326
326
|
|
|
327
327
|
return {
|
|
328
328
|
success: loadedCount,
|
|
@@ -345,6 +345,11 @@ export class PromptManager {
|
|
|
345
345
|
const filePath = typeof fileInfo === 'string' ? path.join(this.promptsDir, fileInfo) : fileInfo.filePath;
|
|
346
346
|
const relativePath = typeof fileInfo === 'string' ? fileInfo : fileInfo.relativePath;
|
|
347
347
|
|
|
348
|
+
if (fileName === GROUP_META_FILENAME) {
|
|
349
|
+
// 跳过组元数据文件
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
348
353
|
try {
|
|
349
354
|
const content = await fs.readFile(filePath, 'utf8');
|
|
350
355
|
const ext = path.extname(fileName).toLowerCase();
|
|
@@ -471,3 +476,6 @@ export class PromptManager {
|
|
|
471
476
|
return await this.loadPrompts();
|
|
472
477
|
}
|
|
473
478
|
}
|
|
479
|
+
|
|
480
|
+
// 创建全局PromptManager实例
|
|
481
|
+
export const promptManager = new PromptManager(config.getPromptsDir());
|
|
@@ -15,10 +15,10 @@ const DEFAULT_PROMPTS_DIR = path.join(DEFAULT_HOME_DIR, 'prompts');
|
|
|
15
15
|
function parseCommandLineArgs() {
|
|
16
16
|
const args = process.argv.slice(2);
|
|
17
17
|
const options = {};
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
for (let i = 0; i < args.length; i++) {
|
|
20
20
|
const arg = args[i];
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
if (arg === '--prompts-dir' || arg === '-p') {
|
|
23
23
|
options.promptsDir = args[i + 1];
|
|
24
24
|
i++; // 跳过下一个参数
|
|
@@ -35,7 +35,7 @@ function parseCommandLineArgs() {
|
|
|
35
35
|
options[key] = value;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
return options;
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -61,7 +61,7 @@ MCP Prompt Server - 智能 Prompt 管理服务器
|
|
|
61
61
|
PROMPTS_DIR Prompts目录路径
|
|
62
62
|
MCP_SERVER_VERSION 服务器版本
|
|
63
63
|
LOG_LEVEL 日志级别 (默认: info)
|
|
64
|
-
MAX_PROMPTS 最大prompt数量限制 (默认:
|
|
64
|
+
MAX_PROMPTS 最大prompt数量限制 (默认: 1000)
|
|
65
65
|
RECURSIVE_SCAN 是否启用递归扫描子目录 (默认: true)
|
|
66
66
|
|
|
67
67
|
示例:
|
|
@@ -77,34 +77,34 @@ MCP Prompt Server - 智能 Prompt 管理服务器
|
|
|
77
77
|
export class Config {
|
|
78
78
|
constructor() {
|
|
79
79
|
const cliArgs = parseCommandLineArgs();
|
|
80
|
-
|
|
80
|
+
|
|
81
81
|
// 处理帮助和版本信息
|
|
82
82
|
if (cliArgs.help) {
|
|
83
83
|
showHelp();
|
|
84
84
|
process.exit(0);
|
|
85
85
|
}
|
|
86
|
-
|
|
86
|
+
|
|
87
87
|
// 确定prompts目录
|
|
88
|
-
this.promptsDir = cliArgs.promptsDir ||
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
this.promptsDir = cliArgs.promptsDir ||
|
|
89
|
+
process.env.PROMPTS_DIR ||
|
|
90
|
+
DEFAULT_PROMPTS_DIR;
|
|
91
91
|
this.configHome = path.dirname(this.promptsDir);
|
|
92
|
-
|
|
92
|
+
|
|
93
93
|
// 服务器端口
|
|
94
94
|
this.port = cliArgs.port || process.env.SERVER_PORT || 5621;
|
|
95
|
-
|
|
95
|
+
|
|
96
96
|
// 其他配置
|
|
97
97
|
this.serverName = process.env.MCP_SERVER_NAME || 'prompt-manager';
|
|
98
|
-
this.serverVersion = process.env.MCP_SERVER_VERSION || '0.0.
|
|
98
|
+
this.serverVersion = process.env.MCP_SERVER_VERSION || '0.0.18';
|
|
99
99
|
this.logLevel = process.env.LOG_LEVEL || 'info';
|
|
100
|
-
this.maxPrompts = parseInt(process.env.MAX_PROMPTS) ||
|
|
100
|
+
this.maxPrompts = parseInt(process.env.MAX_PROMPTS) || 1000;
|
|
101
101
|
this.recursiveScan = process.env.RECURSIVE_SCAN !== 'false'; // 默认启用递归扫描
|
|
102
|
-
|
|
102
|
+
|
|
103
103
|
// 管理员配置
|
|
104
104
|
this.adminEnable = process.env.ADMIN_ENABLE !== 'false'; // 默认启用管理员功能
|
|
105
105
|
this.adminPath = process.env.ADMIN_PATH || '/admin';
|
|
106
106
|
this.exportToken = process.env.EXPORT_TOKEN || crypto.randomBytes(32).toString('hex');
|
|
107
|
-
|
|
107
|
+
|
|
108
108
|
// 管理员账户(从环境变量或默认值)
|
|
109
109
|
const adminUsername = process.env.ADMIN_USERNAME || 'admin';
|
|
110
110
|
const adminPassword = process.env.ADMIN_PASSWORD || 'admin';
|
|
@@ -116,7 +116,7 @@ export class Config {
|
|
|
116
116
|
token: process.env.ADMIN_TOKEN || crypto.randomBytes(32).toString('hex')
|
|
117
117
|
}
|
|
118
118
|
];
|
|
119
|
-
|
|
119
|
+
|
|
120
120
|
if (cliArgs.version) {
|
|
121
121
|
process.stderr.write(this.serverVersion + '\n');
|
|
122
122
|
process.exit(0);
|
|
@@ -254,10 +254,10 @@ export class Config {
|
|
|
254
254
|
if (!exists) {
|
|
255
255
|
throw new Error(`Prompts目录不存在: ${this.promptsDir}`);
|
|
256
256
|
}
|
|
257
|
-
|
|
257
|
+
|
|
258
258
|
// 检查目录是否可读
|
|
259
259
|
await fs.access(this.promptsDir, fs.constants.R_OK);
|
|
260
|
-
|
|
260
|
+
|
|
261
261
|
return true;
|
|
262
262
|
} catch (error) {
|
|
263
263
|
throw new Error(`配置验证失败: ${error.message}`);
|
|
@@ -268,15 +268,15 @@ export class Config {
|
|
|
268
268
|
* 显示当前配置(输出到 stderr,不干扰 MCP 通信)
|
|
269
269
|
*/
|
|
270
270
|
showConfig() {
|
|
271
|
-
process.stderr.write('
|
|
272
|
-
process.stderr.write(
|
|
273
|
-
process.stderr.write(
|
|
274
|
-
process.stderr.write(
|
|
275
|
-
process.stderr.write(
|
|
276
|
-
process.stderr.write(
|
|
277
|
-
process.stderr.write(`Prompts目录: ${this.promptsDir}\n`);
|
|
278
|
-
process.stderr.write(
|
|
279
|
-
process.stderr.write('
|
|
271
|
+
process.stderr.write('===================================== 服务器配置 =====================================\n');
|
|
272
|
+
process.stderr.write(` 服务名称: ${this.serverName}\n`);
|
|
273
|
+
process.stderr.write(` 服务版本: ${this.serverVersion}\n`);
|
|
274
|
+
process.stderr.write(` 服务端口: ${this.port}\n`);
|
|
275
|
+
process.stderr.write(` 日志级别: ${this.logLevel}\n`);
|
|
276
|
+
process.stderr.write(` 递归扫描: ${this.recursiveScan ? '启用' : '禁用'}\n`);
|
|
277
|
+
process.stderr.write(` Prompts目录: ${this.promptsDir}\n`);
|
|
278
|
+
process.stderr.write(` 最大Prompt数量: ${this.maxPrompts}\n`);
|
|
279
|
+
process.stderr.write('======================================================================================\n\n');
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import fse from 'fs-extra';
|
|
6
|
+
import yaml from 'js-yaml';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { logger } from './logger.js';
|
|
9
|
+
|
|
10
|
+
export const GROUP_META_FILENAME = '.groupmeta.json';
|
|
11
|
+
export const GROUP_NAME_REGEX = /^[a-zA-Z0-9-_\u4e00-\u9fa5]{1,64}$/;
|
|
12
|
+
|
|
13
|
+
// 获取当前文件的目录路径
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
const projectRoot = path.resolve(__dirname, '../../..');
|
|
17
|
+
const examplesPromptsRoot = path.join(projectRoot, 'examples', 'prompts');
|
|
18
|
+
const promptsDir = path.join(os.homedir(), '.prompt-manager', 'prompts');
|
|
19
|
+
|
|
20
|
+
let _promptManager;
|
|
21
|
+
|
|
22
|
+
export class Util {
|
|
23
|
+
/**
|
|
24
|
+
* 检查并初始化prompts目录,如果目录为空则从示例目录复制内容
|
|
25
|
+
* @returns
|
|
26
|
+
*/
|
|
27
|
+
async seedPromptsIfEmpty() {
|
|
28
|
+
try {
|
|
29
|
+
const entries = await fse.readdir(promptsDir);
|
|
30
|
+
if (entries.length > 0) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
logger.warn('读取Prompts目录失败,尝试同步示例数据:', error.message);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const exists = await fse.pathExists(examplesPromptsRoot);
|
|
39
|
+
if (!exists) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
await fse.copy(examplesPromptsRoot, promptsDir, {
|
|
43
|
+
overwrite: false,
|
|
44
|
+
errorOnExist: false,
|
|
45
|
+
recursive: true
|
|
46
|
+
});
|
|
47
|
+
logger.info(`已将示例Prompts同步到 ${promptsDir}`);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
logger.warn('同步示例Prompts失败:', error.message);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 生成文件唯一标识码
|
|
55
|
+
* @param {*} relativePath
|
|
56
|
+
* @returns
|
|
57
|
+
*/
|
|
58
|
+
generateUniqueId(relativePath) {
|
|
59
|
+
const hash = crypto.createHash('sha256');
|
|
60
|
+
hash.update(relativePath);
|
|
61
|
+
const hashHex = hash.digest('hex');
|
|
62
|
+
return hashHex.substring(0, 8);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 从文件中读取提示词
|
|
67
|
+
* @returns
|
|
68
|
+
*/
|
|
69
|
+
getPromptsFromFiles() {
|
|
70
|
+
const prompts = [];
|
|
71
|
+
|
|
72
|
+
const traverseDir = (currentPath, relativeDir = '', inheritedEnabled = true) => {
|
|
73
|
+
let currentEnabled = inheritedEnabled;
|
|
74
|
+
if (relativeDir) {
|
|
75
|
+
const meta = this.readGroupMeta(currentPath);
|
|
76
|
+
currentEnabled = currentEnabled && (meta.enabled !== false);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const entries = fs.readdirSync(currentPath, { withFileTypes: true });
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
const fullPath = path.join(currentPath, entry.name);
|
|
83
|
+
if (entry.isDirectory()) {
|
|
84
|
+
const childRelativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
|
|
85
|
+
traverseDir(fullPath, childRelativePath, currentEnabled);
|
|
86
|
+
} else if (entry.isFile() && entry.name.endsWith('.yaml')) {
|
|
87
|
+
try {
|
|
88
|
+
const fileContent = fs.readFileSync(fullPath, 'utf8');
|
|
89
|
+
const prompt = yaml.load(fileContent);
|
|
90
|
+
if (prompt && prompt.name) {
|
|
91
|
+
const relativePath = path.relative(promptsDir, fullPath);
|
|
92
|
+
const normalizedRelativePath = relativePath.split(path.sep).join('/');
|
|
93
|
+
const relativeDirForFile = path.dirname(normalizedRelativePath);
|
|
94
|
+
const topLevelGroup = relativeDirForFile && relativeDirForFile !== '.' ? relativeDirForFile.split('/')[0] : (prompt.group || 'default');
|
|
95
|
+
const groupPath = relativeDirForFile && relativeDirForFile !== '.' ? relativeDirForFile : topLevelGroup;
|
|
96
|
+
prompts.push({
|
|
97
|
+
...prompt,
|
|
98
|
+
uniqueId: this.generateUniqueId(prompt.name + '.yaml'),
|
|
99
|
+
fileName: entry.name,
|
|
100
|
+
relativePath: normalizedRelativePath,
|
|
101
|
+
group: topLevelGroup,
|
|
102
|
+
groupPath,
|
|
103
|
+
groupEnabled: currentEnabled
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
logger.error(`Error processing file ${fullPath}:`, error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
logger.error(`Error reading directory ${currentPath}:`, error);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
traverseDir(promptsDir);
|
|
117
|
+
return prompts;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 计算搜索关键词与prompt的相似度得分
|
|
122
|
+
* @param {string} searchTerm - 搜索关键词
|
|
123
|
+
* @param {Object} prompt - prompt对象
|
|
124
|
+
* @returns {number} 相似度得分 (0-100)
|
|
125
|
+
*/
|
|
126
|
+
calculateSimilarityScore(searchTerm, prompt) {
|
|
127
|
+
let totalScore = 0;
|
|
128
|
+
const searchLower = searchTerm ? searchTerm.toLowerCase() : '';
|
|
129
|
+
|
|
130
|
+
// 搜索字段权重配置(专注于内容搜索,不包含ID检索)
|
|
131
|
+
const fieldWeights = {
|
|
132
|
+
name: 60, // 名称权重高,是主要匹配字段
|
|
133
|
+
description: 40 // 描述权重适中,是辅助匹配字段
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// 计算name匹配得分
|
|
137
|
+
if (prompt && prompt.name && typeof prompt.name === 'string') {
|
|
138
|
+
const nameScore = util.getStringMatchScore(searchLower, prompt.name.toLowerCase());
|
|
139
|
+
totalScore += nameScore * fieldWeights.name;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 计算description匹配得分
|
|
143
|
+
if (prompt.description) {
|
|
144
|
+
const descScore = util.getStringMatchScore(searchLower, prompt.description.toLowerCase());
|
|
145
|
+
totalScore += descScore * fieldWeights.description;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 标准化得分到0-100范围
|
|
149
|
+
const maxPossibleScore = Object.values(fieldWeights).reduce((sum, weight) => sum + weight, 0);
|
|
150
|
+
return Math.round((totalScore / maxPossibleScore) * 100);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 计算两个字符串的匹配得分
|
|
155
|
+
* @param {string} search - 搜索词 (已转小写)
|
|
156
|
+
* @param {string} target - 目标字符串 (已转小写)
|
|
157
|
+
* @returns {number} 匹配得分 (0-1)
|
|
158
|
+
*/
|
|
159
|
+
getStringMatchScore(search, target) {
|
|
160
|
+
if (!search || !target) return 0;
|
|
161
|
+
|
|
162
|
+
// 完全匹配得分最高
|
|
163
|
+
if (target === search) return 1.0;
|
|
164
|
+
|
|
165
|
+
// 完全包含得分较高
|
|
166
|
+
if (target.includes(search)) return 0.8;
|
|
167
|
+
|
|
168
|
+
// 部分词匹配
|
|
169
|
+
const searchWords = search.split(/\s+/).filter(word => word.length > 0);
|
|
170
|
+
const targetWords = target.split(/\s+/).filter(word => word.length > 0);
|
|
171
|
+
|
|
172
|
+
let matchedWords = 0;
|
|
173
|
+
for (const searchWord of searchWords) {
|
|
174
|
+
for (const targetWord of targetWords) {
|
|
175
|
+
if (targetWord.includes(searchWord) || searchWord.includes(targetWord)) {
|
|
176
|
+
matchedWords++;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (searchWords.length > 0) {
|
|
183
|
+
const wordMatchRatio = matchedWords / searchWords.length;
|
|
184
|
+
return wordMatchRatio * 0.6; // 部分词匹配得分
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 更新目录元数据
|
|
192
|
+
* @param {*} dir
|
|
193
|
+
* @param {*} meta
|
|
194
|
+
*/
|
|
195
|
+
writeGroupMeta(dir, meta = {}) {
|
|
196
|
+
const metaPath = this.getGroupMetaPath(dir);
|
|
197
|
+
const data = {
|
|
198
|
+
enabled: meta.enabled !== false
|
|
199
|
+
};
|
|
200
|
+
fs.writeFileSync(metaPath, JSON.stringify(data, null, 2), 'utf8');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 验证目录名称是否有效
|
|
205
|
+
* @param {*} name
|
|
206
|
+
* @returns
|
|
207
|
+
*/
|
|
208
|
+
isValidGroupName(name) {
|
|
209
|
+
return GROUP_NAME_REGEX.test(name);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 验证目录名称
|
|
214
|
+
* @param {*} relativePath
|
|
215
|
+
* @returns
|
|
216
|
+
*/
|
|
217
|
+
validateGroupPath(relativePath) {
|
|
218
|
+
if (!relativePath || typeof relativePath !== 'string') {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const segments = relativePath.split('/').filter(Boolean);
|
|
222
|
+
if (!segments.length) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
for (const segment of segments) {
|
|
226
|
+
if (!this.isValidGroupName(segment)) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return segments;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
*
|
|
235
|
+
* @param {*} relativePath
|
|
236
|
+
* @returns
|
|
237
|
+
*/
|
|
238
|
+
resolveGroupDir(relativePath) {
|
|
239
|
+
const segments = this.validateGroupPath(relativePath);
|
|
240
|
+
if (!segments) return null;
|
|
241
|
+
const targetPath = path.resolve(promptsDir, ...segments);
|
|
242
|
+
const normalizedPromptsDir = path.resolve(promptsDir);
|
|
243
|
+
if (!targetPath.startsWith(normalizedPromptsDir)) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
return { dir: targetPath, segments };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* 获取目录元数据文件路径
|
|
251
|
+
* @param {*} dir
|
|
252
|
+
* @returns
|
|
253
|
+
*/
|
|
254
|
+
getGroupMetaPath(dir) {
|
|
255
|
+
return path.join(dir, GROUP_META_FILENAME);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 读取目录元数据
|
|
260
|
+
* @param {*} dir
|
|
261
|
+
* @returns
|
|
262
|
+
*/
|
|
263
|
+
readGroupMeta(dir) {
|
|
264
|
+
try {
|
|
265
|
+
const metaPath = this.getGroupMetaPath(dir);
|
|
266
|
+
if (!fs.existsSync(metaPath)) {
|
|
267
|
+
return { enabled: true };
|
|
268
|
+
}
|
|
269
|
+
const raw = fs.readFileSync(metaPath, 'utf8');
|
|
270
|
+
const data = JSON.parse(raw);
|
|
271
|
+
return {
|
|
272
|
+
enabled: data.enabled !== false
|
|
273
|
+
};
|
|
274
|
+
} catch (error) {
|
|
275
|
+
logger.warn('读取类目元数据失败:', error);
|
|
276
|
+
return { enabled: true };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 获取所有分组(直接从目录读取)
|
|
282
|
+
* @param {*} baseDir
|
|
283
|
+
* @param {*} relativePath
|
|
284
|
+
* @returns
|
|
285
|
+
*/
|
|
286
|
+
buildGroupTree(baseDir, relativePath = '') {
|
|
287
|
+
const nodes = [];
|
|
288
|
+
try {
|
|
289
|
+
const entries = fs.readdirSync(baseDir, { withFileTypes: true });
|
|
290
|
+
for (const entry of entries) {
|
|
291
|
+
if (!entry.isDirectory()) continue;
|
|
292
|
+
if (entry.name.startsWith('.')) continue;
|
|
293
|
+
const childRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
294
|
+
const childDir = path.join(baseDir, entry.name);
|
|
295
|
+
const children = this.buildGroupTree(childDir, childRelativePath);
|
|
296
|
+
const meta = this.readGroupMeta(childDir);
|
|
297
|
+
nodes.push({
|
|
298
|
+
name: entry.name,
|
|
299
|
+
path: childRelativePath,
|
|
300
|
+
children,
|
|
301
|
+
enabled: meta.enabled !== false
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
} catch (error) {
|
|
305
|
+
logger.error('读取分组目录失败:', error);
|
|
306
|
+
}
|
|
307
|
+
nodes.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
|
|
308
|
+
return nodes;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async processPromptContent(prompt, args) {
|
|
312
|
+
let content = '';
|
|
313
|
+
|
|
314
|
+
if (prompt.messages && Array.isArray(prompt.messages)) {
|
|
315
|
+
const userMessages = prompt.messages.filter(msg => msg.role === 'user');
|
|
316
|
+
|
|
317
|
+
for (const message of userMessages) {
|
|
318
|
+
if (message.content && typeof message.content.text === 'string') {
|
|
319
|
+
let text = message.content.text;
|
|
320
|
+
|
|
321
|
+
// Replace arguments
|
|
322
|
+
if (args) {
|
|
323
|
+
for (const [key, value] of Object.entries(args)) {
|
|
324
|
+
const placeholder = new RegExp(`{{${key}}}`, 'g');
|
|
325
|
+
text = text.replace(placeholder, String(value));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
content += text + '\n\n';
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return content.trim();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* 获取admin-ui目录的绝对路径
|
|
339
|
+
* @returns {string} admin-ui目录的绝对路径
|
|
340
|
+
*/
|
|
341
|
+
getAdminUiRoot() {
|
|
342
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
343
|
+
const __dirname = path.dirname(path.dirname(__filename));
|
|
344
|
+
return path.join(__dirname, '..', 'admin-ui');
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
async getPromptManager() {
|
|
348
|
+
if (!_promptManager) {
|
|
349
|
+
const serviceModule = await import('../services/manager.js');
|
|
350
|
+
_promptManager = serviceModule.promptManager;
|
|
351
|
+
}
|
|
352
|
+
return _promptManager;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export const util = new Util()
|