@becrafter/prompt-manager 0.1.31 → 0.2.3-alpha.7
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 +20 -4
- package/packages/resources/tools/agent-browser/agent-browser.tool.js +18 -17
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@becrafter/prompt-manager",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.3-alpha.7",
|
|
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": "npm run build
|
|
30
|
-
"build: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",
|
|
@@ -47,12 +47,19 @@
|
|
|
47
47
|
"format:check": "cd packages/server && npm run format:check",
|
|
48
48
|
"verify": "npm run verify:publish",
|
|
49
49
|
"verify:publish": "node scripts/verify-publish.js",
|
|
50
|
+
"verify:build": "node scripts/verify-build.js",
|
|
50
51
|
"release:pre-check": "node scripts/pre-release-check.js",
|
|
51
52
|
"release:update-version": "node scripts/update-version.js",
|
|
52
53
|
"release:rollback": "node scripts/rollback-release.js rollback",
|
|
53
54
|
"check:deps": "bash scripts/check-dependencies.sh",
|
|
54
55
|
"check:env": "bash scripts/check-env.sh",
|
|
55
56
|
"setup:env": "node scripts/preinstall-check.js",
|
|
57
|
+
"clean": "bash scripts/clean-environment.sh",
|
|
58
|
+
"clean:cache": "bash scripts/clean-environment.sh cache-only",
|
|
59
|
+
"clean:deps": "bash scripts/clean-environment.sh deps-only",
|
|
60
|
+
"clean:build": "bash scripts/clean-environment.sh build-only",
|
|
61
|
+
"clean:reinstall": "npm run clean && npm install",
|
|
62
|
+
"test:build": "bash scripts/build.sh test-build",
|
|
56
63
|
"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"
|
|
57
64
|
},
|
|
58
65
|
"keywords": [
|
|
@@ -81,7 +88,7 @@
|
|
|
81
88
|
"node-fetch": "^3.3.2",
|
|
82
89
|
"node-pty": "^1.0.0",
|
|
83
90
|
"sharp": "^0.34.4",
|
|
84
|
-
"tar": "^7.5.
|
|
91
|
+
"tar": "^7.5.4",
|
|
85
92
|
"to-ico": "^1.0.1",
|
|
86
93
|
"ws": "^8.18.0",
|
|
87
94
|
"yaml": "^2.4.1",
|
|
@@ -114,5 +121,14 @@
|
|
|
114
121
|
},
|
|
115
122
|
"devDependencies": {
|
|
116
123
|
"@electron/rebuild": "^4.0.2"
|
|
124
|
+
},
|
|
125
|
+
"overrides": {
|
|
126
|
+
"form-data": "^4.0.1",
|
|
127
|
+
"lodash": "^4.17.21",
|
|
128
|
+
"minimist": "^1.2.8",
|
|
129
|
+
"qs": "^6.14.1",
|
|
130
|
+
"tough-cookie": "^4.1.4",
|
|
131
|
+
"jpeg-js": "^0.4.4",
|
|
132
|
+
"url-regex": "^5.0.0"
|
|
117
133
|
}
|
|
118
134
|
}
|
|
@@ -795,7 +795,7 @@ export default {
|
|
|
795
795
|
return { dragged: { from: params.source, to: params.target } };
|
|
796
796
|
|
|
797
797
|
case 'screenshot':
|
|
798
|
-
const
|
|
798
|
+
const screenshotPage = browser.getPage();
|
|
799
799
|
const screenshotOptions = {};
|
|
800
800
|
if (params.fullPage) screenshotOptions.fullPage = true;
|
|
801
801
|
if (params.screenshotFormat) screenshotOptions.type = params.screenshotFormat;
|
|
@@ -806,7 +806,7 @@ export default {
|
|
|
806
806
|
const buffer = await locator.screenshot(screenshotOptions);
|
|
807
807
|
return { buffer: buffer.toString('base64'), selector: params.selector };
|
|
808
808
|
} else {
|
|
809
|
-
const buffer = await
|
|
809
|
+
const buffer = await screenshotPage.screenshot(screenshotOptions);
|
|
810
810
|
if (params.screenshotPath) {
|
|
811
811
|
const fs = await import('fs');
|
|
812
812
|
fs.writeFileSync(params.screenshotPath, buffer);
|
|
@@ -908,60 +908,60 @@ export default {
|
|
|
908
908
|
|
|
909
909
|
case 'getbyrole':
|
|
910
910
|
if (params.role && params.subaction) {
|
|
911
|
-
const
|
|
912
|
-
const locator =
|
|
911
|
+
const rolePage = browser.getPage();
|
|
912
|
+
const locator = rolePage.getByRole(params.role, { name: params.name });
|
|
913
913
|
await this.executeSubaction(locator, params.subaction, params.value);
|
|
914
914
|
}
|
|
915
915
|
return { role: params.role, name: params.name };
|
|
916
916
|
|
|
917
917
|
case 'getbytext':
|
|
918
918
|
if (params.text && params.subaction) {
|
|
919
|
-
const
|
|
919
|
+
const textPage = browser.getPage();
|
|
920
920
|
const options = {};
|
|
921
921
|
if (params.exact !== undefined) options.exact = params.exact;
|
|
922
|
-
const locator =
|
|
922
|
+
const locator = textPage.getByText(params.text, options);
|
|
923
923
|
await this.executeSubaction(locator, params.subaction);
|
|
924
924
|
}
|
|
925
925
|
return { text: params.text };
|
|
926
926
|
|
|
927
927
|
case 'getbylabel':
|
|
928
928
|
if (params.label && params.subaction) {
|
|
929
|
-
const
|
|
930
|
-
const locator =
|
|
929
|
+
const labelPage = browser.getPage();
|
|
930
|
+
const locator = labelPage.getByLabel(params.label);
|
|
931
931
|
await this.executeSubaction(locator, params.subaction, params.value);
|
|
932
932
|
}
|
|
933
933
|
return { label: params.label };
|
|
934
934
|
|
|
935
935
|
case 'getbyplaceholder':
|
|
936
936
|
if (params.placeholder && params.subaction) {
|
|
937
|
-
const
|
|
938
|
-
const locator =
|
|
937
|
+
const placeholderPage = browser.getPage();
|
|
938
|
+
const locator = placeholderPage.getByPlaceholder(params.placeholder);
|
|
939
939
|
await this.executeSubaction(locator, params.subaction, params.value);
|
|
940
940
|
}
|
|
941
941
|
return { placeholder: params.placeholder };
|
|
942
942
|
|
|
943
943
|
case 'getbyalttext':
|
|
944
944
|
if (params.altText && params.subaction) {
|
|
945
|
-
const
|
|
946
|
-
const locator =
|
|
945
|
+
const altTextPage = browser.getPage();
|
|
946
|
+
const locator = altTextPage.getByAltText(params.altText);
|
|
947
947
|
await this.executeSubaction(locator, params.subaction);
|
|
948
948
|
}
|
|
949
949
|
return { altText: params.altText };
|
|
950
950
|
|
|
951
951
|
case 'getbytitle':
|
|
952
952
|
if (params.name && params.subaction) {
|
|
953
|
-
const
|
|
953
|
+
const titlePage = browser.getPage();
|
|
954
954
|
const options = {};
|
|
955
955
|
if (params.exact !== undefined) options.exact = params.exact;
|
|
956
|
-
const locator =
|
|
956
|
+
const locator = titlePage.getByTitle(params.name, options);
|
|
957
957
|
await this.executeSubaction(locator, params.subaction);
|
|
958
958
|
}
|
|
959
959
|
return { title: params.name };
|
|
960
960
|
|
|
961
961
|
case 'getbytestid':
|
|
962
962
|
if (params.testId && params.subaction) {
|
|
963
|
-
const
|
|
964
|
-
const locator =
|
|
963
|
+
const testIdPage = browser.getPage();
|
|
964
|
+
const locator = testIdPage.getByTestId(params.testId);
|
|
965
965
|
await this.executeSubaction(locator, params.subaction, params.value);
|
|
966
966
|
}
|
|
967
967
|
return { testId: params.testId };
|
|
@@ -1199,7 +1199,8 @@ export default {
|
|
|
1199
1199
|
case 'download':
|
|
1200
1200
|
if (params.selector && params.downloadPath) {
|
|
1201
1201
|
const locator = browser.getLocator(params.selector);
|
|
1202
|
-
const
|
|
1202
|
+
const downloadPage = browser.getPage();
|
|
1203
|
+
const downloadPromise = downloadPage.waitForEvent('download');
|
|
1203
1204
|
await locator.click();
|
|
1204
1205
|
const download = await downloadPromise;
|
|
1205
1206
|
await download.saveAs(params.downloadPath);
|
|
@@ -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();
|