@becrafter/prompt-manager 0.0.16 → 0.0.19
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/IFLOW.md +175 -0
- package/README.md +66 -80
- package/app/desktop/assets/icons/icon.icns +0 -0
- package/app/desktop/assets/icons/icon.ico +0 -0
- package/app/desktop/assets/icons/icon_1024x1024.png +0 -0
- package/app/desktop/assets/icons/icon_128x128.png +0 -0
- package/app/desktop/assets/icons/icon_16x16.png +0 -0
- package/app/desktop/assets/icons/icon_24x24.png +0 -0
- package/app/desktop/assets/icons/icon_256x256.png +0 -0
- package/app/desktop/assets/icons/icon_32x32.png +0 -0
- package/app/desktop/assets/icons/icon_48x48.png +0 -0
- package/app/desktop/assets/icons/icon_512x512.png +0 -0
- package/app/desktop/assets/icons/icon_64x64.png +0 -0
- package/app/desktop/assets/icons/icon_96x96.png +0 -0
- package/app/desktop/assets/templates/about.html +147 -0
- package/app/desktop/main.js +178 -460
- package/app/desktop/package-lock.json +1150 -412
- package/app/desktop/package.json +54 -11
- package/app/desktop/preload.js +7 -0
- package/app/desktop/src/core/error-handler.js +108 -0
- package/app/desktop/src/core/event-emitter.js +84 -0
- package/app/desktop/src/core/logger.js +108 -0
- package/app/desktop/src/core/state-manager.js +125 -0
- package/app/desktop/src/services/module-loader.js +193 -0
- package/app/desktop/src/services/runtime-manager.js +152 -0
- package/app/desktop/src/services/service-manager.js +169 -0
- package/app/desktop/src/services/update-manager.js +268 -0
- package/app/desktop/src/ui/about-dialog-manager.js +208 -0
- package/app/desktop/src/ui/tray-manager.js +202 -0
- package/app/desktop/src/utils/icon-manager.js +141 -0
- package/app/desktop/src/utils/path-utils.js +58 -0
- package/app/desktop/src/utils/resource-paths.js +72 -0
- package/app/desktop/src/utils/template-renderer.js +284 -0
- package/app/desktop/src/utils/version-utils.js +59 -0
- package/examples/prompts/engineer/engineer-professional.yaml +92 -0
- package/examples/prompts/engineer/laowang-engineer.yaml +132 -0
- package/examples/prompts/engineer/nekomata-engineer.yaml +123 -0
- package/examples/prompts/engineer/ojousama-engineer.yaml +124 -0
- package/examples/prompts/workflow/sixstep-workflow.yaml +192 -0
- package/package.json +10 -3
- package/packages/admin-ui/admin.html +2 -2
- package/packages/resources/tools/filesystem/filesystem.tool.js +184 -0
- package/packages/resources/tools/index.js +16 -0
- 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/mcp.handler.js +265 -0
- package/packages/server/mcp/mcp.server.js +181 -0
- package/packages/server/mcp/toolx.handler.js +131 -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/scripts/build-icons.js +105 -0
- package/scripts/icns-builder/package.json +12 -0
- package/packages/server/mcp.js +0 -234
- package/packages/server/mcpManager.js +0 -205
- /package/app/desktop/assets/{icon.png → icons/icon.png} +0 -0
- /package/packages/server/{logger.js → utils/logger.js} +0 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 文件系统工具
|
|
6
|
+
* @param {Object} params - 工具参数
|
|
7
|
+
* @param {string} mode - 操作模式
|
|
8
|
+
* @returns {Promise<Object>} 执行结果
|
|
9
|
+
*/
|
|
10
|
+
export default async function filesystem(params, mode = 'execute') {
|
|
11
|
+
// 根据模式执行不同的操作
|
|
12
|
+
switch (mode) {
|
|
13
|
+
case 'manual':
|
|
14
|
+
// 生成 Markdown 格式的手册
|
|
15
|
+
return generateManual();
|
|
16
|
+
|
|
17
|
+
case 'execute':
|
|
18
|
+
// 执行模式 - 实际执行操作
|
|
19
|
+
const { action, path: filePath, content } = params;
|
|
20
|
+
|
|
21
|
+
if (!action) {
|
|
22
|
+
throw new Error('缺少必需参数: action');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!filePath) {
|
|
26
|
+
throw new Error('缺少必需参数: path');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
switch (action) {
|
|
30
|
+
case 'read':
|
|
31
|
+
try {
|
|
32
|
+
const fileContent = await fs.readFile(filePath, 'utf8');
|
|
33
|
+
return {
|
|
34
|
+
success: true,
|
|
35
|
+
action: 'read',
|
|
36
|
+
path: filePath,
|
|
37
|
+
content: fileContent
|
|
38
|
+
};
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw new Error(`读取文件失败: ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
case 'write':
|
|
44
|
+
if (content === undefined) {
|
|
45
|
+
throw new Error('写入文件需要提供 content 参数');
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
49
|
+
return {
|
|
50
|
+
success: true,
|
|
51
|
+
action: 'write',
|
|
52
|
+
path: filePath,
|
|
53
|
+
message: '文件写入成功'
|
|
54
|
+
};
|
|
55
|
+
} catch (error) {
|
|
56
|
+
throw new Error(`写入文件失败: ${error.message}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
case 'list':
|
|
60
|
+
try {
|
|
61
|
+
const items = await fs.readdir(filePath);
|
|
62
|
+
return {
|
|
63
|
+
success: true,
|
|
64
|
+
action: 'list',
|
|
65
|
+
path: filePath,
|
|
66
|
+
items: items
|
|
67
|
+
};
|
|
68
|
+
} catch (error) {
|
|
69
|
+
throw new Error(`列出目录内容失败: ${error.message}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
case 'delete':
|
|
73
|
+
try {
|
|
74
|
+
const stats = await fs.stat(filePath);
|
|
75
|
+
if (stats.isDirectory()) {
|
|
76
|
+
await fs.rm(filePath, { recursive: true });
|
|
77
|
+
return {
|
|
78
|
+
success: true,
|
|
79
|
+
action: 'delete',
|
|
80
|
+
path: filePath,
|
|
81
|
+
type: 'directory',
|
|
82
|
+
message: '目录删除成功'
|
|
83
|
+
};
|
|
84
|
+
} else {
|
|
85
|
+
await fs.unlink(filePath);
|
|
86
|
+
return {
|
|
87
|
+
success: true,
|
|
88
|
+
action: 'delete',
|
|
89
|
+
path: filePath,
|
|
90
|
+
type: 'file',
|
|
91
|
+
message: '文件删除成功'
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw new Error(`删除文件或目录失败: ${error.message}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
default:
|
|
99
|
+
throw new Error(`不支持的操作类型: ${action}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
default:
|
|
103
|
+
throw new Error(`不支持的模式: ${mode}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 生成 Markdown 格式的手册
|
|
109
|
+
* @returns {string} Markdown 格式的手册
|
|
110
|
+
*/
|
|
111
|
+
function generateManual() {
|
|
112
|
+
return `# 🔧 filesystem
|
|
113
|
+
|
|
114
|
+
> 文件系统工具 - 用于文件操作
|
|
115
|
+
|
|
116
|
+
## 📋 基础信息
|
|
117
|
+
|
|
118
|
+
- **标识**: \`tool://filesystem\`
|
|
119
|
+
- **分类**: 系统工具
|
|
120
|
+
|
|
121
|
+
## ✅ 适用场景
|
|
122
|
+
|
|
123
|
+
- 读取文件内容进行分析
|
|
124
|
+
- 写入文件内容保存数据
|
|
125
|
+
- 列出目录内容查看文件结构
|
|
126
|
+
- 删除文件或目录进行清理
|
|
127
|
+
|
|
128
|
+
## 📝 参数定义
|
|
129
|
+
|
|
130
|
+
### execute 模式参数
|
|
131
|
+
|
|
132
|
+
| 参数 | 类型 | 必需 | 描述 | 默认值 |
|
|
133
|
+
|------|------|------|------|--------|
|
|
134
|
+
| action | string (read|write|list|delete) | ✅ | 操作类型 | - |
|
|
135
|
+
| path | string | ✅ | 文件或目录路径 | - |
|
|
136
|
+
| content | string | ❌ | 写入的文件内容 (仅在action为write时需要) | - |
|
|
137
|
+
|
|
138
|
+
## 💻 使用示例
|
|
139
|
+
|
|
140
|
+
通过 toolx 调用,使用 YAML 格式:
|
|
141
|
+
|
|
142
|
+
\`\`\`yaml
|
|
143
|
+
# 读取文件内容
|
|
144
|
+
tool: tool://filesystem
|
|
145
|
+
mode: execute
|
|
146
|
+
parameters:
|
|
147
|
+
action: read
|
|
148
|
+
path: /path/to/file.txt
|
|
149
|
+
|
|
150
|
+
# 写入文件内容
|
|
151
|
+
tool: tool://filesystem
|
|
152
|
+
mode: execute
|
|
153
|
+
parameters:
|
|
154
|
+
action: write
|
|
155
|
+
path: /path/to/file.txt
|
|
156
|
+
content: "Hello, World!"
|
|
157
|
+
|
|
158
|
+
# 列出目录内容
|
|
159
|
+
tool: tool://filesystem
|
|
160
|
+
mode: execute
|
|
161
|
+
parameters:
|
|
162
|
+
action: list
|
|
163
|
+
path: /path/to/directory
|
|
164
|
+
|
|
165
|
+
# 删除文件或目录
|
|
166
|
+
tool: tool://filesystem
|
|
167
|
+
mode: execute
|
|
168
|
+
parameters:
|
|
169
|
+
action: delete
|
|
170
|
+
path: /path/to/file-or-directory
|
|
171
|
+
\`\`\`
|
|
172
|
+
|
|
173
|
+
## 🚨 业务错误
|
|
174
|
+
|
|
175
|
+
| 错误码 | 描述 | 解决方案 | 可重试 |
|
|
176
|
+
|--------|------|----------|--------|
|
|
177
|
+
| MISSING_ACTION | 缺少必需参数: action | 提供 action 参数 | ❌ |
|
|
178
|
+
| MISSING_PATH | 缺少必需参数: path | 提供 path 参数 | ❌ |
|
|
179
|
+
| READ_FAILED | 读取文件失败 | 检查文件路径是否存在且可读 | ✅ |
|
|
180
|
+
| WRITE_FAILED | 写入文件失败 | 检查文件路径是否可写 | ✅ |
|
|
181
|
+
| LIST_FAILED | 列出目录内容失败 | 检查目录路径是否存在且可读 | ✅ |
|
|
182
|
+
| DELETE_FAILED | 删除文件或目录失败 | 检查文件或目录是否存在且可删除 | ✅ |
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToolX 工具系统 - 提供各种服务内部工具的统一调用接口
|
|
3
|
+
*
|
|
4
|
+
* 工具目录结构:
|
|
5
|
+
* - 所有工具都位于 /tools 目录下
|
|
6
|
+
* - 每个工具是一个独立的 JS 文件
|
|
7
|
+
* - 工具导出一个默认函数,接受参数和模式作为输入
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// 导出所有可用的工具
|
|
11
|
+
export { default as filesystem } from './filesystem/filesystem.tool.js';
|
|
12
|
+
|
|
13
|
+
// 工具列表
|
|
14
|
+
export const availableTools = [
|
|
15
|
+
'filesystem'
|
|
16
|
+
];
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 管理后台接口
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import express from 'express';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import fse from 'fs-extra';
|
|
9
|
+
import yaml from 'js-yaml';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import { logger } from '../utils/logger.js';
|
|
12
|
+
import { util, GROUP_META_FILENAME } from '../utils/util.js';
|
|
13
|
+
import { config } from '../utils/config.js';
|
|
14
|
+
import {adminAuthMiddleware} from '../middlewares/auth.middleware.js'
|
|
15
|
+
|
|
16
|
+
const router = express.Router();
|
|
17
|
+
|
|
18
|
+
// 获取prompts目录路径(在启动时可能被覆盖)
|
|
19
|
+
let promptsDir = config.getPromptsDir();
|
|
20
|
+
|
|
21
|
+
// 登录端点
|
|
22
|
+
router.post('/login', (req, res) => {
|
|
23
|
+
// 检查是否启用了管理员功能
|
|
24
|
+
if (!config.adminEnable) {
|
|
25
|
+
return res.status(404).json({ error: 'Admin功能未启用' });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { username, password } = req.body;
|
|
29
|
+
|
|
30
|
+
if (!username || !password) {
|
|
31
|
+
return res.status(400).json({ error: '用户名和密码是必需的' });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 验证凭据
|
|
35
|
+
const admin = config.admins.find(a => a.username === username && a.password === password);
|
|
36
|
+
if (!admin) {
|
|
37
|
+
return res.status(401).json({ error: '无效的用户名或密码' });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
res.json({ token: admin.token });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// 获取分组目录
|
|
44
|
+
router.get('/groups', adminAuthMiddleware, (req, res) => {
|
|
45
|
+
try {
|
|
46
|
+
const tree = util.buildGroupTree(promptsDir);
|
|
47
|
+
const hasDefault = tree.some(node => node.path === 'default');
|
|
48
|
+
if (!hasDefault) {
|
|
49
|
+
tree.unshift({ name: 'default', path: 'default', children: [], enabled: true });
|
|
50
|
+
}
|
|
51
|
+
res.json(tree);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
res.status(500).json({ error: error.message });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// 获取所有提示词(支持搜索、过滤和分组)
|
|
58
|
+
router.get('/prompts', adminAuthMiddleware, (req, res) => {
|
|
59
|
+
try {
|
|
60
|
+
const prompts = util.getPromptsFromFiles();
|
|
61
|
+
|
|
62
|
+
// 处理搜索参数
|
|
63
|
+
const search = req.query.search;
|
|
64
|
+
const enabled = req.query.enabled === 'true';
|
|
65
|
+
const groupPathFilter = req.query.groupPath;
|
|
66
|
+
const group = req.query.group;
|
|
67
|
+
|
|
68
|
+
let filteredPrompts = prompts;
|
|
69
|
+
|
|
70
|
+
// 应用分组过滤
|
|
71
|
+
if (groupPathFilter) {
|
|
72
|
+
filteredPrompts = filteredPrompts.filter(prompt => (prompt.groupPath || prompt.group || 'default') === groupPathFilter);
|
|
73
|
+
} else if (group) {
|
|
74
|
+
filteredPrompts = filteredPrompts.filter(prompt => (prompt.group || 'default') === group);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 应用搜索过滤
|
|
78
|
+
if (search) {
|
|
79
|
+
filteredPrompts = filteredPrompts.filter(prompt =>
|
|
80
|
+
prompt.name.includes(search) ||
|
|
81
|
+
(prompt.description && prompt.description.includes(search))
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 应用启用状态过滤
|
|
86
|
+
if (enabled) {
|
|
87
|
+
filteredPrompts = filteredPrompts.filter(prompt => prompt.enabled);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
filteredPrompts.sort((a, b) => (a.name || '').localeCompare(b.name || '', 'zh-CN'));
|
|
91
|
+
|
|
92
|
+
res.json(filteredPrompts);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
res.status(500).json({ error: error.message });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// 获取单个提示词
|
|
99
|
+
router.get('/prompts/:name', adminAuthMiddleware, (req, res) => {
|
|
100
|
+
try {
|
|
101
|
+
const prompts = util.getPromptsFromFiles();
|
|
102
|
+
const targetPath = req.query.path;
|
|
103
|
+
let prompt;
|
|
104
|
+
if (targetPath) {
|
|
105
|
+
prompt = prompts.find(p => p.relativePath === targetPath);
|
|
106
|
+
}
|
|
107
|
+
if (!prompt) {
|
|
108
|
+
prompt = prompts.find(p => p.name === req.params.name);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!prompt) {
|
|
112
|
+
return res.status(404).json({ error: `Prompt "${req.params.name}" 未找到` });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 读取原始YAML文件内容
|
|
116
|
+
const promptPath = path.join(promptsDir, prompt.relativePath);
|
|
117
|
+
const yamlContent = fs.readFileSync(promptPath, 'utf8');
|
|
118
|
+
|
|
119
|
+
res.json({
|
|
120
|
+
...prompt,
|
|
121
|
+
yaml: yamlContent
|
|
122
|
+
});
|
|
123
|
+
} catch (error) {
|
|
124
|
+
res.status(500).json({ error: error.message });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// 保存提示词
|
|
129
|
+
router.post('/prompts', adminAuthMiddleware, (req, res) => {
|
|
130
|
+
try {
|
|
131
|
+
const { name, group, yaml: yamlContent, relativePath: originalRelativePath } = req.body;
|
|
132
|
+
|
|
133
|
+
if (!name || !yamlContent) {
|
|
134
|
+
return res.status(400).json({ error: '名称和YAML内容是必需的' });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 验证名称格式
|
|
138
|
+
if (!/^[a-zA-Z0-9-_]{1,64}$/.test(name)) {
|
|
139
|
+
return res.status(400).json({ error: '名称格式无效' });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 计算目标路径
|
|
143
|
+
const groupName = group || 'default';
|
|
144
|
+
const normalizedOriginalPath = originalRelativePath ? path.normalize(originalRelativePath).replace(/\\/g, '/') : null;
|
|
145
|
+
|
|
146
|
+
const targetSegments = [];
|
|
147
|
+
if (groupName) {
|
|
148
|
+
targetSegments.push(groupName);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const finalFileName = `${name}.yaml`;
|
|
152
|
+
targetSegments.push(finalFileName);
|
|
153
|
+
|
|
154
|
+
const targetRelativePath = path.posix.join(...targetSegments);
|
|
155
|
+
const targetDir = path.join(promptsDir, path.posix.dirname(targetRelativePath));
|
|
156
|
+
const filePath = path.join(promptsDir, targetRelativePath);
|
|
157
|
+
|
|
158
|
+
fse.ensureDirSync(targetDir);
|
|
159
|
+
|
|
160
|
+
// 检查是否重名(同目录下)
|
|
161
|
+
const prompts = util.getPromptsFromFiles();
|
|
162
|
+
const existingPrompt = prompts.find(p => {
|
|
163
|
+
if (p.name !== name) return false;
|
|
164
|
+
const isOriginalFile = normalizedOriginalPath && p.relativePath === normalizedOriginalPath;
|
|
165
|
+
if (isOriginalFile) return false;
|
|
166
|
+
const sameRelativePath = p.relativePath === targetRelativePath;
|
|
167
|
+
if (sameRelativePath) return false;
|
|
168
|
+
const sameDirectory = path.posix.dirname(p.relativePath || '') === path.posix.dirname(targetRelativePath);
|
|
169
|
+
return sameDirectory;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (existingPrompt) {
|
|
173
|
+
return res.status(400).json({ error: '名称已存在' });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 保存文件
|
|
177
|
+
fs.writeFileSync(filePath, yamlContent, 'utf8');
|
|
178
|
+
|
|
179
|
+
// 如果目标路径与原始路径不同,删除旧文件
|
|
180
|
+
if (normalizedOriginalPath && normalizedOriginalPath !== targetRelativePath) {
|
|
181
|
+
const originalFilePath = path.join(promptsDir, normalizedOriginalPath);
|
|
182
|
+
if (fs.existsSync(originalFilePath)) {
|
|
183
|
+
fs.unlinkSync(originalFilePath);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
res.json({ message: '保存成功', relativePath: targetRelativePath, group: groupName });
|
|
188
|
+
} catch (error) {
|
|
189
|
+
res.status(500).json({ error: error.message });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// 创建新分组目录
|
|
194
|
+
router.post('/groups', adminAuthMiddleware, (req, res) => {
|
|
195
|
+
try {
|
|
196
|
+
const { name } = req.body;
|
|
197
|
+
|
|
198
|
+
if (!name) {
|
|
199
|
+
return res.status(400).json({ error: '分组名称是必需的' });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 验证名称格式
|
|
203
|
+
if (!util.isValidGroupName(name)) {
|
|
204
|
+
return res.status(400).json({ error: '名称格式无效,只能包含字母、数字、中划线、下划线和中文' });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const groupDir = path.join(promptsDir, name);
|
|
208
|
+
|
|
209
|
+
// 检查目录是否已存在
|
|
210
|
+
if (fs.existsSync(groupDir)) {
|
|
211
|
+
return res.status(400).json({ error: '分组已存在' });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 创建目录
|
|
215
|
+
fs.mkdirSync(groupDir, { recursive: true });
|
|
216
|
+
|
|
217
|
+
res.json({ message: '分组创建成功' });
|
|
218
|
+
} catch (error) {
|
|
219
|
+
res.status(500).json({ error: error.message });
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// 重命名分组目录
|
|
224
|
+
router.patch('/groups/rename', adminAuthMiddleware, (req, res) => {
|
|
225
|
+
try {
|
|
226
|
+
const { path: groupPath, newName } = req.body || {};
|
|
227
|
+
if (!groupPath || !newName) {
|
|
228
|
+
return res.status(400).json({ error: '分组路径和新名称是必需的' });
|
|
229
|
+
}
|
|
230
|
+
if (!util.isValidGroupName(newName)) {
|
|
231
|
+
return res.status(400).json({ error: '名称格式无效,只能包含字母、数字、中划线、下划线和中文' });
|
|
232
|
+
}
|
|
233
|
+
if (groupPath === 'default') {
|
|
234
|
+
return res.status(400).json({ error: '默认分组不允许重命名' });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const resolved = util.resolveGroupDir(groupPath);
|
|
238
|
+
if (!resolved) {
|
|
239
|
+
return res.status(400).json({ error: '无效的分组路径' });
|
|
240
|
+
}
|
|
241
|
+
const { dir: oldDir, segments } = resolved;
|
|
242
|
+
if (!fs.existsSync(oldDir) || !fs.lstatSync(oldDir).isDirectory()) {
|
|
243
|
+
return res.status(404).json({ error: '分组不存在' });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const parentSegments = segments.slice(0, -1);
|
|
247
|
+
const oldName = segments[segments.length - 1];
|
|
248
|
+
if (newName === oldName) {
|
|
249
|
+
return res.json({ message: '分组名称未变更', path: groupPath });
|
|
250
|
+
}
|
|
251
|
+
const newSegments = [...parentSegments, newName];
|
|
252
|
+
const newDir = path.resolve(promptsDir, ...newSegments);
|
|
253
|
+
if (fs.existsSync(newDir)) {
|
|
254
|
+
return res.status(400).json({ error: '目标名称已存在,请选择其他名称' });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
fse.moveSync(oldDir, newDir);
|
|
258
|
+
|
|
259
|
+
res.json({ message: '分组重命名成功', path: newSegments.join('/') });
|
|
260
|
+
} catch (error) {
|
|
261
|
+
logger.error('分组重命名失败:', error);
|
|
262
|
+
res.status(500).json({ error: error.message });
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// 设置分组目录状态
|
|
267
|
+
router.patch('/groups/status', adminAuthMiddleware, (req, res) => {
|
|
268
|
+
try {
|
|
269
|
+
const { path: groupPath, enabled } = req.body || {};
|
|
270
|
+
if (typeof enabled !== 'boolean') {
|
|
271
|
+
return res.status(400).json({ error: '状态值无效' });
|
|
272
|
+
}
|
|
273
|
+
if (!groupPath) {
|
|
274
|
+
return res.status(400).json({ error: '分组路径是必需的' });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const resolved = util.resolveGroupDir(groupPath);
|
|
278
|
+
if (!resolved) {
|
|
279
|
+
return res.status(400).json({ error: '无效的分组路径' });
|
|
280
|
+
}
|
|
281
|
+
const { dir } = resolved;
|
|
282
|
+
if (!fs.existsSync(dir) || !fs.lstatSync(dir).isDirectory()) {
|
|
283
|
+
return res.status(404).json({ error: '分组不存在' });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
util.writeGroupMeta(dir, { enabled });
|
|
287
|
+
|
|
288
|
+
res.json({ message: '分组状态已更新', enabled });
|
|
289
|
+
} catch (error) {
|
|
290
|
+
logger.error('更新分组状态失败:', error);
|
|
291
|
+
res.status(500).json({ error: error.message });
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// 删除分组目录
|
|
296
|
+
router.delete('/groups', adminAuthMiddleware, (req, res) => {
|
|
297
|
+
try {
|
|
298
|
+
const groupPath = req.query.path;
|
|
299
|
+
if (!groupPath) {
|
|
300
|
+
return res.status(400).json({ error: '分组路径是必需的' });
|
|
301
|
+
}
|
|
302
|
+
if (groupPath === 'default') {
|
|
303
|
+
return res.status(400).json({ error: '默认分组不允许删除' });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const resolved = util.resolveGroupDir(groupPath);
|
|
307
|
+
if (!resolved) {
|
|
308
|
+
return res.status(400).json({ error: '无效的分组路径' });
|
|
309
|
+
}
|
|
310
|
+
const { dir } = resolved;
|
|
311
|
+
if (!fs.existsSync(dir) || !fs.lstatSync(dir).isDirectory()) {
|
|
312
|
+
return res.status(404).json({ error: '分组不存在' });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
316
|
+
.filter(entry => entry.name !== GROUP_META_FILENAME && !entry.name.startsWith('.'));
|
|
317
|
+
|
|
318
|
+
if (entries.length > 0) {
|
|
319
|
+
return res.status(400).json({ error: '目录非空,请先移除其下的Prompt或子目录' });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
fse.removeSync(dir);
|
|
323
|
+
res.json({ message: '分组删除成功' });
|
|
324
|
+
} catch (error) {
|
|
325
|
+
logger.error('删除分组失败:', error);
|
|
326
|
+
res.status(500).json({ error: error.message });
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// 切换提示词启用状态
|
|
331
|
+
router.post('/prompts/:name/toggle', adminAuthMiddleware, (req, res) => {
|
|
332
|
+
try {
|
|
333
|
+
const prompts = util.getPromptsFromFiles();
|
|
334
|
+
const targetPath = req.query.path;
|
|
335
|
+
let prompt;
|
|
336
|
+
if (targetPath) {
|
|
337
|
+
prompt = prompts.find(p => p.relativePath === targetPath);
|
|
338
|
+
}
|
|
339
|
+
if (!prompt) {
|
|
340
|
+
prompt = prompts.find(p => p.name === req.params.name);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (!prompt) {
|
|
344
|
+
return res.status(404).json({ error: `Prompt "${req.params.name}" 未找到` });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 读取原始YAML文件内容
|
|
348
|
+
const promptPath = path.join(promptsDir, prompt.relativePath);
|
|
349
|
+
const yamlContent = fs.readFileSync(promptPath, 'utf8');
|
|
350
|
+
|
|
351
|
+
// 解析YAML
|
|
352
|
+
const promptData = yaml.load(yamlContent);
|
|
353
|
+
|
|
354
|
+
// 切换启用状态
|
|
355
|
+
promptData.enabled = !promptData.enabled;
|
|
356
|
+
|
|
357
|
+
// 保存更新后的YAML
|
|
358
|
+
const newYamlContent = yaml.dump(promptData);
|
|
359
|
+
fs.writeFileSync(promptPath, newYamlContent, 'utf8');
|
|
360
|
+
|
|
361
|
+
res.json({ message: '状态切换成功', enabled: promptData.enabled });
|
|
362
|
+
} catch (error) {
|
|
363
|
+
res.status(500).json({ error: error.message });
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// 删除提示词(软删)
|
|
368
|
+
router.delete('/prompts/:name', adminAuthMiddleware, (req, res) => {
|
|
369
|
+
try {
|
|
370
|
+
const prompts = util.getPromptsFromFiles();
|
|
371
|
+
const targetPath = req.query.path;
|
|
372
|
+
let prompt;
|
|
373
|
+
if (targetPath) {
|
|
374
|
+
prompt = prompts.find(p => p.relativePath === targetPath);
|
|
375
|
+
}
|
|
376
|
+
if (!prompt) {
|
|
377
|
+
prompt = prompts.find(p => p.name === req.params.name);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (!prompt) {
|
|
381
|
+
return res.status(404).json({ error: `Prompt "${req.params.name}" 未找到` });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// 读取原始文件路径
|
|
385
|
+
const promptPath = path.join(promptsDir, prompt.relativePath);
|
|
386
|
+
|
|
387
|
+
// 直接删除文件
|
|
388
|
+
fse.unlinkSync(promptPath);
|
|
389
|
+
|
|
390
|
+
res.json({ message: '删除成功' });
|
|
391
|
+
} catch (error) {
|
|
392
|
+
res.status(500).json({ error: error.message });
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Markdown预览
|
|
397
|
+
router.post('/md-preview', adminAuthMiddleware, (req, res) => {
|
|
398
|
+
try {
|
|
399
|
+
const { yaml: yamlContent, vars } = req.body;
|
|
400
|
+
|
|
401
|
+
if (!yamlContent) {
|
|
402
|
+
return res.status(400).json({ error: 'YAML内容是必需的' });
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// 解析YAML
|
|
406
|
+
const promptData = yaml.load(yamlContent);
|
|
407
|
+
|
|
408
|
+
// 处理变量替换
|
|
409
|
+
let content = '';
|
|
410
|
+
if (promptData.messages && Array.isArray(promptData.messages)) {
|
|
411
|
+
const userMessages = promptData.messages.filter(msg => msg.role === 'user');
|
|
412
|
+
|
|
413
|
+
for (const message of userMessages) {
|
|
414
|
+
if (message.content && typeof message.content.text === 'string') {
|
|
415
|
+
let text = message.content.text;
|
|
416
|
+
|
|
417
|
+
// 替换变量
|
|
418
|
+
if (vars) {
|
|
419
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
420
|
+
const placeholder = new RegExp(`{{${key}}}`, 'g');
|
|
421
|
+
text = text.replace(placeholder, String(value));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
content += text + '\n\n';
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 简单的Markdown转HTML(实际应用中可以使用专门的库)
|
|
431
|
+
const html = content
|
|
432
|
+
.replace(/&/g, '&')
|
|
433
|
+
.replace(/</g, '<')
|
|
434
|
+
.replace(/>/g, '>')
|
|
435
|
+
.replace(/\n/g, '<br>')
|
|
436
|
+
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
437
|
+
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
438
|
+
.replace(/`(.*?)`/g, '<code>$1</code>')
|
|
439
|
+
.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>')
|
|
440
|
+
.replace(/### (.*?)(<br>|$)/g, '<h3>$1</h3>')
|
|
441
|
+
.replace(/## (.*?)(<br>|$)/g, '<h2>$1</h2>')
|
|
442
|
+
.replace(/# (.*?)(<br>|$)/g, '<h1>$1</h1>');
|
|
443
|
+
|
|
444
|
+
res.json({ html });
|
|
445
|
+
} catch (error) {
|
|
446
|
+
res.status(500).json({ error: error.message });
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
export const adminRouter = router;
|