@shareai-lab/kode-sdk 2.7.2 → 2.7.3
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/README.md +3 -0
- package/README.zh-CN.md +3 -0
- package/dist/core/agent.d.ts +7 -0
- package/dist/core/agent.js +100 -7
- package/dist/core/skills/management-manager.d.ts +93 -60
- package/dist/core/skills/management-manager.js +661 -391
- package/dist/core/skills/manager.d.ts +4 -0
- package/dist/core/skills/manager.js +39 -16
- package/dist/core/skills/types.d.ts +12 -0
- package/dist/core/types.d.ts +9 -1
- package/dist/infra/providers/anthropic.js +16 -0
- package/dist/infra/providers/gemini.js +32 -4
- package/dist/infra/providers/openai.js +27 -3
- package/dist/infra/providers/types.d.ts +35 -1
- package/dist/infra/providers/utils.d.ts +19 -1
- package/dist/infra/providers/utils.js +81 -1
- package/dist/infra/store/json-store.js +2 -1
- package/dist/tools/skills.js +2 -2
- package/dist/tools/type-inference.d.ts +1 -1
- package/package.json +3 -3
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* 技能管理器模块(路径1 - 技能管理)
|
|
4
4
|
*
|
|
5
5
|
* 设计原则 (UNIX哲学):
|
|
6
|
-
* - 简洁:
|
|
7
|
-
* - 模块化:
|
|
8
|
-
* - 隔离: 与Agent
|
|
6
|
+
* - 简洁: 只负责技能文件系统的管理操作
|
|
7
|
+
* - 模块化: 单一职责,易于测试和维护
|
|
8
|
+
* - 隔离: 与Agent运行时完全隔离,不参与Agent使用
|
|
9
9
|
*
|
|
10
10
|
* ⚠️ 重要说明:
|
|
11
|
-
* - 此模块专门用于路径1
|
|
12
|
-
* - 与路径2
|
|
11
|
+
* - 此模块专门用于路径1(技能管理)
|
|
12
|
+
* - 与路径2(Agent运行时)完全独立
|
|
13
13
|
* - 请勿与SkillsManager混淆
|
|
14
|
+
*
|
|
15
|
+
* @see docs/skills-management-implementation-plan.md
|
|
14
16
|
*/
|
|
15
17
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
18
|
if (k2 === undefined) k2 = k;
|
|
@@ -49,468 +51,690 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
49
51
|
exports.SkillsManagementManager = void 0;
|
|
50
52
|
const fs = __importStar(require("fs/promises"));
|
|
51
53
|
const path = __importStar(require("path"));
|
|
54
|
+
const os = __importStar(require("os"));
|
|
52
55
|
const crypto = __importStar(require("crypto"));
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const sandbox_file_manager_1 = require("./sandbox-file-manager");
|
|
56
|
-
const sandbox_factory_1 = require("../../infra/sandbox-factory");
|
|
56
|
+
const child_process_1 = require("child_process");
|
|
57
|
+
const util_1 = require("util");
|
|
57
58
|
const logger_1 = require("../../utils/logger");
|
|
59
|
+
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
58
60
|
/**
|
|
59
61
|
* 技能管理器类
|
|
60
62
|
*
|
|
61
63
|
* 职责:
|
|
62
|
-
* -
|
|
63
|
-
* - 协调OperationQueue、SandboxFileManager进行文件系统操作
|
|
64
|
+
* - 提供所有技能管理操作的统一接口(导入、复制、重命名、归档、导出)
|
|
64
65
|
* - 处理业务逻辑和权限验证
|
|
66
|
+
* - 所有操作严格遵循Specification.md规范
|
|
65
67
|
* - ❌ 不参与Agent运行时
|
|
66
68
|
* - ❌ 不提供技能加载、扫描等Agent使用的功能
|
|
67
69
|
*/
|
|
68
70
|
class SkillsManagementManager {
|
|
69
|
-
constructor(skillsDir,
|
|
71
|
+
constructor(skillsDir, archivedDir // 可选,默认为 skills/.archived/
|
|
70
72
|
) {
|
|
71
73
|
this.skillsDir = path.resolve(skillsDir);
|
|
72
74
|
this.archivedDir = archivedDir ? path.resolve(archivedDir) : path.join(this.skillsDir, '.archived');
|
|
73
|
-
this.skillsManager = new manager_1.SkillsManager(this.skillsDir);
|
|
74
|
-
this.operationQueue = new operation_queue_1.OperationQueue();
|
|
75
|
-
this.sandboxFileManager = new sandbox_file_manager_1.SandboxFileManager(sandboxFactory || new sandbox_factory_1.SandboxFactory());
|
|
76
75
|
logger_1.logger.log(`[SkillsManagementManager] Initialized with skills directory: ${this.skillsDir}`);
|
|
77
76
|
logger_1.logger.log(`[SkillsManagementManager] Archived directory: ${this.archivedDir}`);
|
|
78
77
|
}
|
|
78
|
+
// ==================== 公共方法(8个核心操作) ====================
|
|
79
79
|
/**
|
|
80
|
-
*
|
|
80
|
+
* 1. 列出在线技能
|
|
81
|
+
* 扫描skills目录,排除.archived子目录
|
|
82
|
+
* 返回技能清单及其元数据信息
|
|
81
83
|
*/
|
|
82
84
|
async listSkills() {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
85
|
+
const skills = [];
|
|
86
|
+
try {
|
|
87
|
+
// 1. 读取skills目录
|
|
88
|
+
const entries = await fs.readdir(this.skillsDir, { withFileTypes: true });
|
|
89
|
+
// 2. 遍历每个子目录
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
// 排除.archived目录
|
|
92
|
+
if (entry.name === '.archived')
|
|
93
|
+
continue;
|
|
94
|
+
// 只处理目录
|
|
95
|
+
if (!entry.isDirectory())
|
|
96
|
+
continue;
|
|
97
|
+
const skillDir = path.join(this.skillsDir, entry.name);
|
|
98
|
+
// 3. 读取SKILL.md
|
|
99
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
100
|
+
const exists = await this.fileExists(skillMdPath);
|
|
101
|
+
if (!exists)
|
|
102
|
+
continue; // 跳过没有SKILL.md的目录
|
|
103
|
+
const content = await fs.readFile(skillMdPath, 'utf-8');
|
|
104
|
+
// 4. 解析YAML frontmatter
|
|
105
|
+
const metadata = this.parseSkillMd(content);
|
|
106
|
+
if (!metadata)
|
|
107
|
+
continue; // 跳过无效的SKILL.md
|
|
108
|
+
// 获取文件统计信息
|
|
109
|
+
const stat = await this.safeGetFileStat(skillMdPath);
|
|
110
|
+
skills.push({
|
|
111
|
+
name: metadata.name || entry.name,
|
|
112
|
+
description: metadata.description || '',
|
|
113
|
+
path: skillMdPath,
|
|
114
|
+
baseDir: skillDir,
|
|
115
|
+
folderName: entry.name,
|
|
116
|
+
createdAt: stat?.birthtime?.toISOString(),
|
|
117
|
+
updatedAt: stat?.mtime?.toISOString(),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return skills;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
logger_1.logger.error('[SkillsManagementManager] Error listing skills:', error);
|
|
124
|
+
throw error;
|
|
99
125
|
}
|
|
100
|
-
return skillsWithInfo;
|
|
101
126
|
}
|
|
102
127
|
/**
|
|
103
|
-
*
|
|
104
|
-
* @param
|
|
128
|
+
* 2. 安装新技能
|
|
129
|
+
* @param source 技能来源(名称/GitHub仓库/Git URL/本地路径)
|
|
130
|
+
* @param onProgress 可选的进度回调函数,用于实时传递安装日志
|
|
131
|
+
* 执行命令: npx -y ai-agent-skills install --agent project [source]
|
|
132
|
+
* 直接安装到.skills目录
|
|
105
133
|
*/
|
|
106
|
-
async
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
134
|
+
async installSkill(source, onProgress) {
|
|
135
|
+
try {
|
|
136
|
+
// 验证source参数
|
|
137
|
+
if (!source || source.trim().length === 0) {
|
|
138
|
+
throw new Error('技能来源不能为空');
|
|
139
|
+
}
|
|
140
|
+
// 构建命令
|
|
141
|
+
const command = `npx -y ai-agent-skills install --agent project ${source.trim()}`;
|
|
142
|
+
logger_1.logger.log(`[SkillsManagementManager] 正在安装技能: ${source}`);
|
|
143
|
+
onProgress?.({ type: 'log', message: `正在安装技能: ${source}` });
|
|
144
|
+
// 使用spawn替代execAsync以获取实时输出
|
|
145
|
+
const { spawn } = require('child_process');
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
// 使用父目录作为工作目录,避免在.skills内再创建.skills
|
|
148
|
+
const cwd = path.dirname(this.skillsDir);
|
|
149
|
+
const child = spawn(command, [], {
|
|
150
|
+
cwd,
|
|
151
|
+
shell: true,
|
|
152
|
+
env: { ...process.env }
|
|
153
|
+
});
|
|
154
|
+
let stdout = '';
|
|
155
|
+
let stderr = '';
|
|
156
|
+
let hasError = false;
|
|
157
|
+
// 检测错误关键词的辅助函数
|
|
158
|
+
const checkErrorKeywords = (text) => {
|
|
159
|
+
const trimmed = text.trim();
|
|
160
|
+
// 移除ANSI颜色码
|
|
161
|
+
const cleanText = trimmed.replace(/\u001b\[\d+m/g, '');
|
|
162
|
+
return cleanText.includes('not found') ||
|
|
163
|
+
cleanText.includes('ERROR') ||
|
|
164
|
+
cleanText.includes('error:') ||
|
|
165
|
+
cleanText.includes('Failed to') ||
|
|
166
|
+
cleanText.includes('Cannot find');
|
|
167
|
+
};
|
|
168
|
+
// 监听标准输出
|
|
169
|
+
child.stdout?.on('data', (data) => {
|
|
170
|
+
const message = data.toString();
|
|
171
|
+
stdout += message;
|
|
172
|
+
const trimmed = message.trim();
|
|
173
|
+
// 检测stdout中的错误关键词
|
|
174
|
+
if (checkErrorKeywords(trimmed)) {
|
|
175
|
+
hasError = true;
|
|
176
|
+
}
|
|
177
|
+
logger_1.logger.log(`[SkillsManagementManager] ${trimmed}`);
|
|
178
|
+
onProgress?.({ type: 'log', message: trimmed });
|
|
179
|
+
});
|
|
180
|
+
// 监听错误输出
|
|
181
|
+
child.stderr?.on('data', (data) => {
|
|
182
|
+
const message = data.toString();
|
|
183
|
+
stderr += message;
|
|
184
|
+
const trimmed = message.trim();
|
|
185
|
+
// 检测stderr中的错误关键词
|
|
186
|
+
if (checkErrorKeywords(trimmed)) {
|
|
187
|
+
hasError = true;
|
|
188
|
+
}
|
|
189
|
+
logger_1.logger.warn(`[SkillsManagementManager] ${trimmed}`);
|
|
190
|
+
onProgress?.({ type: 'error', message: trimmed });
|
|
191
|
+
});
|
|
192
|
+
// 监听进程退出
|
|
193
|
+
child.on('close', (code) => {
|
|
194
|
+
// 检查是否有错误标识或退出码非0
|
|
195
|
+
if (code !== 0 || hasError) {
|
|
196
|
+
let errorMsg = '';
|
|
197
|
+
if (hasError) {
|
|
198
|
+
// 从stderr中提取关键错误信息
|
|
199
|
+
const errorLines = stderr.split('\n').filter((line) => line.includes('not found') ||
|
|
200
|
+
line.includes('ERROR') ||
|
|
201
|
+
line.includes('error:') ||
|
|
202
|
+
line.includes('Failed') ||
|
|
203
|
+
line.includes('Cannot'));
|
|
204
|
+
errorMsg = errorLines.length > 0 ? errorLines[0].trim() : '安装过程中出现错误';
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
errorMsg = `安装进程退出码: ${code}`;
|
|
208
|
+
}
|
|
209
|
+
logger_1.logger.error(`[SkillsManagementManager] 安装失败: ${errorMsg}`);
|
|
210
|
+
reject(new Error(`安装技能失败: ${errorMsg}`));
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
logger_1.logger.log(`[SkillsManagementManager] 技能已安装: ${source}`);
|
|
214
|
+
resolve();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
// 监听错误
|
|
218
|
+
child.on('error', (error) => {
|
|
219
|
+
const errorMsg = `启动安装进程失败: ${error.message}`;
|
|
220
|
+
logger_1.logger.error(`[SkillsManagementManager] ${errorMsg}`);
|
|
221
|
+
onProgress?.({ type: 'error', message: errorMsg });
|
|
222
|
+
reject(new Error(`安装技能失败: ${errorMsg}`));
|
|
223
|
+
});
|
|
224
|
+
});
|
|
111
225
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
throw new Error(
|
|
226
|
+
catch (error) {
|
|
227
|
+
logger_1.logger.error(`[SkillsManagementManager] 安装技能失败: ${error.message}`);
|
|
228
|
+
onProgress?.({ type: 'error', message: error.message });
|
|
229
|
+
throw new Error(`安装技能失败: ${error.message}`);
|
|
116
230
|
}
|
|
117
|
-
// 获取文件树
|
|
118
|
-
const files = await this.getSkillFileTree(skillName);
|
|
119
|
-
// 获取文件统计信息
|
|
120
|
-
const stat = await this.safeGetFileStat(skill.metadata.path);
|
|
121
|
-
return {
|
|
122
|
-
name: skill.metadata.name,
|
|
123
|
-
description: skill.metadata.description,
|
|
124
|
-
path: skill.metadata.path,
|
|
125
|
-
baseDir: skill.metadata.baseDir,
|
|
126
|
-
createdAt: stat?.birthtime?.toISOString(),
|
|
127
|
-
updatedAt: stat?.mtime?.toISOString(),
|
|
128
|
-
files,
|
|
129
|
-
references: skill.references,
|
|
130
|
-
scripts: skill.scripts,
|
|
131
|
-
assets: skill.assets,
|
|
132
|
-
};
|
|
133
231
|
}
|
|
134
232
|
/**
|
|
135
|
-
*
|
|
233
|
+
* 3. 列出归档技能
|
|
234
|
+
* 扫描.archived目录
|
|
235
|
+
* 返回归档技能清单及其元数据信息
|
|
136
236
|
*/
|
|
137
237
|
async listArchivedSkills() {
|
|
138
|
-
|
|
139
|
-
const archivedDir = this.archivedDir;
|
|
140
|
-
// 检查archived目录是否存在
|
|
141
|
-
const exists = await this.fileExists(archivedDir);
|
|
142
|
-
if (!exists) {
|
|
143
|
-
return [];
|
|
144
|
-
}
|
|
238
|
+
const skills = [];
|
|
145
239
|
try {
|
|
146
|
-
//
|
|
147
|
-
const
|
|
148
|
-
|
|
240
|
+
// 1. 确保.archived目录存在
|
|
241
|
+
const exists = await this.fileExists(this.archivedDir);
|
|
242
|
+
if (!exists) {
|
|
243
|
+
return skills; // 返回空列表
|
|
244
|
+
}
|
|
245
|
+
// 2. 读取.archived目录
|
|
246
|
+
const entries = await fs.readdir(this.archivedDir, { withFileTypes: true });
|
|
247
|
+
// 3. 遍历每个子目录
|
|
149
248
|
for (const entry of entries) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
const archivedPath = path.join(archivedDir, entry.name);
|
|
154
|
-
// 检查是否包含SKILL.md
|
|
155
|
-
const skillMdPath = path.join(archivedPath, 'SKILL.md');
|
|
156
|
-
if (!(await this.fileExists(skillMdPath))) {
|
|
249
|
+
// 只处理目录
|
|
250
|
+
if (!entry.isDirectory())
|
|
157
251
|
continue;
|
|
158
|
-
|
|
159
|
-
//
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const [, date, hour, min, sec, ms] = timestampMatch;
|
|
173
|
-
const isoTimestamp = ms
|
|
174
|
-
? `${date}T${hour}:${min}:${sec}.${ms}Z`
|
|
175
|
-
: `${date}T${hour}:${min}:${sec}Z`;
|
|
176
|
-
const archivedAt = new Date(isoTimestamp).toISOString();
|
|
252
|
+
const skillDir = path.join(this.archivedDir, entry.name);
|
|
253
|
+
// 4. 读取SKILL.md
|
|
254
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
255
|
+
const skillExists = await this.fileExists(skillMdPath);
|
|
256
|
+
if (!skillExists)
|
|
257
|
+
continue; // 跳过没有SKILL.md的目录
|
|
258
|
+
const content = await fs.readFile(skillMdPath, 'utf-8');
|
|
259
|
+
// 5. 解析YAML frontmatter
|
|
260
|
+
const metadata = this.parseSkillMd(content);
|
|
261
|
+
if (!metadata)
|
|
262
|
+
continue; // 跳过无效的SKILL.md
|
|
263
|
+
// 6. 提取原始技能名称(去除-XXXXXXXX后缀)
|
|
264
|
+
const match = entry.name.match(/^(.+)-[a-f0-9]{8}$/);
|
|
265
|
+
const originalName = match ? match[1] : entry.name;
|
|
177
266
|
// 获取文件统计信息
|
|
178
|
-
const stat = await this.safeGetFileStat(
|
|
179
|
-
|
|
180
|
-
originalName,
|
|
267
|
+
const stat = await this.safeGetFileStat(skillDir);
|
|
268
|
+
skills.push({
|
|
269
|
+
originalName: originalName,
|
|
181
270
|
archivedName: entry.name,
|
|
182
|
-
archivedPath,
|
|
183
|
-
|
|
271
|
+
archivedPath: skillDir,
|
|
272
|
+
folderName: entry.name,
|
|
273
|
+
archivedAt: stat?.mtime?.toISOString() || new Date().toISOString(),
|
|
274
|
+
name: metadata.name,
|
|
275
|
+
description: metadata.description,
|
|
276
|
+
license: metadata.license,
|
|
184
277
|
});
|
|
185
278
|
}
|
|
186
|
-
return
|
|
279
|
+
return skills;
|
|
187
280
|
}
|
|
188
281
|
catch (error) {
|
|
189
282
|
logger_1.logger.error('[SkillsManagementManager] Error listing archived skills:', error);
|
|
190
|
-
|
|
283
|
+
throw error;
|
|
191
284
|
}
|
|
192
285
|
}
|
|
193
286
|
/**
|
|
194
|
-
*
|
|
287
|
+
* 4. 导入技能
|
|
288
|
+
* @param zipFilePath zip文件路径
|
|
289
|
+
* @param originalFileName 原始上传文件名(可选,用于无嵌套目录时的技能命名)
|
|
290
|
+
* 验证SKILL.md格式,解压并放置在在线技能目录中
|
|
291
|
+
*
|
|
292
|
+
* 检测逻辑:
|
|
293
|
+
* - 如果解压后根目录直接包含SKILL.md,视为无嵌套目录,使用originalFileName作为技能名称
|
|
294
|
+
* - 如果根目录不包含SKILL.md但包含多个子目录,每个子目录都有SKILL.md,则批量导入
|
|
295
|
+
*/
|
|
296
|
+
async importSkill(zipFilePath, originalFileName) {
|
|
297
|
+
try {
|
|
298
|
+
// 1. 验证zip文件存在
|
|
299
|
+
const exists = await this.fileExists(zipFilePath);
|
|
300
|
+
if (!exists) {
|
|
301
|
+
throw new Error(`Zip文件不存在: ${zipFilePath}`);
|
|
302
|
+
}
|
|
303
|
+
// 2. 创建临时目录
|
|
304
|
+
const tempDir = path.join(os.tmpdir(), `skill-import-${Date.now()}`);
|
|
305
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
306
|
+
try {
|
|
307
|
+
// 3. 解压zip文件
|
|
308
|
+
await this.extractZip(zipFilePath, tempDir);
|
|
309
|
+
// 4. 检测结构
|
|
310
|
+
const rootSkillMdPath = path.join(tempDir, 'SKILL.md');
|
|
311
|
+
const hasRootSkillMd = await this.fileExists(rootSkillMdPath);
|
|
312
|
+
if (hasRootSkillMd && originalFileName) {
|
|
313
|
+
// 4.1 无嵌套目录结构:根目录直接包含SKILL.md
|
|
314
|
+
// 使用原始文件名(去除.zip扩展名)作为技能名称
|
|
315
|
+
let skillName = originalFileName.replace(/\.zip$/i, '');
|
|
316
|
+
// 验证SKILL.md
|
|
317
|
+
const valid = await this.validateSkillMd(rootSkillMdPath);
|
|
318
|
+
if (!valid) {
|
|
319
|
+
throw new Error('技能格式无效, SKILL.md不符合Specification.md规范');
|
|
320
|
+
}
|
|
321
|
+
// 检测重名,如重名则添加后缀
|
|
322
|
+
let targetDir = path.join(this.skillsDir, skillName);
|
|
323
|
+
if (await this.fileExists(targetDir)) {
|
|
324
|
+
const suffix = await this.generateRandomSuffix();
|
|
325
|
+
const newName = `${skillName}-${suffix}`;
|
|
326
|
+
targetDir = path.join(this.skillsDir, newName);
|
|
327
|
+
logger_1.logger.log(`[SkillsManagementManager] 导入技能重名,已添加后缀: ${skillName} -> ${newName}`);
|
|
328
|
+
skillName = newName;
|
|
329
|
+
}
|
|
330
|
+
// 移动到在线技能目录
|
|
331
|
+
await this.safeRename(tempDir, targetDir);
|
|
332
|
+
logger_1.logger.log(`[SkillsManagementManager] 技能已导入: ${skillName}`);
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
// 4.2 嵌套目录结构:批量导入多个技能目录
|
|
336
|
+
const entries = await fs.readdir(tempDir, { withFileTypes: true });
|
|
337
|
+
for (const entry of entries) {
|
|
338
|
+
if (!entry.isDirectory())
|
|
339
|
+
continue;
|
|
340
|
+
const skillDir = path.join(tempDir, entry.name);
|
|
341
|
+
// 验证SKILL.md
|
|
342
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
343
|
+
const valid = await this.validateSkillMd(skillMdPath);
|
|
344
|
+
if (!valid) {
|
|
345
|
+
throw new Error(`技能格式无效: ${entry.name}, SKILL.md不符合Specification.md规范`);
|
|
346
|
+
}
|
|
347
|
+
// 检测重名,如重名则添加后缀
|
|
348
|
+
let targetName = entry.name;
|
|
349
|
+
let targetDir = path.join(this.skillsDir, targetName);
|
|
350
|
+
if (await this.fileExists(targetDir)) {
|
|
351
|
+
// 重名,添加随机后缀
|
|
352
|
+
const suffix = await this.generateRandomSuffix();
|
|
353
|
+
targetName = `${entry.name}-${suffix}`;
|
|
354
|
+
targetDir = path.join(this.skillsDir, targetName);
|
|
355
|
+
logger_1.logger.log(`[SkillsManagementManager] 导入技能重名,已添加后缀: ${entry.name} -> ${targetName}`);
|
|
356
|
+
}
|
|
357
|
+
// 移动到在线技能目录
|
|
358
|
+
await this.safeRename(skillDir, targetDir);
|
|
359
|
+
logger_1.logger.log(`[SkillsManagementManager] 技能已导入: ${targetName}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
// 5. 清理临时目录
|
|
365
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
logger_1.logger.error('[SkillsManagementManager] Error importing skill:', error);
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* 5. 复制技能
|
|
195
375
|
* @param skillName 技能名称
|
|
196
|
-
*
|
|
376
|
+
* 新技能名称: {原技能名称}-{XXXXXXXX}
|
|
197
377
|
*/
|
|
198
|
-
async
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (!skillDetail) {
|
|
220
|
-
throw new Error(`Failed to get skill info after creation: ${skillName}`);
|
|
221
|
-
}
|
|
222
|
-
return skillDetail;
|
|
378
|
+
async copySkill(skillName) {
|
|
379
|
+
try {
|
|
380
|
+
// 1. 验证技能存在
|
|
381
|
+
const sourceDir = path.join(this.skillsDir, skillName);
|
|
382
|
+
const exists = await this.fileExists(sourceDir);
|
|
383
|
+
if (!exists) {
|
|
384
|
+
throw new Error(`技能不存在: ${skillName}`);
|
|
385
|
+
}
|
|
386
|
+
// 2. 生成8位随机后缀
|
|
387
|
+
const suffix = await this.generateRandomSuffix();
|
|
388
|
+
const newSkillName = `${skillName}-${suffix}`;
|
|
389
|
+
const targetDir = path.join(this.skillsDir, newSkillName);
|
|
390
|
+
// 3. 递归复制目录
|
|
391
|
+
await fs.cp(sourceDir, targetDir, { recursive: true });
|
|
392
|
+
logger_1.logger.log(`[SkillsManagementManager] 技能已复制: ${skillName} -> ${newSkillName}`);
|
|
393
|
+
return newSkillName;
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
logger_1.logger.error('[SkillsManagementManager] Error copying skill:', error);
|
|
397
|
+
throw error;
|
|
398
|
+
}
|
|
223
399
|
}
|
|
224
400
|
/**
|
|
225
|
-
* 重命名技能
|
|
226
|
-
* @param oldName
|
|
227
|
-
* @param newName
|
|
401
|
+
* 6. 重命名技能
|
|
402
|
+
* @param oldName 旧技能文件夹名称
|
|
403
|
+
* @param newName 新技能文件夹名称
|
|
404
|
+
* 不支持操作归档技能
|
|
228
405
|
*/
|
|
229
406
|
async renameSkill(oldName, newName) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
407
|
+
try {
|
|
408
|
+
// 1. 验证旧技能存在
|
|
409
|
+
const oldPath = path.join(this.skillsDir, oldName);
|
|
410
|
+
const exists = await this.fileExists(oldPath);
|
|
411
|
+
if (!exists) {
|
|
412
|
+
throw new Error(`技能不存在: ${oldName}`);
|
|
413
|
+
}
|
|
414
|
+
// 2. 验证新名称
|
|
415
|
+
if (!this.isValidSkillName(newName)) {
|
|
416
|
+
throw new Error(`无效的技能名称: ${newName}`);
|
|
417
|
+
}
|
|
418
|
+
const newPath = path.join(this.skillsDir, newName);
|
|
419
|
+
if (await this.fileExists(newPath)) {
|
|
420
|
+
throw new Error(`技能已存在: ${newName}`);
|
|
421
|
+
}
|
|
422
|
+
// 3. 重命名目录(仅修改文件夹名称,不修改SKILL.md内容)
|
|
423
|
+
await this.safeRename(oldPath, newPath);
|
|
424
|
+
logger_1.logger.log(`[SkillsManagementManager] 技能已重命名: ${oldName} -> ${newName}`);
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
logger_1.logger.error('[SkillsManagementManager] Error renaming skill:', error);
|
|
428
|
+
throw error;
|
|
247
429
|
}
|
|
248
430
|
}
|
|
249
431
|
/**
|
|
250
|
-
*
|
|
432
|
+
* 7. 在线技能转归档
|
|
251
433
|
* @param skillName 技能名称
|
|
252
|
-
*
|
|
253
|
-
* @param content 文件内容
|
|
254
|
-
* @param useSandbox 是否使用sandbox(默认true)
|
|
434
|
+
* 归档名称: {原技能名称}-{XXXXXXXX}
|
|
255
435
|
*/
|
|
256
|
-
async
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
436
|
+
async archiveSkill(skillName) {
|
|
437
|
+
try {
|
|
438
|
+
// 1. 验证技能存在
|
|
439
|
+
const skillDir = path.join(this.skillsDir, skillName);
|
|
440
|
+
const exists = await this.fileExists(skillDir);
|
|
441
|
+
if (!exists) {
|
|
442
|
+
throw new Error(`技能不存在: ${skillName}`);
|
|
443
|
+
}
|
|
444
|
+
// 2. 生成8位随机后缀
|
|
445
|
+
const suffix = await this.generateRandomSuffix();
|
|
446
|
+
const archivedName = `${skillName}-${suffix}`;
|
|
447
|
+
// 3. 确保.archived目录存在
|
|
448
|
+
await fs.mkdir(this.archivedDir, { recursive: true });
|
|
449
|
+
// 4. 移动到.archived目录
|
|
450
|
+
const archivedPath = path.join(this.archivedDir, archivedName);
|
|
451
|
+
await this.safeRename(skillDir, archivedPath);
|
|
452
|
+
logger_1.logger.log(`[SkillsManagementManager] 技能已归档: ${skillName} -> ${archivedName}`);
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
logger_1.logger.error('[SkillsManagementManager] Error archiving skill:', error);
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* 8. 归档技能转在线
|
|
461
|
+
* @param archivedSkillName archived中的技能名称(含后缀)
|
|
462
|
+
* 移入前检测重名
|
|
463
|
+
*/
|
|
464
|
+
async unarchiveSkill(archivedSkillName) {
|
|
465
|
+
try {
|
|
466
|
+
// 1. 验证.archived技能存在
|
|
467
|
+
const archivedPath = path.join(this.archivedDir, archivedSkillName);
|
|
468
|
+
const exists = await this.fileExists(archivedPath);
|
|
469
|
+
if (!exists) {
|
|
470
|
+
throw new Error(`归档技能不存在: ${archivedSkillName}`);
|
|
471
|
+
}
|
|
472
|
+
// 2. 提取原始名称(去除-XXXXXXXX后缀)
|
|
473
|
+
const match = archivedSkillName.match(/^(.+)-[a-f0-9]{8}$/);
|
|
474
|
+
if (!match) {
|
|
475
|
+
throw new Error(`无效的归档技能名称: ${archivedSkillName}`);
|
|
476
|
+
}
|
|
477
|
+
const originalName = match[1];
|
|
478
|
+
// 3. 检测重名
|
|
479
|
+
const targetPath = path.join(this.skillsDir, originalName);
|
|
480
|
+
if (await this.fileExists(targetPath)) {
|
|
481
|
+
throw new Error(`技能已存在: ${originalName}`);
|
|
482
|
+
}
|
|
483
|
+
// 4. 移回skills目录
|
|
484
|
+
await this.safeRename(archivedPath, targetPath);
|
|
485
|
+
logger_1.logger.log(`[SkillsManagementManager] 技能已恢复: ${archivedSkillName} -> ${originalName}`);
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
logger_1.logger.error('[SkillsManagementManager] Error unarchiving skill:', error);
|
|
489
|
+
throw error;
|
|
274
490
|
}
|
|
275
491
|
}
|
|
276
492
|
/**
|
|
277
|
-
*
|
|
493
|
+
* 9. 查看在线技能内容
|
|
278
494
|
* @param skillName 技能名称
|
|
495
|
+
* 返回SKILL.md完整内容(包含frontmatter和正文)
|
|
279
496
|
*/
|
|
280
|
-
async
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
497
|
+
async getOnlineSkillContent(skillName) {
|
|
498
|
+
try {
|
|
499
|
+
// 1. 验证技能存在
|
|
500
|
+
const skillDir = path.join(this.skillsDir, skillName);
|
|
501
|
+
const exists = await this.fileExists(skillDir);
|
|
502
|
+
if (!exists) {
|
|
503
|
+
throw new Error(`技能不存在: ${skillName}`);
|
|
504
|
+
}
|
|
505
|
+
// 2. 读取SKILL.md
|
|
506
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
507
|
+
const skillExists = await this.fileExists(skillMdPath);
|
|
508
|
+
if (!skillExists) {
|
|
509
|
+
throw new Error(`SKILL.md不存在: ${skillName}`);
|
|
510
|
+
}
|
|
511
|
+
// 3. 返回完整内容
|
|
512
|
+
const content = await fs.readFile(skillMdPath, 'utf-8');
|
|
513
|
+
return content;
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
logger_1.logger.error('[SkillsManagementManager] Error getting online skill content:', error);
|
|
517
|
+
throw error;
|
|
298
518
|
}
|
|
299
519
|
}
|
|
300
520
|
/**
|
|
301
|
-
*
|
|
302
|
-
* @param archivedSkillName
|
|
521
|
+
* 10. 查看归档技能内容
|
|
522
|
+
* @param archivedSkillName 归档技能名称(含8位后缀)
|
|
523
|
+
* 返回SKILL.md完整内容(包含frontmatter和正文)
|
|
303
524
|
*/
|
|
304
|
-
async
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
525
|
+
async getArchivedSkillContent(archivedSkillName) {
|
|
526
|
+
try {
|
|
527
|
+
// 1. 验证归档技能存在
|
|
528
|
+
const skillDir = path.join(this.archivedDir, archivedSkillName);
|
|
529
|
+
const exists = await this.fileExists(skillDir);
|
|
530
|
+
if (!exists) {
|
|
531
|
+
throw new Error(`归档技能不存在: ${archivedSkillName}`);
|
|
532
|
+
}
|
|
533
|
+
// 2. 读取SKILL.md
|
|
534
|
+
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
|
535
|
+
const skillExists = await this.fileExists(skillMdPath);
|
|
536
|
+
if (!skillExists) {
|
|
537
|
+
throw new Error(`SKILL.md不存在: ${archivedSkillName}`);
|
|
538
|
+
}
|
|
539
|
+
// 3. 返回完整内容
|
|
540
|
+
const content = await fs.readFile(skillMdPath, 'utf-8');
|
|
541
|
+
return content;
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
logger_1.logger.error('[SkillsManagementManager] Error getting archived skill content:', error);
|
|
545
|
+
throw error;
|
|
322
546
|
}
|
|
323
547
|
}
|
|
324
548
|
/**
|
|
325
|
-
*
|
|
549
|
+
* 11. 查看在线技能文件目录结构
|
|
326
550
|
* @param skillName 技能名称
|
|
551
|
+
* 返回JSON格式的目录树结构
|
|
327
552
|
*/
|
|
328
|
-
async
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
553
|
+
async getOnlineSkillStructure(skillName) {
|
|
554
|
+
try {
|
|
555
|
+
// 1. 验证技能存在
|
|
556
|
+
const skillDir = path.join(this.skillsDir, skillName);
|
|
557
|
+
const exists = await this.fileExists(skillDir);
|
|
558
|
+
if (!exists) {
|
|
559
|
+
throw new Error(`技能不存在: ${skillName}`);
|
|
560
|
+
}
|
|
561
|
+
// 2. 构建目录树
|
|
562
|
+
return await this.buildDirectoryTree(skillDir, skillName);
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
logger_1.logger.error('[SkillsManagementManager] Error getting online skill structure:', error);
|
|
566
|
+
throw error;
|
|
567
|
+
}
|
|
341
568
|
}
|
|
342
569
|
/**
|
|
343
|
-
*
|
|
570
|
+
* 12. 查看归档技能文件目录结构
|
|
571
|
+
* @param archivedSkillName 归档技能名称(含8位后缀)
|
|
572
|
+
* 返回JSON格式的目录树结构
|
|
344
573
|
*/
|
|
345
|
-
|
|
346
|
-
|
|
574
|
+
async getArchivedSkillStructure(archivedSkillName) {
|
|
575
|
+
try {
|
|
576
|
+
// 1. 验证归档技能存在
|
|
577
|
+
const skillDir = path.join(this.archivedDir, archivedSkillName);
|
|
578
|
+
const exists = await this.fileExists(skillDir);
|
|
579
|
+
if (!exists) {
|
|
580
|
+
throw new Error(`归档技能不存在: ${archivedSkillName}`);
|
|
581
|
+
}
|
|
582
|
+
// 2. 构建目录树
|
|
583
|
+
return await this.buildDirectoryTree(skillDir, archivedSkillName);
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
logger_1.logger.error('[SkillsManagementManager] Error getting archived skill structure:', error);
|
|
587
|
+
throw error;
|
|
588
|
+
}
|
|
347
589
|
}
|
|
348
|
-
// ==================== 私有方法 ====================
|
|
349
590
|
/**
|
|
350
|
-
*
|
|
591
|
+
* 13. 导出技能
|
|
592
|
+
* @param skillName 技能名称(在线或归档)
|
|
593
|
+
* @param isArchived 是否为归档技能
|
|
594
|
+
* 使用系统zip命令打包,放入临时目录
|
|
351
595
|
*/
|
|
352
|
-
async
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
await fs.mkdir(path.join(skillDir, 'assets'));
|
|
379
|
-
// 3. 生成SKILL.md
|
|
380
|
-
const skillMdContent = this.generateSkillMd(options);
|
|
381
|
-
await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillMdContent, 'utf-8');
|
|
382
|
-
logger_1.logger.log(`[SkillsManagementManager] Skill created: ${skillName}`);
|
|
596
|
+
async exportSkill(skillName, isArchived) {
|
|
597
|
+
try {
|
|
598
|
+
// 1. 确定技能路径
|
|
599
|
+
let skillDir;
|
|
600
|
+
if (isArchived) {
|
|
601
|
+
skillDir = path.join(this.archivedDir, skillName);
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
skillDir = path.join(this.skillsDir, skillName);
|
|
605
|
+
}
|
|
606
|
+
const exists = await this.fileExists(skillDir);
|
|
607
|
+
if (!exists) {
|
|
608
|
+
throw new Error(`技能不存在: ${skillName}`);
|
|
609
|
+
}
|
|
610
|
+
// 2. 生成zip文件路径
|
|
611
|
+
const zipFileName = `${skillName}.zip`;
|
|
612
|
+
const zipFilePath = path.join(os.tmpdir(), zipFileName);
|
|
613
|
+
// 3. 使用系统zip命令打包
|
|
614
|
+
await this.createZip(skillDir, zipFilePath);
|
|
615
|
+
logger_1.logger.log(`[SkillsManagementManager] 技能已导出: ${skillName} -> ${zipFilePath}`);
|
|
616
|
+
return zipFilePath;
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
logger_1.logger.error('[SkillsManagementManager] Error exporting skill:', error);
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
383
622
|
}
|
|
623
|
+
// ==================== 私有辅助方法 ====================
|
|
384
624
|
/**
|
|
385
|
-
*
|
|
625
|
+
* 递归构建目录树
|
|
386
626
|
*/
|
|
387
|
-
async
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
627
|
+
async buildDirectoryTree(dirPath, relativePath = '') {
|
|
628
|
+
try {
|
|
629
|
+
const stats = await fs.stat(dirPath);
|
|
630
|
+
const name = path.basename(dirPath);
|
|
631
|
+
if (!stats.isDirectory()) {
|
|
632
|
+
return {
|
|
633
|
+
name,
|
|
634
|
+
type: 'file',
|
|
635
|
+
path: relativePath || name,
|
|
636
|
+
size: stats.size,
|
|
637
|
+
modifiedTime: stats.mtime.toISOString(),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
641
|
+
const children = [];
|
|
642
|
+
for (const entry of entries) {
|
|
643
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
644
|
+
const entryRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
645
|
+
if (entry.isDirectory()) {
|
|
646
|
+
children.push(await this.buildDirectoryTree(fullPath, entryRelativePath));
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
const stat = await fs.stat(fullPath);
|
|
650
|
+
children.push({
|
|
651
|
+
name: entry.name,
|
|
652
|
+
type: 'file',
|
|
653
|
+
path: entryRelativePath,
|
|
654
|
+
size: stat.size,
|
|
655
|
+
modifiedTime: stat.mtime.toISOString(),
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
name,
|
|
661
|
+
type: 'directory',
|
|
662
|
+
path: relativePath || name,
|
|
663
|
+
children,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
catch (error) {
|
|
667
|
+
logger_1.logger.error('[SkillsManagementManager] Error building directory tree:', error);
|
|
668
|
+
throw error;
|
|
669
|
+
}
|
|
411
670
|
}
|
|
412
671
|
/**
|
|
413
|
-
*
|
|
672
|
+
* 解析SKILL.md的YAML frontmatter
|
|
414
673
|
*/
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
// 3. 写入文件
|
|
432
|
-
if (useSandbox) {
|
|
433
|
-
// 使用sandbox写入(安全)
|
|
434
|
-
await this.sandboxFileManager.writeFile(skill.metadata.baseDir, normalizedPath, content);
|
|
435
|
-
}
|
|
436
|
-
else {
|
|
437
|
-
// 直接写入(不推荐)
|
|
438
|
-
const fullPath = path.join(skill.metadata.baseDir, normalizedPath);
|
|
439
|
-
await fs.writeFile(fullPath, content, 'utf-8');
|
|
440
|
-
}
|
|
441
|
-
logger_1.logger.log(`[SkillsManagementManager] File edited: ${skillName}/${filePath}`);
|
|
674
|
+
parseSkillMd(content) {
|
|
675
|
+
const match = content.match(/^---\n([\s\S]+?)\n---/);
|
|
676
|
+
if (!match)
|
|
677
|
+
return null;
|
|
678
|
+
const yamlContent = match[1];
|
|
679
|
+
// 简单YAML解析(只解析基本字段)
|
|
680
|
+
const nameMatch = yamlContent.match(/^name:\s*(.+)$/m);
|
|
681
|
+
const descMatch = yamlContent.match(/^description:\s*(.+)$/m);
|
|
682
|
+
const licenseMatch = yamlContent.match(/^license:\s*(.+)$/m);
|
|
683
|
+
if (!nameMatch)
|
|
684
|
+
return null;
|
|
685
|
+
return {
|
|
686
|
+
name: nameMatch[1].trim(),
|
|
687
|
+
description: descMatch ? descMatch[1].trim() : '',
|
|
688
|
+
license: licenseMatch ? licenseMatch[1].trim() : undefined,
|
|
689
|
+
};
|
|
442
690
|
}
|
|
443
691
|
/**
|
|
444
|
-
*
|
|
692
|
+
* 验证SKILL.md格式(遵循Specification.md规范)
|
|
445
693
|
*/
|
|
446
|
-
async
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
694
|
+
async validateSkillMd(skillMdPath) {
|
|
695
|
+
const exists = await this.fileExists(skillMdPath);
|
|
696
|
+
if (!exists)
|
|
697
|
+
return false;
|
|
698
|
+
const content = await fs.readFile(skillMdPath, 'utf-8');
|
|
699
|
+
const metadata = this.parseSkillMd(content);
|
|
700
|
+
if (!metadata)
|
|
701
|
+
return false;
|
|
702
|
+
// 验证必需字段
|
|
703
|
+
if (!metadata.name || !metadata.description)
|
|
704
|
+
return false;
|
|
705
|
+
// 验证name字段格式(1-64字符,小写字母数字和连字符)
|
|
706
|
+
const nameRegex = /^[a-z0-9-]{1,64}$/;
|
|
707
|
+
if (!nameRegex.test(metadata.name))
|
|
708
|
+
return false;
|
|
709
|
+
// 验证name不以连字符开头或结尾
|
|
710
|
+
if (metadata.name.startsWith('-') || metadata.name.endsWith('-'))
|
|
711
|
+
return false;
|
|
712
|
+
return true;
|
|
462
713
|
}
|
|
463
714
|
/**
|
|
464
|
-
*
|
|
715
|
+
* 生成8位随机后缀
|
|
716
|
+
* 规则: uuidv4 → sha256 → 全小写 → 取前8位
|
|
465
717
|
*/
|
|
466
|
-
async
|
|
467
|
-
// 1.
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
474
|
-
// 2. 提取原始名称(去掉时间戳后缀,支持带毫秒和不带毫秒两种格式)
|
|
475
|
-
const originalName = archivedSkillName.replace(/_\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(?:-\d{3})?Z$/, '');
|
|
476
|
-
// 3. 检查目标位置是否已存在
|
|
477
|
-
const targetPath = path.join(this.skillsDir, originalName);
|
|
478
|
-
if (await this.fileExists(targetPath)) {
|
|
479
|
-
throw new Error(`Skill already exists: ${originalName}`);
|
|
480
|
-
}
|
|
481
|
-
// 4. 移回skills目录
|
|
482
|
-
await fs.rename(archivedPath, targetPath);
|
|
483
|
-
logger_1.logger.log(`[SkillsManagementManager] Skill restored: ${archivedSkillName} -> ${originalName}`);
|
|
718
|
+
async generateRandomSuffix() {
|
|
719
|
+
// 1. 生成UUID v4
|
|
720
|
+
const uuid = crypto.randomUUID();
|
|
721
|
+
// 2. 计算SHA256
|
|
722
|
+
const hash = crypto.createHash('sha256').update(uuid).digest('hex');
|
|
723
|
+
// 3. 全小写并取前8位
|
|
724
|
+
return hash.toLowerCase().substring(0, 8);
|
|
484
725
|
}
|
|
485
726
|
/**
|
|
486
727
|
* 验证技能名称
|
|
487
728
|
*/
|
|
488
729
|
isValidSkillName(name) {
|
|
489
|
-
//
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
return `---
|
|
498
|
-
name: ${name}
|
|
499
|
-
description: ${description}
|
|
500
|
-
---
|
|
501
|
-
|
|
502
|
-
# ${name}
|
|
503
|
-
|
|
504
|
-
This is a custom skill created for ${name}.
|
|
505
|
-
|
|
506
|
-
## Usage
|
|
507
|
-
|
|
508
|
-
Describe how to use this skill here.
|
|
509
|
-
|
|
510
|
-
## Configuration
|
|
511
|
-
|
|
512
|
-
Add any configuration details here.
|
|
513
|
-
`;
|
|
730
|
+
// 只允许小写字母、数字、连字符
|
|
731
|
+
const nameRegex = /^[a-z0-9-]+$/;
|
|
732
|
+
return nameRegex.test(name) &&
|
|
733
|
+
name.length > 0 &&
|
|
734
|
+
name.length <= 64 &&
|
|
735
|
+
!name.startsWith('-') &&
|
|
736
|
+
!name.endsWith('-') &&
|
|
737
|
+
!name.includes('--');
|
|
514
738
|
}
|
|
515
739
|
/**
|
|
516
740
|
* 检查文件是否存在
|
|
@@ -536,21 +760,67 @@ Add any configuration details here.
|
|
|
536
760
|
}
|
|
537
761
|
}
|
|
538
762
|
/**
|
|
539
|
-
*
|
|
763
|
+
* 解压zip文件
|
|
764
|
+
*/
|
|
765
|
+
async extractZip(zipFilePath, targetDir) {
|
|
766
|
+
const platform = os.platform();
|
|
767
|
+
try {
|
|
768
|
+
let cmd;
|
|
769
|
+
if (platform === 'win32') {
|
|
770
|
+
// Windows: 使用tar命令(Windows 10+内置)
|
|
771
|
+
cmd = `tar -xf "${zipFilePath}" -C "${targetDir}"`;
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
// Linux/Mac: 使用unzip命令
|
|
775
|
+
cmd = `unzip -q "${zipFilePath}" -d "${targetDir}"`;
|
|
776
|
+
}
|
|
777
|
+
await execAsync(cmd);
|
|
778
|
+
}
|
|
779
|
+
catch (error) {
|
|
780
|
+
throw new Error(`解压失败: ${error.message}`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* 跨平台的安全重命名方法
|
|
785
|
+
* Windows上使用"复制-删除"方式避免EPERM错误
|
|
786
|
+
*/
|
|
787
|
+
async safeRename(oldPath, newPath) {
|
|
788
|
+
const platform = os.platform();
|
|
789
|
+
try {
|
|
790
|
+
if (platform === 'win32') {
|
|
791
|
+
// Windows: 使用复制-删除方式(更可靠)
|
|
792
|
+
await fs.cp(oldPath, newPath, { recursive: true });
|
|
793
|
+
await fs.rm(oldPath, { recursive: true, force: true });
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
// Unix-like系统: 直接使用rename
|
|
797
|
+
await fs.rename(oldPath, newPath);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
catch (error) {
|
|
801
|
+
logger_1.logger.error(`[SkillsManagementManager] safeRename failed: ${error.message}`);
|
|
802
|
+
throw error;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* 创建zip文件
|
|
540
807
|
*/
|
|
541
|
-
async
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
808
|
+
async createZip(sourceDir, zipFilePath) {
|
|
809
|
+
const platform = os.platform();
|
|
810
|
+
try {
|
|
811
|
+
let cmd;
|
|
812
|
+
if (platform === 'win32') {
|
|
813
|
+
// Windows: 使用PowerShell的Compress-Archive
|
|
814
|
+
cmd = `powershell -Command "Compress-Archive -Path '${sourceDir}\\*' -DestinationPath '${zipFilePath}' -Force"`;
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
// Linux/Mac: 使用zip命令
|
|
818
|
+
cmd = `cd "${sourceDir}" && zip -r "${zipFilePath}" .`;
|
|
819
|
+
}
|
|
820
|
+
await execAsync(cmd);
|
|
821
|
+
}
|
|
822
|
+
catch (error) {
|
|
823
|
+
throw new Error(`创建zip失败: ${error.message}`);
|
|
554
824
|
}
|
|
555
825
|
}
|
|
556
826
|
}
|