skill-base 2.0.4 → 2.0.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/README.md +177 -115
- package/bin/skill-base.js +29 -3
- package/package.json +4 -1
- package/src/cappy.js +416 -0
- package/src/database.js +11 -0
- package/src/index.js +125 -25
- package/src/middleware/auth.js +96 -32
- package/src/routes/auth.js +1 -1
- package/src/routes/skills.js +10 -5
- package/src/utils/zip.js +15 -4
- package/static/android-chrome-192x192.png +0 -0
- package/static/android-chrome-512x512.png +0 -0
- package/static/apple-touch-icon.png +0 -0
- package/static/assets/index-BkwByEEp.css +1 -0
- package/static/assets/index-CB4Diul3.js +209 -0
- package/static/favicon-16x16.png +0 -0
- package/static/favicon-32x32.png +0 -0
- package/static/favicon.ico +0 -0
- package/static/favicon.svg +14 -0
- package/static/index.html +18 -248
- package/static/site.webmanifest +1 -0
- package/static/admin/users.html +0 -593
- package/static/cli-code.html +0 -203
- package/static/css/.gitkeep +0 -0
- package/static/css/style.css +0 -1567
- package/static/diff.html +0 -466
- package/static/file.html +0 -443
- package/static/js/.gitkeep +0 -0
- package/static/js/admin/users.js +0 -346
- package/static/js/app.js +0 -508
- package/static/js/auth.js +0 -151
- package/static/js/cli-code.js +0 -184
- package/static/js/collaborators.js +0 -283
- package/static/js/diff.js +0 -540
- package/static/js/file.js +0 -619
- package/static/js/i18n.js +0 -739
- package/static/js/index.js +0 -168
- package/static/js/publish.js +0 -718
- package/static/js/settings.js +0 -124
- package/static/js/setup.js +0 -157
- package/static/js/skill.js +0 -808
- package/static/login.html +0 -82
- package/static/publish.html +0 -459
- package/static/settings.html +0 -163
- package/static/setup.html +0 -101
- package/static/skill.html +0 -851
package/static/js/publish.js
DELETED
|
@@ -1,718 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Skill Base - 发布页逻辑
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const DESC_MAX = 500;
|
|
6
|
-
|
|
7
|
-
let selectedZipBlob = null; // 最终要上传的 zip blob
|
|
8
|
-
let selectedFileName = '';
|
|
9
|
-
let isNewSkill = true;
|
|
10
|
-
|
|
11
|
-
document.addEventListener('DOMContentLoaded', async () => {
|
|
12
|
-
// 1. 检查登录状态 + 渲染导航栏
|
|
13
|
-
const user = await checkAuth();
|
|
14
|
-
if (!user) return;
|
|
15
|
-
renderNavbar(user);
|
|
16
|
-
|
|
17
|
-
// 2. 加载已有 Skill 列表
|
|
18
|
-
await loadExistingSkills();
|
|
19
|
-
|
|
20
|
-
// 3. 绑定事件
|
|
21
|
-
setupDropZone();
|
|
22
|
-
setupZipFileInput();
|
|
23
|
-
setupSkillSelect();
|
|
24
|
-
setupFormSubmit();
|
|
25
|
-
setupClearFiles();
|
|
26
|
-
setupDescriptionCounter();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* 描述字数统计(与 maxlength 一致)
|
|
31
|
-
*/
|
|
32
|
-
function setupDescriptionCounter() {
|
|
33
|
-
const ta = document.getElementById('skill-description');
|
|
34
|
-
const countEl = document.getElementById('skill-description-count');
|
|
35
|
-
if (!ta || !countEl) return;
|
|
36
|
-
const sync = () => {
|
|
37
|
-
countEl.textContent = String(ta.value.length);
|
|
38
|
-
};
|
|
39
|
-
sync();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* 从文件夹名 / zip 文件名得到合法 Skill ID,无法得到则返回 ''
|
|
44
|
-
*/
|
|
45
|
-
function slugToSkillId(raw) {
|
|
46
|
-
if (!raw || typeof raw !== 'string') return '';
|
|
47
|
-
let s = raw.trim().replace(/\.zip$/i, '');
|
|
48
|
-
s = s
|
|
49
|
-
.toLowerCase()
|
|
50
|
-
.replace(/[\s_]+/g, '-')
|
|
51
|
-
.replace(/[^a-z0-9\-]+/g, '');
|
|
52
|
-
s = s.replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
53
|
-
if (!s || !/^[a-z0-9\-_]+$/.test(s)) return '';
|
|
54
|
-
return s;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function pickSkillMdPath(paths) {
|
|
58
|
-
const matches = paths.filter((p) => /(^|\/)SKILL\.md$/i.test(p));
|
|
59
|
-
if (!matches.length) return null;
|
|
60
|
-
return matches.slice().sort((a, b) => a.length - b.length)[0];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* 解析 SKILL.md 简单 YAML frontmatter(name / description)
|
|
65
|
-
*/
|
|
66
|
-
function parseYamlFrontmatterBlock(yaml) {
|
|
67
|
-
const out = {};
|
|
68
|
-
const lines = yaml.split(/\r?\n/);
|
|
69
|
-
let i = 0;
|
|
70
|
-
while (i < lines.length) {
|
|
71
|
-
const line = lines[i];
|
|
72
|
-
const m = line.match(/^([\w-]+):\s*(.*)$/);
|
|
73
|
-
if (!m) {
|
|
74
|
-
i += 1;
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
const key = m[1];
|
|
78
|
-
let rest = m[2].trimEnd();
|
|
79
|
-
const blockStarter =
|
|
80
|
-
rest === '>' ||
|
|
81
|
-
rest === '|' ||
|
|
82
|
-
rest === '>-' ||
|
|
83
|
-
rest === '>+' ||
|
|
84
|
-
rest === '|-' ||
|
|
85
|
-
rest === '|+';
|
|
86
|
-
if (blockStarter) {
|
|
87
|
-
i += 1;
|
|
88
|
-
const buf = [];
|
|
89
|
-
while (i < lines.length) {
|
|
90
|
-
const L = lines[i];
|
|
91
|
-
const nextKey = L.match(/^([\w-]+):\s/);
|
|
92
|
-
if (nextKey && !L.startsWith(' ') && buf.length) break;
|
|
93
|
-
if (L.startsWith(' ') || (L === '' && buf.length)) {
|
|
94
|
-
buf.push(L.startsWith(' ') ? L.slice(2) : '');
|
|
95
|
-
} else if (buf.length) break;
|
|
96
|
-
else if (L === '') {
|
|
97
|
-
i += 1;
|
|
98
|
-
continue;
|
|
99
|
-
} else break;
|
|
100
|
-
i += 1;
|
|
101
|
-
}
|
|
102
|
-
out[key] = buf.join('\n').trim();
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
out[key] = rest.replace(/^["'](.+)["']$/, '$1').trim();
|
|
106
|
-
i += 1;
|
|
107
|
-
}
|
|
108
|
-
return out;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* 从 SKILL.md 全文解析 name、description(frontmatter 优先,正文兜底)
|
|
113
|
-
*/
|
|
114
|
-
function parseSkillMd(full) {
|
|
115
|
-
let rest = full;
|
|
116
|
-
let name = '';
|
|
117
|
-
let description = '';
|
|
118
|
-
const fmMatch = full.match(/^---\r?\n([\s\S]*?)\r?\n---\s*\r?\n?/);
|
|
119
|
-
if (fmMatch) {
|
|
120
|
-
const y = parseYamlFrontmatterBlock(fmMatch[1]);
|
|
121
|
-
name = (y.name || '').trim();
|
|
122
|
-
description = (y.description || '').trim();
|
|
123
|
-
rest = full.slice(fmMatch[0].length);
|
|
124
|
-
}
|
|
125
|
-
if (!name) {
|
|
126
|
-
const h1 = rest.match(/^#\s+(.+)$/m);
|
|
127
|
-
if (h1) name = h1[1].trim();
|
|
128
|
-
}
|
|
129
|
-
if (!description) {
|
|
130
|
-
const afterH1 = rest.replace(/^#\s+.+$/m, '').trim();
|
|
131
|
-
const para = afterH1.split(/\n\n+/).find((p) => {
|
|
132
|
-
const t = p.trim();
|
|
133
|
-
return t && !t.startsWith('#') && !t.startsWith('```');
|
|
134
|
-
});
|
|
135
|
-
if (para) description = para.replace(/\s*\n\s*/g, ' ').trim();
|
|
136
|
-
}
|
|
137
|
-
if (description.length > DESC_MAX) description = description.slice(0, DESC_MAX);
|
|
138
|
-
return { name, description };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* 根据 SKILL.md 与包名填表(Skill ID / 名称 / 描述只读,仅来自上传包)
|
|
143
|
-
*/
|
|
144
|
-
function applyAutofillFromSkill(slugFromPackage, parsed) {
|
|
145
|
-
const idInput = document.getElementById('skill-id');
|
|
146
|
-
if (slugFromPackage) idInput.value = slugFromPackage;
|
|
147
|
-
document.getElementById('skill-name').value = (parsed.name || '').trim();
|
|
148
|
-
const descEl = document.getElementById('skill-description');
|
|
149
|
-
descEl.value = (parsed.description || '').slice(0, DESC_MAX);
|
|
150
|
-
const countEl = document.getElementById('skill-description-count');
|
|
151
|
-
if (countEl) countEl.textContent = String(descEl.value.length);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
async function readSkillMdFromZipInstance(zip, fileList) {
|
|
155
|
-
const skillPath = pickSkillMdPath(fileList);
|
|
156
|
-
if (!skillPath) return null;
|
|
157
|
-
const f = zip.file(skillPath);
|
|
158
|
-
if (!f) return null;
|
|
159
|
-
return f.async('string');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* 加载已有 Skill 列表
|
|
164
|
-
*/
|
|
165
|
-
async function loadExistingSkills() {
|
|
166
|
-
try {
|
|
167
|
-
const data = await apiGet('/skills');
|
|
168
|
-
const select = document.getElementById('skill-select');
|
|
169
|
-
|
|
170
|
-
if (data.skills && data.skills.length > 0) {
|
|
171
|
-
data.skills.forEach(skill => {
|
|
172
|
-
const option = document.createElement('option');
|
|
173
|
-
option.value = skill.skill_id;
|
|
174
|
-
option.textContent = `${skill.name} (${skill.skill_id})`;
|
|
175
|
-
option.dataset.name = skill.name;
|
|
176
|
-
option.dataset.description = skill.description || '';
|
|
177
|
-
select.appendChild(option);
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
} catch (error) {
|
|
181
|
-
console.error('加载 Skill 列表失败:', error);
|
|
182
|
-
// 不显示错误,允许用户继续创建新 Skill
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* 设置 Skill 选择下拉框事件
|
|
188
|
-
*/
|
|
189
|
-
function setupSkillSelect() {
|
|
190
|
-
const select = document.getElementById('skill-select');
|
|
191
|
-
|
|
192
|
-
select.addEventListener('change', () => {
|
|
193
|
-
toggleMode(select.value === '');
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* 切换新建/更新模式(元信息始终只读,来自上传包)
|
|
199
|
-
*/
|
|
200
|
-
function toggleMode(isNew) {
|
|
201
|
-
isNewSkill = isNew;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// ========================================
|
|
205
|
-
// 拖拽目录处理
|
|
206
|
-
// ========================================
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* 设置拖拽上传区域
|
|
210
|
-
*/
|
|
211
|
-
function setupDropZone() {
|
|
212
|
-
const dropZone = document.getElementById('drop-zone');
|
|
213
|
-
|
|
214
|
-
dropZone.addEventListener('dragover', (e) => {
|
|
215
|
-
e.preventDefault();
|
|
216
|
-
e.stopPropagation();
|
|
217
|
-
dropZone.classList.add('drag-over');
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
dropZone.addEventListener('dragleave', (e) => {
|
|
221
|
-
e.preventDefault();
|
|
222
|
-
e.stopPropagation();
|
|
223
|
-
dropZone.classList.remove('drag-over');
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
dropZone.addEventListener('drop', async (e) => {
|
|
227
|
-
e.preventDefault();
|
|
228
|
-
e.stopPropagation();
|
|
229
|
-
dropZone.classList.remove('drag-over');
|
|
230
|
-
await handleDrop(e);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
// 点击选择目录
|
|
234
|
-
dropZone.addEventListener('click', () => {
|
|
235
|
-
const input = document.createElement('input');
|
|
236
|
-
input.type = 'file';
|
|
237
|
-
input.webkitdirectory = true;
|
|
238
|
-
input.multiple = true;
|
|
239
|
-
input.onchange = (e) => handleDirectorySelect(e.target.files);
|
|
240
|
-
input.click();
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* 拖拽的 zip 文件(FileSystemFileEntry)
|
|
246
|
-
*/
|
|
247
|
-
async function handleDroppedZipFile(fileEntry) {
|
|
248
|
-
const file = await new Promise((resolve, reject) => {
|
|
249
|
-
fileEntry.file(resolve, reject);
|
|
250
|
-
});
|
|
251
|
-
const slug = slugToSkillId(file.name);
|
|
252
|
-
showToast('正在读取 zip...', 'info');
|
|
253
|
-
try {
|
|
254
|
-
const zip = await JSZip.loadAsync(file);
|
|
255
|
-
const paths = [];
|
|
256
|
-
zip.forEach((relPath, zf) => {
|
|
257
|
-
if (!zf.dir) paths.push(relPath);
|
|
258
|
-
});
|
|
259
|
-
if (!pickSkillMdPath(paths)) {
|
|
260
|
-
showToast('zip 中未找到 SKILL.md', 'error');
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
const text = await readSkillMdFromZipInstance(zip, paths);
|
|
264
|
-
const parsed = parseSkillMd(text);
|
|
265
|
-
selectedZipBlob = file;
|
|
266
|
-
selectedFileName = file.name;
|
|
267
|
-
renderFilePreview(paths, file.size);
|
|
268
|
-
applyAutofillFromSkill(slug, parsed);
|
|
269
|
-
showToast(`已选择 zip,共 ${paths.length} 个文件`, 'success');
|
|
270
|
-
} catch (error) {
|
|
271
|
-
console.error('Failed to read zip:', error);
|
|
272
|
-
showToast('读取 zip 失败: ' + error.message, 'error');
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* 处理拖拽的目录或 zip
|
|
278
|
-
*/
|
|
279
|
-
async function handleDrop(event) {
|
|
280
|
-
const items = event.dataTransfer.items;
|
|
281
|
-
|
|
282
|
-
if (!items || items.length === 0) {
|
|
283
|
-
showToast('未检测到拖拽的文件', 'error');
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const first = items[0];
|
|
288
|
-
if (first.kind !== 'file') {
|
|
289
|
-
showToast('请拖拽文件夹或 zip 文件', 'error');
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const topEntry = first.webkitGetAsEntry();
|
|
294
|
-
if (!topEntry) {
|
|
295
|
-
showToast('无法读取拖拽项', 'error');
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
if (topEntry.isFile && topEntry.name.toLowerCase().endsWith('.zip')) {
|
|
300
|
-
await handleDroppedZipFile(topEntry);
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
let rootSlug = '';
|
|
305
|
-
if (topEntry.isDirectory) {
|
|
306
|
-
rootSlug = slugToSkillId(topEntry.name);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const zip = new JSZip();
|
|
310
|
-
const fileList = [];
|
|
311
|
-
let totalSize = 0;
|
|
312
|
-
|
|
313
|
-
showToast('正在读取文件...', 'info');
|
|
314
|
-
|
|
315
|
-
try {
|
|
316
|
-
for (const item of items) {
|
|
317
|
-
if (item.kind === 'file') {
|
|
318
|
-
const entry = item.webkitGetAsEntry();
|
|
319
|
-
if (entry) {
|
|
320
|
-
await traverseEntry(entry, zip, '', fileList, (size) => {
|
|
321
|
-
totalSize += size;
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (!pickSkillMdPath(fileList)) {
|
|
328
|
-
showToast('上传的目录中未找到 SKILL.md 文件', 'error');
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const skillText = await readSkillMdFromZipInstance(zip, fileList);
|
|
333
|
-
const parsed = skillText != null ? parseSkillMd(skillText) : { name: '', description: '' };
|
|
334
|
-
|
|
335
|
-
showToast('正在打包文件...', 'info');
|
|
336
|
-
selectedZipBlob = await zip.generateAsync({ type: 'blob' });
|
|
337
|
-
selectedFileName = 'skill-package.zip';
|
|
338
|
-
|
|
339
|
-
renderFilePreview(fileList, totalSize);
|
|
340
|
-
applyAutofillFromSkill(rootSlug, parsed);
|
|
341
|
-
showToast(`已选择 ${fileList.length} 个文件`, 'success');
|
|
342
|
-
} catch (error) {
|
|
343
|
-
console.error('Failed to process dropped files:', error);
|
|
344
|
-
showToast('处理文件失败: ' + error.message, 'error');
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* 递归遍历目录 entry
|
|
350
|
-
*/
|
|
351
|
-
async function traverseEntry(entry, zip, path, fileList, onFileSize) {
|
|
352
|
-
if (entry.isFile) {
|
|
353
|
-
const file = await new Promise((resolve, reject) => {
|
|
354
|
-
entry.file(resolve, reject);
|
|
355
|
-
});
|
|
356
|
-
const fullPath = path + entry.name;
|
|
357
|
-
zip.file(fullPath, file);
|
|
358
|
-
fileList.push(fullPath);
|
|
359
|
-
if (onFileSize) onFileSize(file.size);
|
|
360
|
-
} else if (entry.isDirectory) {
|
|
361
|
-
const dirPath = path + entry.name + '/';
|
|
362
|
-
const reader = entry.createReader();
|
|
363
|
-
|
|
364
|
-
const entries = await new Promise((resolve, reject) => {
|
|
365
|
-
const results = [];
|
|
366
|
-
const readEntries = () => {
|
|
367
|
-
reader.readEntries(items => {
|
|
368
|
-
if (items.length === 0) {
|
|
369
|
-
resolve(results);
|
|
370
|
-
} else {
|
|
371
|
-
results.push(...items);
|
|
372
|
-
readEntries();
|
|
373
|
-
}
|
|
374
|
-
}, reject);
|
|
375
|
-
};
|
|
376
|
-
readEntries();
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
for (const child of entries) {
|
|
380
|
-
await traverseEntry(child, zip, dirPath, fileList, onFileSize);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* 处理选择目录(input webkitdirectory)
|
|
387
|
-
*/
|
|
388
|
-
async function handleDirectorySelect(files) {
|
|
389
|
-
if (!files || files.length === 0) {
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const zip = new JSZip();
|
|
394
|
-
const fileList = [];
|
|
395
|
-
let totalSize = 0;
|
|
396
|
-
|
|
397
|
-
showToast('正在读取文件...', 'info');
|
|
398
|
-
|
|
399
|
-
try {
|
|
400
|
-
for (const file of files) {
|
|
401
|
-
const path = file.webkitRelativePath;
|
|
402
|
-
zip.file(path, file);
|
|
403
|
-
fileList.push(path);
|
|
404
|
-
totalSize += file.size;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (!pickSkillMdPath(fileList)) {
|
|
408
|
-
showToast('上传的目录中未找到 SKILL.md 文件', 'error');
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
const skillText = await readSkillMdFromZipInstance(zip, fileList);
|
|
413
|
-
const parsed = skillText != null ? parseSkillMd(skillText) : { name: '', description: '' };
|
|
414
|
-
const rootSlug = slugToSkillId(files[0].webkitRelativePath.split('/')[0] || '');
|
|
415
|
-
|
|
416
|
-
showToast('正在打包文件...', 'info');
|
|
417
|
-
selectedZipBlob = await zip.generateAsync({ type: 'blob' });
|
|
418
|
-
selectedFileName = 'skill-package.zip';
|
|
419
|
-
|
|
420
|
-
renderFilePreview(fileList, totalSize);
|
|
421
|
-
applyAutofillFromSkill(rootSlug, parsed);
|
|
422
|
-
showToast(`已选择 ${fileList.length} 个文件`, 'success');
|
|
423
|
-
} catch (error) {
|
|
424
|
-
console.error('Failed to process directory selection:', error);
|
|
425
|
-
showToast('处理文件失败: ' + error.message, 'error');
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// ========================================
|
|
430
|
-
// zip 文件选择
|
|
431
|
-
// ========================================
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* 设置 zip 文件选择
|
|
435
|
-
*/
|
|
436
|
-
function setupZipFileInput() {
|
|
437
|
-
const input = document.getElementById('zip-file-input');
|
|
438
|
-
|
|
439
|
-
input.addEventListener('change', async (e) => {
|
|
440
|
-
const file = e.target.files[0];
|
|
441
|
-
if (!file) return;
|
|
442
|
-
if (!file.name.toLowerCase().endsWith('.zip')) {
|
|
443
|
-
showToast('请选择 .zip 文件', 'error');
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
const slug = slugToSkillId(file.name);
|
|
447
|
-
showToast('正在读取 zip...', 'info');
|
|
448
|
-
try {
|
|
449
|
-
const zip = await JSZip.loadAsync(file);
|
|
450
|
-
const paths = [];
|
|
451
|
-
zip.forEach((relPath, zf) => {
|
|
452
|
-
if (!zf.dir) paths.push(relPath);
|
|
453
|
-
});
|
|
454
|
-
if (!pickSkillMdPath(paths)) {
|
|
455
|
-
showToast('zip 中未找到 SKILL.md', 'error');
|
|
456
|
-
input.value = '';
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
const text = await readSkillMdFromZipInstance(zip, paths);
|
|
460
|
-
const parsed = parseSkillMd(text);
|
|
461
|
-
selectedZipBlob = file;
|
|
462
|
-
selectedFileName = file.name;
|
|
463
|
-
renderFilePreview(paths, file.size);
|
|
464
|
-
applyAutofillFromSkill(slug, parsed);
|
|
465
|
-
showToast('已选择 zip 文件', 'success');
|
|
466
|
-
} catch (err) {
|
|
467
|
-
console.error('Failed to read zip:', err);
|
|
468
|
-
showToast('读取 zip 失败: ' + err.message, 'error');
|
|
469
|
-
input.value = '';
|
|
470
|
-
}
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* 渲染文件预览列表
|
|
476
|
-
*/
|
|
477
|
-
function renderFilePreview(fileList, totalSize) {
|
|
478
|
-
const preview = document.getElementById('file-preview');
|
|
479
|
-
const listContainer = document.getElementById('file-preview-list');
|
|
480
|
-
const summary = document.getElementById('file-preview-summary');
|
|
481
|
-
|
|
482
|
-
// 清空列表
|
|
483
|
-
listContainer.innerHTML = '';
|
|
484
|
-
|
|
485
|
-
// 显示文件列表(最多 20 个)
|
|
486
|
-
const maxDisplay = 20;
|
|
487
|
-
const displayList = fileList.slice(0, maxDisplay);
|
|
488
|
-
|
|
489
|
-
displayList.forEach(file => {
|
|
490
|
-
const item = document.createElement('div');
|
|
491
|
-
item.className = 'file-item';
|
|
492
|
-
item.textContent = file;
|
|
493
|
-
listContainer.appendChild(item);
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
// 如果超过 20 个,显示省略提示
|
|
497
|
-
if (fileList.length > maxDisplay) {
|
|
498
|
-
const more = document.createElement('div');
|
|
499
|
-
more.className = 'file-preview-more';
|
|
500
|
-
more.textContent = `...and ${fileList.length - maxDisplay} more files`;
|
|
501
|
-
listContainer.appendChild(more);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// 显示总结
|
|
505
|
-
summary.textContent = `${fileList.length} files, total size: ${formatFileSize(totalSize || selectedZipBlob?.size || 0)}`;
|
|
506
|
-
|
|
507
|
-
// 显示预览区域
|
|
508
|
-
preview.classList.add('visible');
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* 设置清除文件按钮
|
|
513
|
-
*/
|
|
514
|
-
function setupClearFiles() {
|
|
515
|
-
const clearBtn = document.getElementById('clear-files');
|
|
516
|
-
|
|
517
|
-
clearBtn.addEventListener('click', () => {
|
|
518
|
-
clearSelectedFiles();
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* 清除已选文件
|
|
524
|
-
*/
|
|
525
|
-
function clearSelectedFiles() {
|
|
526
|
-
selectedZipBlob = null;
|
|
527
|
-
selectedFileName = '';
|
|
528
|
-
|
|
529
|
-
document.getElementById('skill-id').value = '';
|
|
530
|
-
document.getElementById('skill-name').value = '';
|
|
531
|
-
const descEl = document.getElementById('skill-description');
|
|
532
|
-
descEl.value = '';
|
|
533
|
-
const countEl = document.getElementById('skill-description-count');
|
|
534
|
-
if (countEl) countEl.textContent = '0';
|
|
535
|
-
|
|
536
|
-
const preview = document.getElementById('file-preview');
|
|
537
|
-
preview.classList.remove('visible');
|
|
538
|
-
|
|
539
|
-
const zipInput = document.getElementById('zip-file-input');
|
|
540
|
-
zipInput.value = '';
|
|
541
|
-
|
|
542
|
-
showToast('已清除选择', 'info');
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// ========================================
|
|
546
|
-
// 发布
|
|
547
|
-
// ========================================
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* 设置表单提交
|
|
551
|
-
*/
|
|
552
|
-
function setupFormSubmit() {
|
|
553
|
-
const form = document.getElementById('publishForm');
|
|
554
|
-
|
|
555
|
-
form.addEventListener('submit', async (e) => {
|
|
556
|
-
e.preventDefault();
|
|
557
|
-
await publish();
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/**
|
|
562
|
-
* 发布
|
|
563
|
-
*/
|
|
564
|
-
async function publish() {
|
|
565
|
-
// 验证文件
|
|
566
|
-
if (!selectedZipBlob) {
|
|
567
|
-
showToast('请先选择要上传的文件或目录', 'error');
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
const skillId = document.getElementById('skill-id').value.trim();
|
|
572
|
-
if (!skillId) {
|
|
573
|
-
showToast('无法从上传包得到 Skill ID,请使用合法文件夹名或 zip 文件名(小写字母、数字、连字符、下划线)', 'error');
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// 验证 Skill ID 格式
|
|
578
|
-
if (!/^[a-z0-9\-_]+$/.test(skillId)) {
|
|
579
|
-
showToast('Skill ID 只能包含小写字母、数字、下划线和连字符', 'error');
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
const skillSelect = document.getElementById('skill-select');
|
|
584
|
-
const selectedExistingId = skillSelect.value.trim();
|
|
585
|
-
if (selectedExistingId && selectedExistingId !== skillId) {
|
|
586
|
-
showToast('上传包的 Skill ID 与下拉框所选已有 Skill 不一致,请重新选择或更换压缩包', 'error');
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
if (isNewSkill) {
|
|
591
|
-
const name = document.getElementById('skill-name').value.trim();
|
|
592
|
-
if (!name) {
|
|
593
|
-
showToast('SKILL.md 中缺少可用的 Skill 名称(name 或首行标题)', 'error');
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
const desc = (document.getElementById('skill-description')?.value || '').trim();
|
|
597
|
-
if (!desc) {
|
|
598
|
-
showToast('SKILL.md 中缺少描述(description 或首段正文)', 'error');
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
if (desc.length > DESC_MAX) {
|
|
602
|
-
showToast(`描述不能超过 ${DESC_MAX} 字`, 'error');
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// 构建 FormData
|
|
608
|
-
const formData = new FormData();
|
|
609
|
-
formData.append('zip_file', selectedZipBlob, selectedFileName);
|
|
610
|
-
formData.append('skill_id', skillId);
|
|
611
|
-
|
|
612
|
-
if (isNewSkill) {
|
|
613
|
-
formData.append('name', document.getElementById('skill-name').value.trim());
|
|
614
|
-
formData.append('description', document.getElementById('skill-description').value.trim());
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
const changelog = document.getElementById('changelog').value.trim();
|
|
618
|
-
if (changelog) {
|
|
619
|
-
formData.append('changelog', changelog);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// 禁用提交按钮
|
|
623
|
-
const submitBtn = document.getElementById('submit-btn');
|
|
624
|
-
submitBtn.disabled = true;
|
|
625
|
-
submitBtn.textContent = '发布中...';
|
|
626
|
-
|
|
627
|
-
// 显示进度条
|
|
628
|
-
showProgress();
|
|
629
|
-
|
|
630
|
-
try {
|
|
631
|
-
const result = await apiUpload('/skills/publish', formData);
|
|
632
|
-
|
|
633
|
-
// 显示成功信息
|
|
634
|
-
showSuccess(result);
|
|
635
|
-
} catch (err) {
|
|
636
|
-
showToast('发布失败:' + err.message, 'error');
|
|
637
|
-
hideProgress();
|
|
638
|
-
submitBtn.disabled = false;
|
|
639
|
-
submitBtn.textContent = '发布新版本';
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* 显示进度
|
|
645
|
-
*/
|
|
646
|
-
function showProgress() {
|
|
647
|
-
const container = document.getElementById('progress-container');
|
|
648
|
-
const bar = document.getElementById('progress-bar');
|
|
649
|
-
const text = document.getElementById('progress-text');
|
|
650
|
-
|
|
651
|
-
container.classList.add('visible');
|
|
652
|
-
|
|
653
|
-
// 模拟进度动画
|
|
654
|
-
let progress = 0;
|
|
655
|
-
const interval = setInterval(() => {
|
|
656
|
-
progress += Math.random() * 15;
|
|
657
|
-
if (progress >= 90) {
|
|
658
|
-
progress = 90;
|
|
659
|
-
clearInterval(interval);
|
|
660
|
-
}
|
|
661
|
-
bar.style.width = progress + '%';
|
|
662
|
-
text.textContent = `Uploading... ${Math.round(progress)}%`;
|
|
663
|
-
}, 200);
|
|
664
|
-
|
|
665
|
-
// 保存 interval 以便后续清除
|
|
666
|
-
container.dataset.interval = interval;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
/**
|
|
670
|
-
* 隐藏进度
|
|
671
|
-
*/
|
|
672
|
-
function hideProgress() {
|
|
673
|
-
const container = document.getElementById('progress-container');
|
|
674
|
-
const bar = document.getElementById('progress-bar');
|
|
675
|
-
|
|
676
|
-
// 清除动画
|
|
677
|
-
if (container.dataset.interval) {
|
|
678
|
-
clearInterval(parseInt(container.dataset.interval));
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
bar.style.width = '0%';
|
|
682
|
-
container.classList.remove('visible');
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* 显示发布成功
|
|
687
|
-
*/
|
|
688
|
-
function showSuccess(result) {
|
|
689
|
-
// 完成进度条
|
|
690
|
-
const bar = document.getElementById('progress-bar');
|
|
691
|
-
const text = document.getElementById('progress-text');
|
|
692
|
-
const container = document.getElementById('progress-container');
|
|
693
|
-
|
|
694
|
-
// 清除动画
|
|
695
|
-
if (container.dataset.interval) {
|
|
696
|
-
clearInterval(parseInt(container.dataset.interval));
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
bar.style.width = '100%';
|
|
700
|
-
text.textContent = 'Upload complete!';
|
|
701
|
-
|
|
702
|
-
// 隐藏表单
|
|
703
|
-
const form = document.getElementById('publishForm');
|
|
704
|
-
form.style.display = 'none';
|
|
705
|
-
|
|
706
|
-
// 显示成功信息
|
|
707
|
-
const successContainer = document.getElementById('success-container');
|
|
708
|
-
const successMessage = document.getElementById('success-message');
|
|
709
|
-
const viewLink = document.getElementById('view-skill-link');
|
|
710
|
-
|
|
711
|
-
const version = result.version || result.version_number || 'v1.0.0';
|
|
712
|
-
const skillId = result.skill_id || document.getElementById('skill-id').value.trim();
|
|
713
|
-
|
|
714
|
-
successMessage.textContent = `Version ${version} published successfully`;
|
|
715
|
-
viewLink.href = `/skill.html?id=${encodeURIComponent(skillId)}`;
|
|
716
|
-
|
|
717
|
-
successContainer.classList.add('visible');
|
|
718
|
-
}
|