From 5c444afe06737912f7958e40c4ebb9be48d53dde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Sun, 19 Apr 2026 01:13:31 +0800 Subject: [PATCH] Add files via upload --- web/static/i18n/en-US.json | 31 +++- web/static/i18n/zh-CN.json | 31 +++- web/static/js/skills.js | 367 +++++++++++++++++++++++++++++++------ web/templates/index.html | 42 +++-- 4 files changed, 386 insertions(+), 85 deletions(-) diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 698726d7..d0e9733e 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -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", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index fcbc6d5b..08b4cf1c 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -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": "添加知识", diff --git a/web/static/js/skills.js b/web/static/js/skills.js index ca47719b..c8ea1456 100644 --- a/web/static/js/skills.js +++ b/web/static/js/skills.js @@ -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 `
-

${escapeHtml(skill.name || '')}

+

${escapeHtml(skill.name || sid)}

+ ${meta ? `
${escapeHtml(meta)}
` : ''}
${escapeHtml(skill.description || _t('skills.noDescription'))}
- - - + + +
`; }).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 = '
' + escapeHtml(_t('skillModal.noPackageFiles')) + '
'; + return; + } + el.innerHTML = rows.map(f => { + const path = f.path || ''; + if (f.is_dir) { + return `
${escapeHtml(path)}/
`; + } + const sel = path === skillActivePath + ? 'font-weight:600;background:rgba(99,102,241,0.12);' + : ''; + return `
${escapeHtml(path)}
`; + }).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 `
  • ${rel}${dn ? ' — ' + dn : ''}
  • `; + }).join(''); + scriptsBlock = `
    ${escapeHtml(scriptsLabel)}
    `; + } + modal.innerHTML = ` `; 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 () { diff --git a/web/templates/index.html b/web/templates/index.html index b9785450..fe23f33d 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -2114,7 +2114,7 @@