@becrafter/prompt-manager 0.2.2 → 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.2.2",
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": "cd packages/server && npx esbuild ./index.js --bundle --outdir=dist --format=esm --platform=node --target=node18",
30
- "build:admin-ui": "cd packages/admin-ui && npx webpack --mode production",
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",
@@ -88,7 +88,7 @@
88
88
  "node-fetch": "^3.3.2",
89
89
  "node-pty": "^1.0.0",
90
90
  "sharp": "^0.34.4",
91
- "tar": "^7.5.1",
91
+ "tar": "^7.5.4",
92
92
  "to-ico": "^1.0.1",
93
93
  "ws": "^8.18.0",
94
94
  "yaml": "^2.4.1",
@@ -121,5 +121,14 @@
121
121
  },
122
122
  "devDependencies": {
123
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"
124
133
  }
125
134
  }
@@ -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();
@@ -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
- // 添加执行权限 - 使用 find 命令来处理所有平台
290
- execSync(
291
- `find ${ptyPath} -type f -name "*.node" -o -name "spawn-helper" | xargs chmod +x 2>/dev/null || true`,
292
- {
293
- stdio: 'pipe',
294
- timeout: 5000
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.sessionId}`);
678
- this.removeSession(session.sessionId);
697
+ logger.info(`Cleaning up inactive session: ${session.id}`);
698
+ this.removeSession(session.id);
679
699
  }
680
700
  }
681
701
  }