@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@becrafter/prompt-manager",
3
- "version": "0.1.31",
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 --prefix packages/server",
30
- "build:admin-ui": "npm run build --prefix 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",
@@ -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.1",
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 page = browser.getPage();
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 page.screenshot(screenshotOptions);
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 page = browser.getPage();
912
- const locator = page.getByRole(params.role, { name: params.name });
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 page = browser.getPage();
919
+ const textPage = browser.getPage();
920
920
  const options = {};
921
921
  if (params.exact !== undefined) options.exact = params.exact;
922
- const locator = page.getByText(params.text, options);
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 page = browser.getPage();
930
- const locator = page.getByLabel(params.label);
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 page = browser.getPage();
938
- const locator = page.getByPlaceholder(params.placeholder);
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 page = browser.getPage();
946
- const locator = page.getByAltText(params.altText);
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 page = browser.getPage();
953
+ const titlePage = browser.getPage();
954
954
  const options = {};
955
955
  if (params.exact !== undefined) options.exact = params.exact;
956
- const locator = page.getByTitle(params.name, options);
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 page = browser.getPage();
964
- const locator = page.getByTestId(params.testId);
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 downloadPromise = page.waitForEvent('download');
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 {*} results
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 {*} result
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 {*} result
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();