@becrafter/prompt-manager 0.2.2 → 0.2.4-dev.5
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/package.json +15 -4
- package/packages/server/api/admin.routes.js +385 -0
- package/packages/server/mcp/prompt.handler.js +6 -6
- package/packages/server/server.js +13 -0
- package/packages/server/services/TerminalService.js +37 -17
- package/packages/server/services/skill-sync.service.js +223 -0
- package/packages/server/services/skills.service.js +731 -0
- package/packages/server/utils/config.js +8 -0
- package/packages/server/utils/util.js +27 -21
- package/packages/web/0.26974981f2e663e001ca.js +1 -0
- package/packages/web/112.895e73058c36df517412.js +1 -0
- package/packages/web/130.2145d293d97ee88902a1.js +1 -0
- package/packages/web/142.4ef07b4323c04c87bd81.js +1 -0
- package/packages/web/165.3b013a0e62121061ce41.js +2 -0
- package/packages/web/165.3b013a0e62121061ce41.js.LICENSE.txt +9 -0
- package/packages/web/203.a3697c577f74b6538257.js +1 -0
- package/packages/web/241.c0b98c14268a1d46d55d.js +1 -0
- package/packages/web/249.e10b6b8a1ac4f66185ea.js +1 -0
- package/packages/web/291.8c281d2823aa37833137.js +1 -0
- package/packages/web/319.5b4ef8bdd727fdac0520.js +1 -0
- package/packages/web/32.22d5433bcd9b6b4e98f1.js +1 -0
- package/packages/web/325.8810a3f94093d0f19341.js +1 -0
- package/packages/web/366.24666676c36ca63de8e1.js +1 -0
- package/packages/web/378.07aa22b5f489f8f1ec36.js +1 -0
- package/packages/web/391.f39db51c15feb65b46cb.js +1 -0
- package/packages/web/393.ab70195ea156eebec40b.js +1 -0
- package/packages/web/412.6d38dd3b290782c51ed6.js +1 -0
- package/packages/web/465.fb5ab26f362d3eb53da9.js +1 -0
- package/packages/web/48.7643e76aedee153e680c.js +1 -0
- package/packages/web/480.237737e805b568087d34.js +1 -0
- package/packages/web/489.182e98261f30e4da62d6.js +1 -0
- package/packages/web/490.3f311f0c36d0c52f8b52.js +1 -0
- package/packages/web/492.d07d9857e3682710f3a5.js +1 -0
- package/packages/web/495.60324846746eea7d588b.js +1 -0
- package/packages/web/510.b6e2c433d2f9bc9a1edd.js +1 -0
- package/packages/web/543.223a1c0b8345b4e65ed3.js +1 -0
- package/packages/web/567.e438fd025d21373bc95b.js +1 -0
- package/packages/web/592.c670d3500195bdb18ca4.js +1 -0
- package/packages/web/616.a8b495ba380ec0bcafd9.js +1 -0
- package/packages/web/617.bc4db899b54108068892.js +1 -0
- package/packages/web/641.1e5e900d36819777d808.js +1 -0
- package/packages/web/672.ae0e48b24fd6863bb4d1.js +1 -0
- package/packages/web/731.595770c0a8f30f8c453d.js +1 -0
- package/packages/web/746.3faf506c16dc0e6ae25a.js +1 -0
- package/packages/web/756.16d54ad97aadec6eb440.js +1 -0
- package/packages/web/77.d3de1da7532138022a7c.js +1 -0
- package/packages/web/802.0a9a7f92703bd3ed0228.js +1 -0
- package/packages/web/815.879527c51deda81ff3e6.js +1 -0
- package/packages/web/821.12f33b53b488b3995bb9.js +1 -0
- package/packages/web/846.64e9bb4b76cae3245c47.js +1 -0
- package/packages/web/869.aadd9a07fd56e04ba5dd.js +1 -0
- package/packages/web/885.6dc8edf4c16f6b578da6.js +1 -0
- package/packages/web/901.45604b599d11f86ae9d8.js +1 -0
- package/packages/web/928.69835904eae42113a3ce.js +1 -0
- package/packages/web/955.827020bdb97be41eb055.js +1 -0
- package/packages/web/981.e4d081b9d1dac0cd275f.js +1 -0
- package/packages/web/992.a3843959b85c7fdc6cf9.js +1 -0
- package/packages/web/996.07a82c3066973c75984b.js +1 -0
- package/packages/web/css/codemirror-theme_xq-light.css +43 -0
- package/packages/web/css/codemirror.css +344 -0
- package/packages/web/css/main.b013c98c7a56511f8710.css +9995 -0
- package/packages/web/css/main.css +4404 -0
- package/packages/web/css/skills.css +1753 -0
- package/packages/web/css/terminal.css +1218 -0
- package/packages/web/index.html +3 -0
- package/packages/web/main.cf6dd52dbaa4f935d45a.js +2 -0
- package/packages/web/main.cf6dd52dbaa4f935d45a.js.LICENSE.txt +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@becrafter/prompt-manager",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4-dev.5",
|
|
4
4
|
"description": "Remote MCP Server for managing prompts",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"fix:pty": "npm rebuild node-pty",
|
|
27
27
|
"help": "cd packages/server && node server.js --help",
|
|
28
28
|
"build": "npm run build:admin-ui && npm run build:core",
|
|
29
|
-
"build:core": "cd packages/server &&
|
|
30
|
-
"build:admin-ui": "cd packages/admin-ui &&
|
|
29
|
+
"build:core": "cd packages/server && npm run build",
|
|
30
|
+
"build:admin-ui": "cd packages/admin-ui && npm run build",
|
|
31
31
|
"build:desktop": "bash scripts/build.sh",
|
|
32
32
|
"build:desktop:all": "bash scripts/build.sh build:all",
|
|
33
33
|
"build:icons": "node ./scripts/build-icons.js",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"clean:build": "bash scripts/clean-environment.sh build-only",
|
|
61
61
|
"clean:reinstall": "npm run clean && npm install",
|
|
62
62
|
"test:build": "bash scripts/build.sh test-build",
|
|
63
|
+
"test:npm-package": "node scripts/test-npm-package.js",
|
|
63
64
|
"postinstall": "npm rebuild node-pty && chmod +x node_modules/node-pty/prebuilds/*/pty.node node_modules/node-pty/prebuilds/*/spawn-helper 2>/dev/null || true"
|
|
64
65
|
},
|
|
65
66
|
"keywords": [
|
|
@@ -88,7 +89,8 @@
|
|
|
88
89
|
"node-fetch": "^3.3.2",
|
|
89
90
|
"node-pty": "^1.0.0",
|
|
90
91
|
"sharp": "^0.34.4",
|
|
91
|
-
"
|
|
92
|
+
"sync-hub": "^0.0.3",
|
|
93
|
+
"tar": "^7.5.4",
|
|
92
94
|
"to-ico": "^1.0.1",
|
|
93
95
|
"ws": "^8.18.0",
|
|
94
96
|
"yaml": "^2.4.1",
|
|
@@ -121,5 +123,14 @@
|
|
|
121
123
|
},
|
|
122
124
|
"devDependencies": {
|
|
123
125
|
"@electron/rebuild": "^4.0.2"
|
|
126
|
+
},
|
|
127
|
+
"overrides": {
|
|
128
|
+
"form-data": "^4.0.1",
|
|
129
|
+
"lodash": "^4.17.21",
|
|
130
|
+
"minimist": "^1.2.8",
|
|
131
|
+
"qs": "^6.14.1",
|
|
132
|
+
"tough-cookie": "^4.1.4",
|
|
133
|
+
"jpeg-js": "^0.4.4",
|
|
134
|
+
"url-regex": "^5.0.0"
|
|
124
135
|
}
|
|
125
136
|
}
|
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import express from 'express';
|
|
6
6
|
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
7
8
|
import fs from 'fs';
|
|
8
9
|
import fse from 'fs-extra';
|
|
9
10
|
import yaml from 'js-yaml';
|
|
11
|
+
import multer from 'multer';
|
|
12
|
+
import AdmZip from 'adm-zip';
|
|
10
13
|
import { spawn } from 'child_process';
|
|
11
14
|
import { logger } from '../utils/logger.js';
|
|
12
15
|
import { util, GROUP_META_FILENAME } from '../utils/util.js';
|
|
@@ -14,14 +17,99 @@ import { config } from '../utils/config.js';
|
|
|
14
17
|
import { adminAuthMiddleware } from '../middlewares/auth.middleware.js';
|
|
15
18
|
import { templateManager } from '../services/template.service.js';
|
|
16
19
|
import { modelManager } from '../services/model.service.js';
|
|
20
|
+
import { skillsManager } from '../services/skills.service.js';
|
|
17
21
|
import { optimizationService } from '../services/optimization.service.js';
|
|
18
22
|
import { webSocketService } from '../services/WebSocketService.js';
|
|
23
|
+
import { skillSyncService } from '../services/skill-sync.service.js';
|
|
19
24
|
|
|
20
25
|
const router = express.Router();
|
|
21
26
|
|
|
22
27
|
// 获取prompts目录路径(在启动时可能被覆盖)
|
|
23
28
|
const promptsDir = config.getPromptsDir();
|
|
24
29
|
const PROMPT_NAME_REGEX = /^(?![.]{1,2}$)[^\\/:*?"<>|\r\n]{1,64}$/;
|
|
30
|
+
const SKILL_NAME_REGEX = /^(?![.]{1,2}$)[^\\/:*?"<>|\r\n]{1,64}$/;
|
|
31
|
+
const SKILL_MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
32
|
+
const SKILL_MAX_FILES_COUNT = 50;
|
|
33
|
+
const SKILL_MAX_TOTAL_SIZE = 100 * 1024 * 1024;
|
|
34
|
+
|
|
35
|
+
const skillsUpload = multer({
|
|
36
|
+
storage: multer.diskStorage({
|
|
37
|
+
destination: (req, file, cb) => {
|
|
38
|
+
cb(null, os.tmpdir());
|
|
39
|
+
},
|
|
40
|
+
filename: (req, file, cb) => {
|
|
41
|
+
const timestamp = Date.now();
|
|
42
|
+
const safeName = file.originalname.replace(/[\\/:*?"<>|\s]+/g, '-');
|
|
43
|
+
cb(null, `skill-upload-${timestamp}-${safeName}`);
|
|
44
|
+
}
|
|
45
|
+
}),
|
|
46
|
+
limits: { fileSize: SKILL_MAX_FILE_SIZE },
|
|
47
|
+
fileFilter: (req, file, cb) => {
|
|
48
|
+
if (path.extname(file.originalname).toLowerCase() !== '.zip') {
|
|
49
|
+
return cb(new Error('仅支持上传 .zip 格式的技能包'));
|
|
50
|
+
}
|
|
51
|
+
cb(null, true);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function parseSkillFrontmatter(content) {
|
|
56
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
57
|
+
if (!frontmatterMatch) {
|
|
58
|
+
throw new Error('SKILL.md 必须包含 YAML 前置部分(以 --- 包裹)');
|
|
59
|
+
}
|
|
60
|
+
const frontmatterYaml = frontmatterMatch[1];
|
|
61
|
+
let frontmatter;
|
|
62
|
+
try {
|
|
63
|
+
frontmatter = yaml.load(frontmatterYaml);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new Error(`YAML 前置解析失败: ${error.message}`);
|
|
66
|
+
}
|
|
67
|
+
return frontmatter;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function shouldIgnoreUploadEntry(entryPath) {
|
|
71
|
+
const baseName = path.basename(entryPath);
|
|
72
|
+
if (baseName === '__MACOSX' || baseName === '.DS_Store') {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return entryPath.includes(`${path.sep}__MACOSX${path.sep}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function collectSkillFiles(rootDir) {
|
|
79
|
+
const files = [];
|
|
80
|
+
let totalSize = 0;
|
|
81
|
+
|
|
82
|
+
const walk = async currentDir => {
|
|
83
|
+
const entries = await fse.readdir(currentDir, { withFileTypes: true });
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
86
|
+
if (shouldIgnoreUploadEntry(fullPath)) continue;
|
|
87
|
+
|
|
88
|
+
if (entry.isDirectory()) {
|
|
89
|
+
await walk(fullPath);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const stats = await fse.stat(fullPath);
|
|
94
|
+
if (stats.size > SKILL_MAX_FILE_SIZE) {
|
|
95
|
+
throw new Error(`文件 "${entry.name}" 大小超过限制 (最大 10MB)`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
totalSize += stats.size;
|
|
99
|
+
if (totalSize > SKILL_MAX_TOTAL_SIZE) {
|
|
100
|
+
throw new Error('技能总大小超过限制 (最大 100MB)');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
files.push(fullPath);
|
|
104
|
+
if (files.length > SKILL_MAX_FILES_COUNT) {
|
|
105
|
+
throw new Error(`文件数量超过限制 (最多 ${SKILL_MAX_FILES_COUNT} 个)`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
await walk(rootDir);
|
|
111
|
+
return files;
|
|
112
|
+
}
|
|
25
113
|
|
|
26
114
|
// 获取服务器配置端点
|
|
27
115
|
router.get('/config', (req, res) => {
|
|
@@ -909,4 +997,301 @@ router.put('/optimization/config', adminAuthMiddleware, (req, res) => {
|
|
|
909
997
|
}
|
|
910
998
|
});
|
|
911
999
|
|
|
1000
|
+
// ==================== 技能管理路由 ====================
|
|
1001
|
+
|
|
1002
|
+
// 获取所有技能
|
|
1003
|
+
router.get('/skills', adminAuthMiddleware, (req, res) => {
|
|
1004
|
+
try {
|
|
1005
|
+
const { search, type } = req.query;
|
|
1006
|
+
let skills = skillsManager.getSkillsSummary();
|
|
1007
|
+
|
|
1008
|
+
// 应用搜索过滤
|
|
1009
|
+
if (search) {
|
|
1010
|
+
const searchLower = search.toLowerCase();
|
|
1011
|
+
skills = skills.filter(
|
|
1012
|
+
skill => skill.name.toLowerCase().includes(searchLower) || skill.description.toLowerCase().includes(searchLower)
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// 应用类型过滤
|
|
1017
|
+
if (type) {
|
|
1018
|
+
skills = skills.filter(skill => skill.type === type);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// 按名称排序
|
|
1022
|
+
skills.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
|
|
1023
|
+
|
|
1024
|
+
res.json(skills);
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
logger.error('获取技能列表失败:', error);
|
|
1027
|
+
res.status(500).json({ error: error.message });
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
// 重新加载技能
|
|
1032
|
+
router.post('/skills/reload', adminAuthMiddleware, async (req, res) => {
|
|
1033
|
+
try {
|
|
1034
|
+
const result = await skillsManager.reloadSkills();
|
|
1035
|
+
res.json({ message: '技能加载成功', ...result });
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
logger.error('重新加载技能失败:', error);
|
|
1038
|
+
res.status(500).json({ error: error.message });
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
// 上传技能包
|
|
1043
|
+
router.post('/skills/upload', adminAuthMiddleware, skillsUpload.single('file'), async (req, res) => {
|
|
1044
|
+
const cleanupPaths = [];
|
|
1045
|
+
try {
|
|
1046
|
+
if (!req.file) {
|
|
1047
|
+
return res.status(400).json({ error: '未接收到上传文件' });
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// 创建解压目录
|
|
1051
|
+
const extractDir = await fse.mkdtemp(path.join(os.tmpdir(), 'skill-upload-'));
|
|
1052
|
+
cleanupPaths.push(extractDir);
|
|
1053
|
+
cleanupPaths.push(req.file.path);
|
|
1054
|
+
|
|
1055
|
+
// 解压 ZIP
|
|
1056
|
+
const zip = new AdmZip(req.file.path);
|
|
1057
|
+
zip.extractAllTo(extractDir, true);
|
|
1058
|
+
|
|
1059
|
+
// 检测根目录结构
|
|
1060
|
+
const entries = await fse.readdir(extractDir, { withFileTypes: true });
|
|
1061
|
+
const validEntries = entries.filter(entry => !shouldIgnoreUploadEntry(entry.name));
|
|
1062
|
+
|
|
1063
|
+
let skillRootDir = extractDir;
|
|
1064
|
+
let skillName = null;
|
|
1065
|
+
let frontmatter = null;
|
|
1066
|
+
|
|
1067
|
+
if (validEntries.length === 1 && validEntries[0].isDirectory()) {
|
|
1068
|
+
// 格式2:包含根目录
|
|
1069
|
+
skillRootDir = path.join(extractDir, validEntries[0].name);
|
|
1070
|
+
skillName = validEntries[0].name;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const skillMdPath = path.join(skillRootDir, 'SKILL.md');
|
|
1074
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
1075
|
+
return res.status(400).json({ error: '技能包缺少 SKILL.md 文件' });
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
const skillMdContent = await fse.readFile(skillMdPath, 'utf8');
|
|
1079
|
+
frontmatter = parseSkillFrontmatter(skillMdContent);
|
|
1080
|
+
skillsManager.validateSkillFrontmatter(frontmatter);
|
|
1081
|
+
|
|
1082
|
+
if (!skillName) {
|
|
1083
|
+
// 格式1:从 SKILL.md 提取名称
|
|
1084
|
+
skillName = frontmatter?.name;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (!skillName) {
|
|
1088
|
+
return res.status(400).json({ error: '无法解析技能名称' });
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
if (!SKILL_NAME_REGEX.test(skillName)) {
|
|
1092
|
+
return res.status(400).json({ error: '技能名称格式无效' });
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// 校验文件数量和大小
|
|
1096
|
+
await collectSkillFiles(skillRootDir);
|
|
1097
|
+
|
|
1098
|
+
const targetDir = path.join(config.getSkillsDir(), skillName);
|
|
1099
|
+
const overwrite = req.body?.overwrite === 'true';
|
|
1100
|
+
|
|
1101
|
+
if (fs.existsSync(targetDir)) {
|
|
1102
|
+
if (!overwrite) {
|
|
1103
|
+
return res.status(409).json({
|
|
1104
|
+
error: '技能已存在',
|
|
1105
|
+
canOverwrite: true,
|
|
1106
|
+
skillName
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
await fse.remove(targetDir);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
await fse.copy(skillRootDir, targetDir, {
|
|
1113
|
+
filter: src => !shouldIgnoreUploadEntry(src)
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
await skillsManager.reloadSkills();
|
|
1117
|
+
|
|
1118
|
+
res.json({
|
|
1119
|
+
success: true,
|
|
1120
|
+
skillName
|
|
1121
|
+
});
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
logger.error('上传技能包失败:', error);
|
|
1124
|
+
res.status(400).json({ error: error.message || '上传失败' });
|
|
1125
|
+
} finally {
|
|
1126
|
+
await Promise.all(
|
|
1127
|
+
cleanupPaths.map(async filePath => {
|
|
1128
|
+
try {
|
|
1129
|
+
await fse.remove(filePath);
|
|
1130
|
+
} catch (cleanupError) {
|
|
1131
|
+
logger.warn('清理临时文件失败:', cleanupError.message);
|
|
1132
|
+
}
|
|
1133
|
+
})
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
|
|
1138
|
+
// 获取单个技能
|
|
1139
|
+
router.get('/skills/:id', adminAuthMiddleware, (req, res) => {
|
|
1140
|
+
try {
|
|
1141
|
+
const skill = skillsManager.getSkill(req.params.id);
|
|
1142
|
+
|
|
1143
|
+
if (!skill) {
|
|
1144
|
+
return res.status(404).json({ error: `技能 "${req.params.id}" 未找到` });
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// 读取原始文件内容
|
|
1148
|
+
const rawContent = fs.readFileSync(skill.filePath, 'utf8');
|
|
1149
|
+
|
|
1150
|
+
res.json({
|
|
1151
|
+
...skill,
|
|
1152
|
+
rawContent
|
|
1153
|
+
});
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
logger.error('获取技能失败:', error);
|
|
1156
|
+
res.status(500).json({ error: error.message });
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
// 创建技能
|
|
1161
|
+
router.post('/skills', adminAuthMiddleware, async (req, res) => {
|
|
1162
|
+
try {
|
|
1163
|
+
const { name, frontmatter, markdown, files } = req.body;
|
|
1164
|
+
|
|
1165
|
+
if (!name || !frontmatter) {
|
|
1166
|
+
return res.status(400).json({ error: '名称和前置元数据是必需的' });
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
const skill = await skillsManager.createSkill({ name, frontmatter, markdown, files });
|
|
1170
|
+
res.json(skill);
|
|
1171
|
+
} catch (error) {
|
|
1172
|
+
logger.error('创建技能失败:', error);
|
|
1173
|
+
res.status(400).json({ error: error.message });
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
// 更新技能
|
|
1178
|
+
router.put('/skills/:id', adminAuthMiddleware, async (req, res) => {
|
|
1179
|
+
try {
|
|
1180
|
+
const { frontmatter, markdown, files } = req.body;
|
|
1181
|
+
const skill = await skillsManager.updateSkill(req.params.id, { frontmatter, markdown, files });
|
|
1182
|
+
res.json(skill);
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
if (error.message.includes('不存在') || error.message.includes('不能修改')) {
|
|
1185
|
+
return res.status(404).json({ error: error.message });
|
|
1186
|
+
}
|
|
1187
|
+
logger.error('更新技能失败:', error);
|
|
1188
|
+
res.status(400).json({ error: error.message });
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
// 删除技能
|
|
1193
|
+
router.delete('/skills/:id', adminAuthMiddleware, async (req, res) => {
|
|
1194
|
+
try {
|
|
1195
|
+
await skillsManager.deleteSkill(req.params.id);
|
|
1196
|
+
res.json({ message: '技能删除成功' });
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
if (error.message.includes('不存在')) {
|
|
1199
|
+
return res.status(404).json({ error: error.message });
|
|
1200
|
+
}
|
|
1201
|
+
logger.error('删除技能失败:', error);
|
|
1202
|
+
res.status(400).json({ error: error.message });
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
// 复制技能
|
|
1207
|
+
router.post('/skills/:id/duplicate', adminAuthMiddleware, async (req, res) => {
|
|
1208
|
+
try {
|
|
1209
|
+
const { newName } = req.body;
|
|
1210
|
+
if (!newName) {
|
|
1211
|
+
return res.status(400).json({ error: '新名称是必需的' });
|
|
1212
|
+
}
|
|
1213
|
+
const skill = await skillsManager.duplicateSkill(req.params.id, newName);
|
|
1214
|
+
res.json(skill);
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
if (error.message.includes('不存在')) {
|
|
1217
|
+
return res.status(404).json({ error: error.message });
|
|
1218
|
+
}
|
|
1219
|
+
logger.error('复制技能失败:', error);
|
|
1220
|
+
res.status(400).json({ error: error.message });
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
// 验证技能格式
|
|
1225
|
+
router.post('/skills/validate', adminAuthMiddleware, (req, res) => {
|
|
1226
|
+
try {
|
|
1227
|
+
const { content } = req.body;
|
|
1228
|
+
|
|
1229
|
+
if (!content) {
|
|
1230
|
+
return res.status(400).json({ error: '技能内容是必需的' });
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
const skill = skillsManager.parseSkillContent(content);
|
|
1234
|
+
res.json({ valid: true, skill });
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
res.status(400).json({
|
|
1237
|
+
valid: false,
|
|
1238
|
+
error: error.message,
|
|
1239
|
+
suggestion: '确保 SKILL.md 包含正确的 YAML 前置部分(以 --- 包裹)和 Markdown 内容'
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
// 导出技能
|
|
1245
|
+
router.get('/skills/:id/export', adminAuthMiddleware, async (req, res) => {
|
|
1246
|
+
try {
|
|
1247
|
+
const { buffer, fileName } = await skillsManager.exportSkill(req.params.id);
|
|
1248
|
+
|
|
1249
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
1250
|
+
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}"`);
|
|
1251
|
+
res.send(buffer);
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
if (error.message.includes('不存在')) {
|
|
1254
|
+
return res.status(404).json({ error: error.message });
|
|
1255
|
+
}
|
|
1256
|
+
logger.error('导出技能失败:', error);
|
|
1257
|
+
res.status(500).json({ error: error.message });
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
// ==================== 技能同步路由 ====================
|
|
1262
|
+
|
|
1263
|
+
// 获取同步配置
|
|
1264
|
+
router.get('/skill-sync/config', adminAuthMiddleware, (req, res) => {
|
|
1265
|
+
try {
|
|
1266
|
+
const config = skillSyncService.getConfig();
|
|
1267
|
+
res.json(config);
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
logger.error('获取技能同步配置失败:', error);
|
|
1270
|
+
res.status(500).json({ error: error.message });
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
// 更新同步配置
|
|
1275
|
+
router.post('/skill-sync/config', adminAuthMiddleware, async (req, res) => {
|
|
1276
|
+
try {
|
|
1277
|
+
const newConfig = req.body;
|
|
1278
|
+
await skillSyncService.updateConfig(newConfig);
|
|
1279
|
+
res.json({ message: '同步配置已更新', config: skillSyncService.getConfig() });
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
logger.error('更新技能同步配置失败:', error);
|
|
1282
|
+
res.status(500).json({ error: error.message });
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
// 手动执行同步
|
|
1287
|
+
router.post('/skill-sync/run', adminAuthMiddleware, async (req, res) => {
|
|
1288
|
+
try {
|
|
1289
|
+
const results = await skillSyncService.runSync();
|
|
1290
|
+
res.json({ message: '同步已执行', results });
|
|
1291
|
+
} catch (error) {
|
|
1292
|
+
logger.error('手动执行技能同步失败:', error);
|
|
1293
|
+
res.status(500).json({ error: error.message });
|
|
1294
|
+
}
|
|
1295
|
+
});
|
|
1296
|
+
|
|
912
1297
|
export const adminRouter = router;
|
|
@@ -133,8 +133,8 @@ export async function handleReloadPrompts(_args) {
|
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
135
|
* 格式化搜索结果
|
|
136
|
-
* @param {
|
|
137
|
-
* @returns
|
|
136
|
+
* @param {Array} results
|
|
137
|
+
* @returns {Array}
|
|
138
138
|
*/
|
|
139
139
|
function formatResults(results = []) {
|
|
140
140
|
if (!Array.isArray(results)) return [];
|
|
@@ -161,8 +161,8 @@ function formatResults(results = []) {
|
|
|
161
161
|
|
|
162
162
|
/**
|
|
163
163
|
* 处理列表格式输出
|
|
164
|
-
* @param {
|
|
165
|
-
* @returns
|
|
164
|
+
* @param {Object} result
|
|
165
|
+
* @returns {string}
|
|
166
166
|
*/
|
|
167
167
|
function formatListOutput(result) {
|
|
168
168
|
// 生成当前时间戳
|
|
@@ -244,9 +244,9 @@ function formatDetailOutput(result) {
|
|
|
244
244
|
|
|
245
245
|
/**
|
|
246
246
|
* 将对象转换为格式化的text类型输出
|
|
247
|
-
* @param {
|
|
247
|
+
* @param {Object} result
|
|
248
248
|
* @param {string} format - 输出格式类型: 'list' 或 'detail'
|
|
249
|
-
* @returns
|
|
249
|
+
* @returns {Object}
|
|
250
250
|
*/
|
|
251
251
|
function convertToText(result, format) {
|
|
252
252
|
let ret = '';
|
|
@@ -7,6 +7,7 @@ import { syncSystemTools } from './toolm/tool-sync.service.js';
|
|
|
7
7
|
import { syncAuthorConfig } from './toolm/author-sync.service.js';
|
|
8
8
|
import { startLogCleanupTask } from './toolm/tool-logger.service.js';
|
|
9
9
|
import { webSocketService } from './services/WebSocketService.js';
|
|
10
|
+
import { skillSyncService } from './services/skill-sync.service.js';
|
|
10
11
|
import { checkPortAvailable } from './utils/port-checker.js';
|
|
11
12
|
|
|
12
13
|
// 动态导入 promptManager,以处理 Electron 打包后的路径问题
|
|
@@ -122,6 +123,18 @@ export async function startServer(options = {}) {
|
|
|
122
123
|
logger.warn('加载优化模型失败,继续启动服务', { error: error.message });
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
// 加载技能
|
|
127
|
+
try {
|
|
128
|
+
const { skillsManager } = await import('./services/skills.service.js');
|
|
129
|
+
await skillsManager.loadSkills();
|
|
130
|
+
logger.info('技能加载完成');
|
|
131
|
+
|
|
132
|
+
// 初始化技能同步服务
|
|
133
|
+
await skillSyncService.init();
|
|
134
|
+
} catch (error) {
|
|
135
|
+
logger.warn('加载技能或初始化同步服务失败,继续启动服务', { error: error.message });
|
|
136
|
+
}
|
|
137
|
+
|
|
125
138
|
// 同步系统工具到沙箱环境
|
|
126
139
|
try {
|
|
127
140
|
await syncSystemTools();
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
import { spawn } from 'child_process';
|
|
9
9
|
import { randomUUID } from 'crypto';
|
|
10
10
|
import { logger } from '../utils/logger.js';
|
|
11
|
+
import fs from 'fs';
|
|
11
12
|
import path from 'path';
|
|
12
13
|
import os from 'os';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
13
15
|
|
|
14
16
|
// 延迟加载 node-pty,避免编译错误
|
|
15
17
|
let pty = null;
|
|
@@ -244,22 +246,19 @@ export class TerminalService {
|
|
|
244
246
|
*/
|
|
245
247
|
async fixNodePtyPermissions() {
|
|
246
248
|
try {
|
|
247
|
-
const { execSync } = await import('child_process');
|
|
248
249
|
const platform = process.platform;
|
|
249
250
|
|
|
250
251
|
// 只在 Unix-like 系统上修复权限(macOS, Linux)
|
|
251
252
|
if (platform !== 'win32') {
|
|
252
253
|
logger.info('🔧 检查并修复 node-pty 二进制文件权限...');
|
|
253
254
|
|
|
255
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
256
|
+
const __dirname = path.dirname(__filename);
|
|
257
|
+
|
|
254
258
|
// 尝试多个可能的 node-pty 路径
|
|
255
259
|
const possiblePaths = [
|
|
256
260
|
// 路径1: 在包的 node_modules 中(开发环境)
|
|
257
|
-
path.join(
|
|
258
|
-
path.dirname(path.dirname(new URL(import.meta.url).pathname)),
|
|
259
|
-
'node_modules',
|
|
260
|
-
'node-pty',
|
|
261
|
-
'prebuilds'
|
|
262
|
-
),
|
|
261
|
+
path.join(path.dirname(__dirname), 'node_modules', 'node-pty', 'prebuilds'),
|
|
263
262
|
// 路径2: 在根 node_modules 中(npm 安装环境)
|
|
264
263
|
path.join(process.cwd(), 'node_modules', 'node-pty', 'prebuilds'),
|
|
265
264
|
// 路径3: 相对于当前工作目录
|
|
@@ -274,8 +273,16 @@ export class TerminalService {
|
|
|
274
273
|
)
|
|
275
274
|
];
|
|
276
275
|
|
|
276
|
+
// 路径4: Electron 打包环境(app.asar.unpacked / resources)
|
|
277
|
+
if (process.resourcesPath) {
|
|
278
|
+
possiblePaths.push(
|
|
279
|
+
path.join(process.resourcesPath, 'app.asar.unpacked', 'node_modules', 'node-pty', 'prebuilds'),
|
|
280
|
+
path.join(process.resourcesPath, 'app.asar.unpacked', 'app', 'node_modules', 'node-pty', 'prebuilds'),
|
|
281
|
+
path.join(process.resourcesPath, 'node_modules', 'node-pty', 'prebuilds')
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
277
285
|
let ptyPath = null;
|
|
278
|
-
const fs = await import('fs');
|
|
279
286
|
|
|
280
287
|
for (const possiblePath of possiblePaths) {
|
|
281
288
|
if (fs.existsSync(possiblePath)) {
|
|
@@ -286,14 +293,27 @@ export class TerminalService {
|
|
|
286
293
|
|
|
287
294
|
if (ptyPath) {
|
|
288
295
|
try {
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
{
|
|
293
|
-
|
|
294
|
-
|
|
296
|
+
// 递归修复 .node 与 spawn-helper 权限,避免空格路径问题
|
|
297
|
+
const fixPermissionsRecursive = async targetPath => {
|
|
298
|
+
const entries = await fs.promises.readdir(targetPath, { withFileTypes: true });
|
|
299
|
+
for (const entry of entries) {
|
|
300
|
+
const entryPath = path.join(targetPath, entry.name);
|
|
301
|
+
if (entry.isDirectory()) {
|
|
302
|
+
await fixPermissionsRecursive(entryPath);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (entry.isFile() && (entry.name.endsWith('.node') || entry.name === 'spawn-helper')) {
|
|
307
|
+
try {
|
|
308
|
+
await fs.promises.chmod(entryPath, 0o755);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
logger.debug(`node-pty 权限修复失败: ${entryPath} - ${error.message}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
295
313
|
}
|
|
296
|
-
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
await fixPermissionsRecursive(ptyPath);
|
|
297
317
|
logger.info('✅ node-pty 权限修复完成');
|
|
298
318
|
} catch (error) {
|
|
299
319
|
// 静默失败,不影响服务启动
|
|
@@ -674,8 +694,8 @@ export class TerminalService {
|
|
|
674
694
|
|
|
675
695
|
for (const session of this.sessions.values()) {
|
|
676
696
|
if (!session.isActive || now - session.lastActivity > timeoutMs) {
|
|
677
|
-
logger.info(`Cleaning up inactive session: ${session.
|
|
678
|
-
this.removeSession(session.
|
|
697
|
+
logger.info(`Cleaning up inactive session: ${session.id}`);
|
|
698
|
+
this.removeSession(session.id);
|
|
679
699
|
}
|
|
680
700
|
}
|
|
681
701
|
}
|