import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { ensurePublishSkillInstalled, PUBLISH_SKILL_NAME, renderPublishSkill, resolvePublicBaseUrl, } from './user-publish.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); export const DEFAULT_USER_SKILLS = { web: true, search: true, 'schedule-assistant': true, 'service-integration-smoke': true, 'form-builder': true, 'table-viewer': true, 'product-campaign-page': true, 'docx-generate': true, 'long-image-download': true, [PUBLISH_SKILL_NAME]: false, }; /** Optional presets for admin role configuration (creator / developer). */ export const USER_ROLE_SKILL_PRESETS = { user: DEFAULT_USER_SKILLS, creator: { ...DEFAULT_USER_SKILLS, [PUBLISH_SKILL_NAME]: true, kanban: true, timeline: true, }, developer: { ...DEFAULT_USER_SKILLS, git: true, 'diff-viewer': true, 'code-playground': true, 'test-runner': true, }, }; export function parseSkillFrontmatter(content) { const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (!match) return { name: null, description: '' }; const block = match[1]; const name = block.match(/^name:\s*(.+)$/m)?.[1]?.trim() ?? null; const description = block.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? ''; return { name, description }; } export function listPlatformSkillCatalog(h5Root = __dirname) { const skillsRoot = path.join(h5Root, 'skills'); if (!fs.existsSync(skillsRoot)) return []; const catalog = []; for (const entry of fs.readdirSync(skillsRoot, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const skillPath = path.join(skillsRoot, entry.name, 'SKILL.md'); if (!fs.existsSync(skillPath)) continue; const raw = fs.readFileSync(skillPath, 'utf8'); const meta = parseSkillFrontmatter(raw); const name = meta.name || entry.name; catalog.push({ name, dirName: entry.name, label: name, description: meta.description || '平台通用技能', category: 'platform', requiresPublish: name === PUBLISH_SKILL_NAME, }); } return catalog.sort((a, b) => a.name.localeCompare(b.name)); } export function catalogSkillNames(catalog) { return catalog.map((item) => item.name); } export function isValidSkillName(catalog, name) { return catalog.some((item) => item.name === name); } export function normalizeSkillPatch(catalog, patch) { const normalized = {}; for (const [key, value] of Object.entries(patch ?? {})) { if (!isValidSkillName(catalog, key)) continue; normalized[key] = Boolean(value); } return normalized; } export function resolveSkillMap(roleGrants, userOverrides, catalog) { const resolved = {}; for (const item of catalog) { const key = item.name; if (key in userOverrides) { resolved[key] = userOverrides[key]; } else if (key in roleGrants) { resolved[key] = roleGrants[key]; } else { resolved[key] = DEFAULT_USER_SKILLS[key] ?? false; } } return resolved; } export function grantedSkillNames(skillMap) { return Object.entries(skillMap) .filter(([, enabled]) => enabled) .map(([name]) => name); } export function applySkillGrantsToCapabilities(capabilities, skillMap) { const effective = { ...capabilities }; const enabled = grantedSkillNames(skillMap); if (enabled.length > 0) { effective.skills = true; } if (enabled.includes(PUBLISH_SKILL_NAME)) { effective.static_publish = true; } return effective; } function copySkillTree(srcDir, destDir) { fs.mkdirSync(destDir, { recursive: true }); for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) { const from = path.join(srcDir, entry.name); const to = path.join(destDir, entry.name); if (entry.isDirectory()) { copySkillTree(from, to); } else { fs.copyFileSync(from, to); } } } export function syncSkillsToWorkspace({ h5Root, publishDir, skillMap, catalog, user, publicBaseUrl, }) { const enabled = new Set(grantedSkillNames(skillMap)); const platformNames = new Set(catalog.map((item) => item.name)); const agentsSkills = path.join(publishDir, '.agents', 'skills'); if (fs.existsSync(agentsSkills)) { for (const entry of fs.readdirSync(agentsSkills, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const skillName = entry.name; const catalogItem = catalog.find((item) => item.name === skillName || item.dirName === skillName); const resolvedName = catalogItem?.name ?? skillName; if (platformNames.has(resolvedName) && !enabled.has(resolvedName)) { fs.rmSync(path.join(agentsSkills, entry.name), { recursive: true, force: true }); } } } for (const item of catalog) { if (!enabled.has(item.name)) continue; const srcDir = path.join(h5Root, 'skills', item.dirName); const destDir = path.join(agentsSkills, item.name); if (item.name === PUBLISH_SKILL_NAME && user) { ensurePublishSkillInstalled(publishDir, { slug: String(user.id).trim().toLowerCase(), username: user.username ? String(user.username).trim().toLowerCase() : undefined, publicBaseUrl, publishDir, }); continue; } if (fs.existsSync(srcDir)) { if (fs.existsSync(destDir)) fs.rmSync(destDir, { recursive: true, force: true }); copySkillTree(srcDir, destDir); } } }