mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-04-21 10:16:32 +02:00
Add files via upload
This commit is contained in:
@@ -719,9 +719,18 @@
|
||||
"pathLabel": "Path:",
|
||||
"modTimeLabel": "Modified:",
|
||||
"contentLabel": "Content:",
|
||||
"cardVersion": "v{{version}}",
|
||||
"cardScripts": "{{count}} script(s)",
|
||||
"cardFiles": "{{count}} file(s)",
|
||||
"versionLabel": "Version:",
|
||||
"scriptsHeading": "Scripts:",
|
||||
"summaryHint": "(summary — load full body if needed)",
|
||||
"loadFullBody": "Load full body",
|
||||
"loadFullFailed": "Failed to load full skill body",
|
||||
"nameRequired": "Skill name is required",
|
||||
"contentRequired": "Skill content is required",
|
||||
"nameInvalid": "Skill name can only contain letters, numbers, hyphens and underscores",
|
||||
"nameInvalid": "Use lowercase letters, digits, and hyphens only (Agent Skills name rules)",
|
||||
"descriptionRequired": "Description is required (written to SKILL.md front matter)",
|
||||
"saveSuccess": "Skill updated",
|
||||
"createdSuccess": "Skill created",
|
||||
"deleteConfirm": "Are you sure you want to delete skill \"{{name}}\"? This cannot be undone.",
|
||||
@@ -1470,12 +1479,24 @@
|
||||
"editSkill": "Edit Skill",
|
||||
"skillName": "Skill name",
|
||||
"skillNamePlaceholder": "e.g. sql-injection-testing",
|
||||
"skillNameHint": "Letters, numbers, hyphens and underscores only",
|
||||
"skillNameHint": "Lowercase letters, digits, hyphens (Agent Skills name)",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "Short description",
|
||||
"contentLabel": "Content (Markdown)",
|
||||
"contentPlaceholder": "Enter skill content in Markdown...",
|
||||
"contentHint": "YAML front matter supported (optional), e.g.:"
|
||||
"descriptionHint": "Maps to the description field in SKILL.md YAML (when creating/editing SKILL.md)",
|
||||
"packageFiles": "Package files",
|
||||
"editingFile": "Editing",
|
||||
"newFile": "New file",
|
||||
"newFilePlaceholder": "Relative path, e.g. FORMS.md or scripts/extra.sh",
|
||||
"newFilePathRequired": "Enter a path for the new file",
|
||||
"newFilePathInvalid": "Invalid path (no .. or absolute paths)",
|
||||
"noPackageFiles": "No files listed",
|
||||
"unsavedSwitch": "You have unsaved changes. Switch file anyway?",
|
||||
"contentLabel": "Content",
|
||||
"contentPlaceholder": "Edit the selected file…",
|
||||
"contentPlaceholderAdd": "SKILL.md body only (front matter is generated)…",
|
||||
"bodyHintEdit": "For SKILL.md this is the body only (no --- header); save merges with name/description into standard SKILL.md.",
|
||||
"contentHintAdd": "Creates standard SKILL.md (YAML front matter + body). Drop open-source skill folders into skills/ as-is.",
|
||||
"contentHint": "See Claude Agent Skills format"
|
||||
},
|
||||
"knowledgeItemModal": {
|
||||
"addKnowledge": "Add knowledge",
|
||||
|
||||
@@ -719,9 +719,18 @@
|
||||
"pathLabel": "路径:",
|
||||
"modTimeLabel": "修改时间:",
|
||||
"contentLabel": "内容:",
|
||||
"cardVersion": "v{{version}}",
|
||||
"cardScripts": "{{count}} 个脚本",
|
||||
"cardFiles": "{{count}} 个文件",
|
||||
"versionLabel": "版本:",
|
||||
"scriptsHeading": "脚本:",
|
||||
"summaryHint": "(摘要 — 可加载完整正文)",
|
||||
"loadFullBody": "加载完整正文",
|
||||
"loadFullFailed": "加载完整正文失败",
|
||||
"nameRequired": "skill名称不能为空",
|
||||
"contentRequired": "skill内容不能为空",
|
||||
"nameInvalid": "skill名称只能包含字母、数字、连字符和下划线",
|
||||
"nameInvalid": "目录名须为小写字母、数字与连字符(与 Agent Skills 的 name 一致)",
|
||||
"descriptionRequired": "描述不能为空(将写入 SKILL.md 的 front matter)",
|
||||
"saveSuccess": "skill已更新",
|
||||
"createdSuccess": "skill已创建",
|
||||
"deleteConfirm": "确定要删除skill \"{{name}}\" 吗?此操作不可恢复。",
|
||||
@@ -1470,12 +1479,24 @@
|
||||
"editSkill": "编辑Skill",
|
||||
"skillName": "Skill名称",
|
||||
"skillNamePlaceholder": "例如: sql-injection-testing",
|
||||
"skillNameHint": "只能包含字母、数字、连字符和下划线",
|
||||
"skillNameHint": "小写字母、数字、连字符(与 Agent Skills 的 name 一致)",
|
||||
"description": "描述",
|
||||
"descriptionPlaceholder": "Skill的简短描述",
|
||||
"contentLabel": "内容(Markdown格式)",
|
||||
"contentPlaceholder": "输入skill内容,支持Markdown格式...",
|
||||
"contentHint": "支持YAML front matter格式(可选),例如:"
|
||||
"descriptionHint": "对应 SKILL.md 中 YAML 的 description 字段(创建/编辑 SKILL.md 时使用)",
|
||||
"packageFiles": "包内文件",
|
||||
"editingFile": "正在编辑",
|
||||
"newFile": "新建文件",
|
||||
"newFilePlaceholder": "新文件路径,如 FORMS.md 或 scripts/extra.sh",
|
||||
"newFilePathRequired": "请填写新文件路径",
|
||||
"newFilePathInvalid": "路径无效(禁止 .. 与绝对路径)",
|
||||
"noPackageFiles": "未列出文件",
|
||||
"unsavedSwitch": "当前文件有未保存修改,确定切换?",
|
||||
"contentLabel": "内容",
|
||||
"contentPlaceholder": "编辑当前选中的文件…",
|
||||
"contentPlaceholderAdd": "SKILL.md 正文(无需手写 YAML 头)…",
|
||||
"bodyHintEdit": "当前为 SKILL.md 的正文部分(不含 --- 头);保存时会合并名称/描述生成标准 SKILL.md。",
|
||||
"contentHintAdd": "保存后生成标准 SKILL.md(YAML front matter + 正文)。开源技能目录可直接放入 skills/ 使用。",
|
||||
"contentHint": "标准格式见 Claude Agent Skills 文档"
|
||||
},
|
||||
"knowledgeItemModal": {
|
||||
"addKnowledge": "添加知识",
|
||||
|
||||
+307
-60
@@ -4,6 +4,11 @@ function _t(key, opts) {
|
||||
}
|
||||
let skillsList = [];
|
||||
let currentEditingSkillName = null;
|
||||
let skillModalAddMode = true;
|
||||
let skillActivePath = 'SKILL.md';
|
||||
let skillFileDirty = false;
|
||||
let skillPackageFiles = [];
|
||||
let skillModalControlsWired = false;
|
||||
let isSavingSkill = false; // 防止重复提交
|
||||
let skillsSearchKeyword = '';
|
||||
let skillsSearchTimeout = null; // 搜索防抖定时器
|
||||
@@ -154,20 +159,40 @@ function renderSkillsList() {
|
||||
}
|
||||
|
||||
skillsListEl.innerHTML = filteredSkills.map(skill => {
|
||||
const sid = skill.id || skill.name || '';
|
||||
const ver = skill.version ? _t('skills.cardVersion', { version: skill.version }) : '';
|
||||
const sc = typeof skill.script_count === 'number' && skill.script_count > 0
|
||||
? _t('skills.cardScripts', { count: skill.script_count })
|
||||
: '';
|
||||
const fc = typeof skill.file_count === 'number' && skill.file_count > 0
|
||||
? _t('skills.cardFiles', { count: skill.file_count })
|
||||
: '';
|
||||
const meta = [ver, fc, sc].filter(Boolean).join(' · ');
|
||||
return `
|
||||
<div class="skill-card">
|
||||
<div class="skill-card-header">
|
||||
<h3 class="skill-card-title">${escapeHtml(skill.name || '')}</h3>
|
||||
<h3 class="skill-card-title">${escapeHtml(skill.name || sid)}</h3>
|
||||
${meta ? `<div class="skill-card-meta" style="opacity:0.85;font-size:12px;margin-top:4px;">${escapeHtml(meta)}</div>` : ''}
|
||||
<div class="skill-card-description">${escapeHtml(skill.description || _t('skills.noDescription'))}</div>
|
||||
</div>
|
||||
<div class="skill-card-actions">
|
||||
<button class="btn-secondary btn-small" onclick="viewSkill('${escapeHtml(skill.name)}')">${_t('common.view')}</button>
|
||||
<button class="btn-secondary btn-small" onclick="editSkill('${escapeHtml(skill.name)}')">${_t('common.edit')}</button>
|
||||
<button class="btn-secondary btn-small btn-danger" onclick="deleteSkill('${escapeHtml(skill.name)}')">${_t('common.delete')}</button>
|
||||
<button type="button" class="btn-secondary btn-small" data-skill-view="${escapeHtml(sid)}">${_t('common.view')}</button>
|
||||
<button type="button" class="btn-secondary btn-small" data-skill-edit="${escapeHtml(sid)}">${_t('common.edit')}</button>
|
||||
<button type="button" class="btn-secondary btn-small btn-danger" data-skill-delete="${escapeHtml(sid)}">${_t('common.delete')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
skillsListEl.querySelectorAll('[data-skill-view]').forEach(btn => {
|
||||
btn.addEventListener('click', () => viewSkill(btn.getAttribute('data-skill-view')));
|
||||
});
|
||||
skillsListEl.querySelectorAll('[data-skill-edit]').forEach(btn => {
|
||||
btn.addEventListener('click', () => editSkill(btn.getAttribute('data-skill-edit')));
|
||||
});
|
||||
skillsListEl.querySelectorAll('[data-skill-delete]').forEach(btn => {
|
||||
btn.addEventListener('click', () => deleteSkill(btn.getAttribute('data-skill-delete')));
|
||||
});
|
||||
|
||||
// 确保列表容器可以滚动,分页栏可见
|
||||
// 使用 setTimeout 确保 DOM 更新完成后再检查
|
||||
@@ -392,39 +417,174 @@ async function refreshSkills() {
|
||||
}
|
||||
|
||||
// 显示添加skill模态框
|
||||
function wireSkillModalOnce() {
|
||||
if (skillModalControlsWired) return;
|
||||
skillModalControlsWired = true;
|
||||
const addTa = document.getElementById('skill-content-add');
|
||||
const edTa = document.getElementById('skill-content');
|
||||
if (addTa) addTa.addEventListener('input', () => { if (skillModalAddMode) skillFileDirty = true; });
|
||||
if (edTa) edTa.addEventListener('input', () => { if (!skillModalAddMode) skillFileDirty = true; });
|
||||
const nb = document.getElementById('skill-new-file-btn');
|
||||
if (nb) {
|
||||
nb.addEventListener('click', () => {
|
||||
if (!currentEditingSkillName) return;
|
||||
const inp = document.getElementById('skill-new-file-path');
|
||||
const p = (inp && inp.value || '').trim();
|
||||
if (!p) {
|
||||
showNotification(_t('skillModal.newFilePathRequired'), 'error');
|
||||
return;
|
||||
}
|
||||
if (p.includes('..') || p.startsWith('/')) {
|
||||
showNotification(_t('skillModal.newFilePathInvalid'), 'error');
|
||||
return;
|
||||
}
|
||||
selectSkillPackageFile(currentEditingSkillName, p, { force: true, freshContent: '' });
|
||||
if (inp) inp.value = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showAddSkillModal() {
|
||||
wireSkillModalOnce();
|
||||
const modal = document.getElementById('skill-modal');
|
||||
if (!modal) return;
|
||||
|
||||
skillModalAddMode = true;
|
||||
skillFileDirty = false;
|
||||
skillActivePath = 'SKILL.md';
|
||||
skillPackageFiles = [];
|
||||
const pkg = document.getElementById('skill-package-editor');
|
||||
const addEd = document.getElementById('skill-add-editor');
|
||||
if (pkg) pkg.style.display = 'none';
|
||||
if (addEd) addEd.style.display = 'block';
|
||||
|
||||
document.getElementById('skill-modal-title').textContent = _t('skills.addSkill');
|
||||
document.getElementById('skill-name').value = '';
|
||||
document.getElementById('skill-name').disabled = false;
|
||||
document.getElementById('skill-description').value = '';
|
||||
document.getElementById('skill-content').value = '';
|
||||
|
||||
const addTa = document.getElementById('skill-content-add');
|
||||
if (addTa) addTa.value = '';
|
||||
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
// 编辑skill
|
||||
async function editSkill(skillName) {
|
||||
function renderSkillPackageTree() {
|
||||
const el = document.getElementById('skill-package-tree');
|
||||
if (!el) return;
|
||||
const rows = (skillPackageFiles || []).filter(f => f.path && f.path !== '.').sort((a, b) =>
|
||||
String(a.path).localeCompare(String(b.path)));
|
||||
if (rows.length === 0) {
|
||||
el.innerHTML = '<div class="empty-state" style="padding:8px;">' + escapeHtml(_t('skillModal.noPackageFiles')) + '</div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = rows.map(f => {
|
||||
const path = f.path || '';
|
||||
if (f.is_dir) {
|
||||
return `<div style="padding:4px 6px;opacity:0.85;font-weight:600;">${escapeHtml(path)}/</div>`;
|
||||
}
|
||||
const sel = path === skillActivePath
|
||||
? 'font-weight:600;background:rgba(99,102,241,0.12);'
|
||||
: '';
|
||||
return `<div style="padding:4px 6px;cursor:pointer;border-radius:4px;margin-bottom:2px;${sel}" data-skill-tree-path="${escapeHtml(path)}" class="skill-tree-item">${escapeHtml(path)}</div>`;
|
||||
}).join('');
|
||||
el.querySelectorAll('[data-skill-tree-path]').forEach(node => {
|
||||
node.addEventListener('click', () => {
|
||||
const p = node.getAttribute('data-skill-tree-path');
|
||||
if (p) selectSkillPackageFile(currentEditingSkillName, p, {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function selectSkillPackageFile(skillId, path, opts) {
|
||||
const force = opts && opts.force;
|
||||
const freshContent = opts && Object.prototype.hasOwnProperty.call(opts, 'freshContent')
|
||||
? opts.freshContent
|
||||
: null;
|
||||
if (!force && skillFileDirty) {
|
||||
if (!confirm(_t('skillModal.unsavedSwitch'))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
skillActivePath = path;
|
||||
const label = document.getElementById('skill-active-path');
|
||||
if (label) label.textContent = path;
|
||||
const hint = document.getElementById('skill-body-hint-edit');
|
||||
if (hint) hint.style.display = path === 'SKILL.md' ? 'block' : 'none';
|
||||
const ta = document.getElementById('skill-content');
|
||||
if (!ta) return;
|
||||
|
||||
if (freshContent !== null) {
|
||||
ta.value = freshContent;
|
||||
skillFileDirty = true;
|
||||
renderSkillPackageTree();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiFetch(`/api/skills/${encodeURIComponent(skillName)}`);
|
||||
if (!response.ok) {
|
||||
if (path === 'SKILL.md') {
|
||||
const response = await apiFetch(`/api/skills/${encodeURIComponent(skillId)}?depth=full`);
|
||||
if (!response.ok) throw new Error(_t('skills.loadDetailFailed'));
|
||||
const data = await response.json();
|
||||
const skill = data.skill;
|
||||
ta.value = skill && skill.content != null ? skill.content : '';
|
||||
} else {
|
||||
const response = await apiFetch(`/api/skills/${encodeURIComponent(skillId)}/file?path=${encodeURIComponent(path)}`);
|
||||
if (!response.ok) throw new Error(_t('skills.loadDetailFailed'));
|
||||
const data = await response.json();
|
||||
ta.value = data.content != null ? data.content : '';
|
||||
}
|
||||
skillFileDirty = false;
|
||||
renderSkillPackageTree();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showNotification(_t('skills.loadDetailFailed') + ': ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑skill
|
||||
async function editSkill(skillId) {
|
||||
wireSkillModalOnce();
|
||||
try {
|
||||
const [detailRes, filesRes] = await Promise.all([
|
||||
apiFetch(`/api/skills/${encodeURIComponent(skillId)}?depth=full`),
|
||||
apiFetch(`/api/skills/${encodeURIComponent(skillId)}/files`)
|
||||
]);
|
||||
if (!detailRes.ok) {
|
||||
throw new Error(_t('skills.loadDetailFailed'));
|
||||
}
|
||||
const data = await response.json();
|
||||
const data = await detailRes.json();
|
||||
const skill = data.skill;
|
||||
|
||||
const modal = document.getElementById('skill-modal');
|
||||
if (!modal) return;
|
||||
|
||||
skillModalAddMode = false;
|
||||
skillFileDirty = false;
|
||||
skillActivePath = 'SKILL.md';
|
||||
const pkg = document.getElementById('skill-package-editor');
|
||||
const addEd = document.getElementById('skill-add-editor');
|
||||
if (pkg) pkg.style.display = 'block';
|
||||
if (addEd) addEd.style.display = 'none';
|
||||
|
||||
document.getElementById('skill-modal-title').textContent = _t('skills.editSkill');
|
||||
document.getElementById('skill-name').value = skill.name;
|
||||
document.getElementById('skill-name').disabled = true; // 编辑时不允许修改名称
|
||||
document.getElementById('skill-name').value = skill.id || skillId;
|
||||
document.getElementById('skill-name').disabled = true;
|
||||
document.getElementById('skill-description').value = skill.description || '';
|
||||
document.getElementById('skill-content').value = skill.content || '';
|
||||
|
||||
currentEditingSkillName = skillName;
|
||||
|
||||
if (filesRes.ok) {
|
||||
const fd = await filesRes.json();
|
||||
skillPackageFiles = fd.files || [];
|
||||
} else {
|
||||
skillPackageFiles = [];
|
||||
}
|
||||
renderSkillPackageTree();
|
||||
|
||||
const ta = document.getElementById('skill-content');
|
||||
if (ta) ta.value = skill.content || '';
|
||||
const hint = document.getElementById('skill-body-hint-edit');
|
||||
if (hint) hint.style.display = 'block';
|
||||
|
||||
currentEditingSkillName = skillId;
|
||||
modal.style.display = 'flex';
|
||||
} catch (error) {
|
||||
console.error('加载skill详情失败:', error);
|
||||
@@ -432,48 +592,86 @@ async function editSkill(skillName) {
|
||||
}
|
||||
}
|
||||
|
||||
// 查看skill
|
||||
async function viewSkill(skillName) {
|
||||
// 查看 skill:先摘要再按需拉全文(与多代理 Eino skill 渐进披露思路一致)
|
||||
async function viewSkill(skillId) {
|
||||
try {
|
||||
const response = await apiFetch(`/api/skills/${encodeURIComponent(skillName)}`);
|
||||
if (!response.ok) {
|
||||
const sumRes = await apiFetch(`/api/skills/${encodeURIComponent(skillId)}?depth=summary`);
|
||||
if (!sumRes.ok) {
|
||||
throw new Error(_t('skills.loadDetailFailed'));
|
||||
}
|
||||
const data = await response.json();
|
||||
const skill = data.skill;
|
||||
const sumData = await sumRes.json();
|
||||
const sumSkill = sumData.skill;
|
||||
|
||||
// 创建查看模态框
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal';
|
||||
modal.id = 'skill-view-modal';
|
||||
const viewTitle = _t('skills.viewSkillTitle', { name: skill.name });
|
||||
const viewTitle = _t('skills.viewSkillTitle', { name: sumSkill.name || skillId });
|
||||
const descLabel = _t('skills.descriptionLabel');
|
||||
const pathLabel = _t('skills.pathLabel');
|
||||
const modTimeLabel = _t('skills.modTimeLabel');
|
||||
const contentLabel = _t('skills.contentLabel');
|
||||
const closeBtn = _t('common.close');
|
||||
const editBtn = _t('common.edit');
|
||||
const loadFullLabel = _t('skills.loadFullBody');
|
||||
const scriptsLabel = _t('skills.scriptsHeading');
|
||||
|
||||
let scriptsBlock = '';
|
||||
if (Array.isArray(sumSkill.scripts) && sumSkill.scripts.length > 0) {
|
||||
const lines = sumSkill.scripts.map(s => {
|
||||
const rel = escapeHtml(s.rel_path || s.RelPath || '');
|
||||
const dn = escapeHtml(s.description || s.Description || '');
|
||||
return `<li><code>${rel}</code>${dn ? ' — ' + dn : ''}</li>`;
|
||||
}).join('');
|
||||
scriptsBlock = `<div style="margin-bottom: 16px;"><strong>${escapeHtml(scriptsLabel)}</strong><ul style="margin:8px 0 0 18px;">${lines}</ul></div>`;
|
||||
}
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content" style="max-width: 900px; max-height: 90vh;">
|
||||
<div class="modal-header">
|
||||
<h2>${escapeHtml(viewTitle)}</h2>
|
||||
<span class="modal-close" onclick="closeSkillViewModal()">×</span>
|
||||
<span class="modal-close" data-skill-view-close>×</span>
|
||||
</div>
|
||||
<div class="modal-body" style="overflow-y: auto; max-height: calc(90vh - 120px);">
|
||||
${skill.description ? `<div style="margin-bottom: 16px;"><strong>${escapeHtml(descLabel)}</strong> ${escapeHtml(skill.description)}</div>` : ''}
|
||||
<div style="margin-bottom: 8px;"><strong>${escapeHtml(pathLabel)}</strong> ${escapeHtml(skill.path || '')}</div>
|
||||
<div style="margin-bottom: 16px;"><strong>${escapeHtml(modTimeLabel)}</strong> ${escapeHtml(skill.mod_time || '')}</div>
|
||||
<div style="margin-bottom: 8px;"><strong>${escapeHtml(contentLabel)}</strong></div>
|
||||
<pre style="background: #f5f5f5; padding: 16px; border-radius: 4px; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;">${escapeHtml(skill.content || '')}</pre>
|
||||
${sumSkill.version ? `<div style="margin-bottom: 8px;"><strong>${escapeHtml(_t('skills.versionLabel'))}</strong> ${escapeHtml(sumSkill.version)}</div>` : ''}
|
||||
${sumSkill.description ? `<div style="margin-bottom: 16px;"><strong>${escapeHtml(descLabel)}</strong> ${escapeHtml(sumSkill.description)}</div>` : ''}
|
||||
${scriptsBlock}
|
||||
<div style="margin-bottom: 8px;"><strong>${escapeHtml(pathLabel)}</strong> ${escapeHtml(sumSkill.path || '')}</div>
|
||||
<div style="margin-bottom: 16px;"><strong>${escapeHtml(modTimeLabel)}</strong> ${escapeHtml(sumSkill.mod_time || '')}</div>
|
||||
<div style="margin-bottom: 8px;"><strong>${escapeHtml(contentLabel)}</strong> <span style="opacity:0.8;font-size:12px;">${escapeHtml(_t('skills.summaryHint'))}</span></div>
|
||||
<pre id="skill-view-body" style="background: #f5f5f5; padding: 16px; border-radius: 4px; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;">${escapeHtml(sumSkill.content || '')}</pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="closeSkillViewModal()">${escapeHtml(closeBtn)}</button>
|
||||
<button class="btn-primary" onclick="editSkill('${escapeHtml(skill.name)}'); closeSkillViewModal();">${escapeHtml(editBtn)}</button>
|
||||
<button type="button" class="btn-secondary" data-skill-load-full>${escapeHtml(loadFullLabel)}</button>
|
||||
<button type="button" class="btn-secondary" data-skill-view-close>${escapeHtml(closeBtn)}</button>
|
||||
<button type="button" class="btn-primary" data-skill-view-edit>${escapeHtml(editBtn)}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
modal.style.display = 'flex';
|
||||
|
||||
const close = () => closeSkillViewModal();
|
||||
modal.querySelectorAll('[data-skill-view-close]').forEach(el => el.addEventListener('click', close));
|
||||
modal.querySelector('[data-skill-view-edit]').addEventListener('click', () => {
|
||||
close();
|
||||
editSkill(skillId);
|
||||
});
|
||||
modal.querySelector('[data-skill-load-full]').addEventListener('click', async () => {
|
||||
const pre = modal.querySelector('#skill-view-body');
|
||||
const btn = modal.querySelector('[data-skill-load-full]');
|
||||
if (!pre || !btn) return;
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const fullRes = await apiFetch(`/api/skills/${encodeURIComponent(skillId)}?depth=full`);
|
||||
if (!fullRes.ok) throw new Error(_t('skills.loadDetailFailed'));
|
||||
const fullData = await fullRes.json();
|
||||
pre.textContent = fullData.skill && fullData.skill.content != null ? fullData.skill.content : '';
|
||||
} catch (e) {
|
||||
showNotification(_t('skills.loadFullFailed') + ': ' + e.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('查看skill失败:', error);
|
||||
showNotification(_t('skills.viewFailed') + ': ' + error.message, 'error');
|
||||
@@ -494,6 +692,10 @@ function closeSkillModal() {
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
currentEditingSkillName = null;
|
||||
skillModalAddMode = true;
|
||||
skillFileDirty = false;
|
||||
skillPackageFiles = [];
|
||||
skillActivePath = 'SKILL.md';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,22 +705,28 @@ async function saveSkill() {
|
||||
|
||||
const name = document.getElementById('skill-name').value.trim();
|
||||
const description = document.getElementById('skill-description').value.trim();
|
||||
const content = document.getElementById('skill-content').value.trim();
|
||||
|
||||
if (!name) {
|
||||
showNotification(_t('skills.nameRequired'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
showNotification(_t('skills.contentRequired'), 'error');
|
||||
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name)) {
|
||||
showNotification(_t('skills.nameInvalid'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证skill名称
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
||||
showNotification(_t('skills.nameInvalid'), 'error');
|
||||
return;
|
||||
if (skillModalAddMode || !currentEditingSkillName) {
|
||||
if (!description) {
|
||||
showNotification(_t('skills.descriptionRequired'), 'error');
|
||||
return;
|
||||
}
|
||||
const content = (document.getElementById('skill-content-add') || {}).value;
|
||||
const body = (content || '').trim();
|
||||
if (!body) {
|
||||
showNotification(_t('skills.contentRequired'), 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
isSavingSkill = true;
|
||||
@@ -529,29 +737,64 @@ async function saveSkill() {
|
||||
}
|
||||
|
||||
try {
|
||||
const isEdit = !!currentEditingSkillName;
|
||||
const url = isEdit ? `/api/skills/${encodeURIComponent(currentEditingSkillName)}` : '/api/skills';
|
||||
const method = isEdit ? 'PUT' : 'POST';
|
||||
|
||||
const response = await apiFetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
description: description,
|
||||
content: content
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || _t('skills.saveFailed'));
|
||||
if (skillModalAddMode || !currentEditingSkillName) {
|
||||
const content = (document.getElementById('skill-content-add') || {}).value;
|
||||
const body = (content || '').trim();
|
||||
const response = await apiFetch('/api/skills', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, description, content: body })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || _t('skills.saveFailed'));
|
||||
}
|
||||
showNotification(_t('skills.createdSuccess'), 'success');
|
||||
closeSkillModal();
|
||||
await loadSkills(skillsPagination.currentPage, skillsPagination.pageSize);
|
||||
return;
|
||||
}
|
||||
|
||||
showNotification(isEdit ? _t('skills.saveSuccess') : _t('skills.createdSuccess'), 'success');
|
||||
closeSkillModal();
|
||||
const path = skillActivePath || 'SKILL.md';
|
||||
const ta = document.getElementById('skill-content');
|
||||
const raw = ta ? ta.value : '';
|
||||
if (path === 'SKILL.md') {
|
||||
if (!raw.trim()) {
|
||||
showNotification(_t('skills.contentRequired'), 'error');
|
||||
return;
|
||||
}
|
||||
const response = await apiFetch(`/api/skills/${encodeURIComponent(currentEditingSkillName)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
description: description,
|
||||
content: raw.trim()
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || _t('skills.saveFailed'));
|
||||
}
|
||||
} else {
|
||||
const response = await apiFetch(`/api/skills/${encodeURIComponent(currentEditingSkillName)}/file`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: path, content: raw })
|
||||
});
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || _t('skills.saveFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
skillFileDirty = false;
|
||||
showNotification(_t('skills.saveSuccess'), 'success');
|
||||
const filesRes = await apiFetch(`/api/skills/${encodeURIComponent(currentEditingSkillName)}/files`);
|
||||
if (filesRes.ok) {
|
||||
const fd = await filesRes.json();
|
||||
skillPackageFiles = fd.files || [];
|
||||
renderSkillPackageTree();
|
||||
}
|
||||
await loadSkills(skillsPagination.currentPage, skillsPagination.pageSize);
|
||||
} catch (error) {
|
||||
console.error('保存skill失败:', error);
|
||||
@@ -795,6 +1038,10 @@ document.addEventListener('languagechange', function () {
|
||||
renderSkillsPagination();
|
||||
}
|
||||
}
|
||||
const pkg = document.getElementById('skill-package-editor');
|
||||
if (pkg && pkg.style.display !== 'none' && currentEditingSkillName) {
|
||||
renderSkillPackageTree();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
|
||||
+27
-15
@@ -2114,7 +2114,7 @@
|
||||
<!-- 知识项编辑模态框 -->
|
||||
<!-- Skill模态框 -->
|
||||
<div id="skill-modal" class="modal">
|
||||
<div class="modal-content" style="max-width: 900px;">
|
||||
<div class="modal-content" style="max-width: 1100px;">
|
||||
<div class="modal-header">
|
||||
<h2 id="skill-modal-title" data-i18n="skillModal.addSkill">添加Skill</h2>
|
||||
<span class="modal-close" onclick="closeSkillModal()">×</span>
|
||||
@@ -2123,23 +2123,35 @@
|
||||
<div class="form-group">
|
||||
<label for="skill-name"><span data-i18n="skillModal.skillName">Skill名称</span> <span style="color: red;">*</span></label>
|
||||
<input type="text" id="skill-name" data-i18n="skillModal.skillNamePlaceholder" data-i18n-attr="placeholder" placeholder="例如: sql-injection-testing" required />
|
||||
<small class="form-hint" data-i18n="skillModal.skillNameHint">只能包含字母、数字、连字符和下划线</small>
|
||||
<small class="form-hint" data-i18n="skillModal.skillNameHint">小写字母、数字、连字符(与 Agent Skills 的 name 一致)</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="skill-description" data-i18n="skillModal.description">描述</label>
|
||||
<input type="text" id="skill-description" data-i18n="skillModal.descriptionPlaceholder" data-i18n-attr="placeholder" placeholder="Skill的简短描述" />
|
||||
<label for="skill-description"><span data-i18n="skillModal.description">描述</span> <span style="color: red;">*</span></label>
|
||||
<input type="text" id="skill-description" data-i18n="skillModal.descriptionPlaceholder" data-i18n-attr="placeholder" placeholder="Skill的简短描述" required />
|
||||
<small class="form-hint" data-i18n="skillModal.descriptionHint">写入 SKILL.md 的 YAML front matter(description 字段)</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="skill-content"><span data-i18n="skillModal.contentLabel">内容(Markdown格式)</span> <span style="color: red;">*</span></label>
|
||||
<textarea id="skill-content" rows="20" data-i18n="skillModal.contentPlaceholder" data-i18n-attr="placeholder" placeholder="输入skill内容,支持Markdown格式..." style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.875rem; line-height: 1.5;" required></textarea>
|
||||
<small class="form-hint"><span data-i18n="skillModal.contentHint">支持YAML front matter格式(可选),例如:</span><br>
|
||||
---<br>
|
||||
name: skill-name<br>
|
||||
description: Skill描述<br>
|
||||
version: 1.0.0<br>
|
||||
---<br><br>
|
||||
# Skill标题<br>
|
||||
这里是skill内容...</small>
|
||||
<div class="form-group" id="skill-package-editor" style="display: none;">
|
||||
<label data-i18n="skillModal.packageFiles">包内文件(标准 Agent Skills 布局)</label>
|
||||
<div style="display: flex; gap: 12px; align-items: flex-start; min-height: 300px;">
|
||||
<div id="skill-package-tree" style="flex: 0 0 240px; max-height: 440px; overflow: auto; border: 1px solid rgba(127,127,127,0.25); border-radius: 6px; padding: 8px; font-size: 13px; line-height: 1.4;"></div>
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<div style="margin-bottom: 8px; font-size: 13px;">
|
||||
<span data-i18n="skillModal.editingFile">正在编辑</span> <code id="skill-active-path">SKILL.md</code>
|
||||
</div>
|
||||
<div id="skill-new-file-row" style="display: flex; gap: 8px; margin-bottom: 8px; align-items: center;">
|
||||
<input type="text" id="skill-new-file-path" data-i18n="skillModal.newFilePlaceholder" data-i18n-attr="placeholder" placeholder="新文件路径,如 FORMS.md" style="flex: 1;" />
|
||||
<button type="button" class="btn-secondary btn-small" id="skill-new-file-btn" data-i18n="skillModal.newFile">新建文件</button>
|
||||
</div>
|
||||
<label for="skill-content"><span data-i18n="skillModal.contentLabel">内容</span> <span style="color: red;">*</span></label>
|
||||
<textarea id="skill-content" rows="18" data-i18n="skillModal.contentPlaceholder" data-i18n-attr="placeholder" placeholder="输入skill内容,支持Markdown格式..." style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.875rem; line-height: 1.5; width: 100%; box-sizing: border-box;" required></textarea>
|
||||
<small class="form-hint" id="skill-body-hint-edit" data-i18n="skillModal.bodyHintEdit" style="display: none;">选择 SKILL.md 时,此处为正文(不含 YAML 头);保存时与上方描述一并写回标准 SKILL.md。</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" id="skill-add-editor">
|
||||
<label for="skill-content-add"><span data-i18n="skillModal.contentLabel">正文(Markdown)</span> <span style="color: red;">*</span></label>
|
||||
<textarea id="skill-content-add" rows="18" data-i18n="skillModal.contentPlaceholderAdd" data-i18n-attr="placeholder" placeholder="SKILL.md 正文(无需手写 front matter,由名称与描述自动生成)..." style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.875rem; line-height: 1.5; width: 100%; box-sizing: border-box;" required></textarea>
|
||||
<small class="form-hint" data-i18n="skillModal.contentHintAdd">创建时只需填写正文;保存后生成标准 SKILL.md(含 YAML 头)。开源技能包可直接放入 skills/<name>/ 使用。</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
Reference in New Issue
Block a user