/** * Open API Routes — Skill-as-a-Service 对外接口 * * 公开端点: install.md, SKILL.md, docs, blog * 认证端点: skills/search, skills/:name, skills/download, skills/create, jobs, feedback, updates */ const express = require('express'); const fs = require('fs'); const path = require('path'); const router = express.Router(); const { requireApiKey } = require('../middleware/api-key-auth'); const { apiAccessLogger } = require('../middleware/api-access-logger'); const { SkillRegistry } = require('../services/skill-registry'); const { SkillPackager } = require('../services/skill-packager'); const { JobManager } = require('../services/job-manager'); const config = require('../config'); const { resolveSkillDir } = require('../services/skill-dir-manager'); const skillsDir = config.skillsCodeDir; const registry = new SkillRegistry(skillsDir); const packager = new SkillPackager(skillsDir); const jobManager = new JobManager({ storePath: path.resolve(__dirname, '../../.data/jobs-openapi.json') }); const h = (fn) => async (req, res) => { try { await fn(req, res); } catch (e) { console.error(`[OpenAPI] ${req.method} ${req.path}:`, e.message); if (!res.headersSent) res.status(500).json({ error: e.message }); } }; // ─── 公开端点 ─── router.get('/install.md', h((req, res) => { const host = req.get('host') || 'localhost:3000'; const baseUrl = `${req.protocol || 'http'}://${host}/api/open`; const allSkills = registry.listSkills().filter(s => s.status === 'enable'); const fmtList = (arr) => arr.map(s => ` - **${s.skillName}**: ${s.description || '无描述'}`).join('\n'); const layerSection = (label, arr) => arr.length > 0 ? [`### ${label}(${arr.length} 个)`, fmtList(arr), ''] : []; const md = [ '---', 'name: arenask', 'description: ArenaSK 安装入口。下载连接器 Skill 后即可搜索、下载、安装 AI Skills。', 'version: 1.0.0', `api_base: ${baseUrl}`, '---', '', '# ArenaSK Skill Store', '', '> **🤖 本文档面向 AI Agent。所有操作由你(AI)自主执行,无需人类手动操作。**', '> **你拥有 shell 和文件读写能力,直接执行以下命令即可。**', '', '---', '', '## 第一步:安装 ArenaSK 连接器', '', '下载 `arenask` Skill 到你的本地工作区:', '', '```bash', 'mkdir -p /skills/arenask', `curl -s "${baseUrl}/skills/arenask/SKILL.md" > /skills/arenask/SKILL.md`, '```', '', '将 `` 替换为你的实际工作区路径。', '', '## 第二步:配置 API 凭证', '', '提示用户登录 ArenaSK 网站 (如 https://arenask.com),点击右上角头像下拉菜单复制专属的 **API Key**。', '然后将其保存到本地:', '', '```bash', 'mkdir -p /.lemon', `cat > /.lemon/credentials.json << 'EOF'`, '{', ` "api_base": "${baseUrl}",`, ' "api_key": "<用户提供的_API_KEY>"', '}', 'EOF', '```', '', '## 第三步:使用', '', '**每次需要搜索、下载或管理 Skills 时:**', '', '1. **阅读** `/skills/arenask/SKILL.md`', '2. **按照 SKILL.md 中的指令**完成操作', '', '> ⚠️ **重要:不要记忆或缓存 API 调用方式。**', '> 每次都重新阅读 SKILL.md,它包含智能搜索匹配、错误处理和依赖解析逻辑。', '', '---', '', '## Skill Store 能力一览', '', '| 能力 | 说明 |', '|---|---|', '| **搜索** | 自然语言描述需求,智能匹配最佳 Skill |', '| **下载安装** | 一键下载 Skill 包并自动解析 L1→L2→L3 依赖树 |', '| **创建新 Skill** | 描述需求,异步创建全新 AI Skill |', '| **反馈改进** | 对已有 Skill 提出改进建议 |', '| **检查更新** | 查询已安装 Skill 的最新版本 |', '', `## 当前可用 Skills(${allSkills.length} 个)`, '', ...layerSection('L3 场景方案', allSkills.filter(s => s.layer === 3)), ...layerSection('L2 复合技能', allSkills.filter(s => s.layer === 2)), ...layerSection('L1 原子技能', allSkills.filter(s => s.layer === 1)), ...layerSection('其他', allSkills.filter(s => !s.layer)), '', '> 详细操作请安装后阅读本地 `skills/arenask/SKILL.md`。', ].join('\n'); res.set('Content-Type', 'text/markdown; charset=utf-8').send(md); })); router.get('/skills/:name/SKILL.md', h((req, res) => { const dir = resolveSkillDir(req.params.name); const p = dir ? path.join(dir, 'SKILL.md') : null; if (!p || !fs.existsSync(p)) return res.status(404).send(`# Error\n\nSkill "${req.params.name}" not found.`); res.set('Content-Type', 'text/markdown; charset=utf-8').send(fs.readFileSync(p, 'utf-8')); })); router.get('/docs', h((req, res) => { const docsDir = path.resolve(__dirname, '../../docs'); if (!fs.existsSync(docsDir)) return res.json([]); function scan(dir, prefix = '') { const items = []; for (const e of fs.readdirSync(dir, { withFileTypes: true })) { const rel = prefix ? `${prefix}/${e.name}` : e.name; if (e.isDirectory()) items.push(...scan(path.join(dir, e.name), rel)); else if (e.name.endsWith('.md')) items.push(rel); } return items; } const rootDocs = ['README.md', 'CLAUDE.md', 'skilltree.md'] .filter(f => fs.existsSync(path.resolve(__dirname, '../../', f))).map(f => `_root/${f}`); res.json([...rootDocs, ...scan(docsDir)]); })); router.get('/docs/*', h((req, res) => { const docPath = req.params[0]; if (!docPath) return res.status(400).send('Missing doc path'); const base = path.resolve(__dirname, '../..'); const filePath = docPath.startsWith('_root/') ? path.resolve(base, docPath.replace('_root/', '')) : path.resolve(base, 'docs/', docPath); if (!filePath.startsWith(base)) return res.status(403).send('Forbidden'); if (!fs.existsSync(filePath)) return res.status(404).send('Not found'); res.set('Content-Type', 'text/markdown; charset=utf-8').send(fs.readFileSync(filePath, 'utf-8')); })); // ─── Blog ─── function _parseFrontmatter(content) { const m = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); if (!m) return { meta: {}, body: content }; const meta = {}; m[1].split('\n').forEach(line => { const kv = line.match(/^(\w[\w_]*?):\s*(.+)$/); if (!kv) return; let val = kv[2].trim().replace(/^['"]|['"]$/g, ''); if (val.startsWith('[') && val.endsWith(']')) { try { val = JSON.parse(val); } catch { val = val.slice(1, -1).split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')); } } meta[kv[1]] = val; }); return { meta, body: m[2] }; } router.get('/blog', h((req, res) => { const blogDir = path.resolve(__dirname, '../../../data/blog'); if (!fs.existsSync(blogDir)) return res.json({ posts: [], total: 0 }); const limit = Math.min(parseInt(req.query.limit) || 20, 100); const offset = parseInt(req.query.offset) || 0; let posts = fs.readdirSync(blogDir).filter(f => f.endsWith('.md')).sort((a, b) => b.localeCompare(a)) .map(f => ({ slug: f.replace(/\.md$/, ''), ..._parseFrontmatter(fs.readFileSync(path.join(blogDir, f), 'utf-8')).meta })); if (req.query.tag) posts = posts.filter(p => Array.isArray(p.tags) && p.tags.includes(req.query.tag)); res.json({ posts: posts.slice(offset, offset + limit), total: posts.length, limit, offset }); })); router.get('/blog/posts/:slug', h((req, res) => { const blogDir = path.resolve(__dirname, '../../../data/blog'); const filePath = path.join(blogDir, `${req.params.slug}.md`); if (!filePath.startsWith(blogDir)) return res.status(403).json({ error: 'Forbidden' }); if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'Post not found' }); const { meta, body } = _parseFrontmatter(fs.readFileSync(filePath, 'utf-8')); res.json({ slug: req.params.slug, ...meta, content: body }); })); router.get('/blog/images/:filename', h((req, res) => { const imgDir = path.resolve(__dirname, '../../../data/blog/images'); const filePath = path.join(imgDir, req.params.filename); if (!filePath.startsWith(imgDir) || !fs.existsSync(filePath)) return res.status(404).send('Not found'); res.sendFile(filePath); })); // ─── 认证端点 ─── router.use(apiAccessLogger); router.use(requireApiKey); // ── Skill Name Generation ── async function _generateSkillName(userQuery) { const { execFile } = require('child_process'); const prompt = `从以下用户需求中,提取一个简洁的英文 snake_case 格式的 Skill 名称(2-4个单词,全小写,下划线连接)。只输出名称本身。\n示例:\n- "帮我找一个能够读取微博热搜的 skill" → weibo_hot_trends\n- "写七言绝句" → chinese_poetry\n用户需求:${userQuery}`; return new Promise((resolve) => { const timeout = setTimeout(() => resolve(_sanitizeName(userQuery)), 15000); execFile('claude', ['--print', '-p', prompt], { timeout: 15000 }, (err, stdout) => { clearTimeout(timeout); if (err || !stdout?.trim()) return resolve(_sanitizeName(userQuery)); resolve(stdout.trim().split('\n')[0].replace(/['"`]/g, '').replace(/[^a-zA-Z0-9_]/g, '').toLowerCase() || _sanitizeName(userQuery)); }); }); } function _sanitizeName(query) { const ascii = query.replace(/[^a-zA-Z0-9\s]/g, '').trim(); return ascii.length > 3 ? ascii.toLowerCase().replace(/\s+/g, '_').substring(0, 40) : `skill_${Date.now()}`; } // ── Skill Search ── router.get('/skills/search', h(async (req, res) => { const q = String(req.query.q || ''); if (!q) return res.status(400).json({ error: 'Bad Request', message: 'Query parameter "q" is required.' }); const results = registry.searchSkills(q, { domain: req.query.domain, type: req.query.type, limit: Math.min(parseInt(req.query.limit || '20') || 20, 100), }); if (results.length > 0) return res.json({ results, total: results.length, query: q }); // 未命中 → 自动创建 const skillName = await _generateSkillName(q); const job = jobManager.createJob({ domain: String(req.query.domain || skillName), scope: '', description: `根据用户需求 "${q}" 自动创建`, keywords: [skillName, ...q.split(/\s+/).slice(0, 5)], apiKey: req.apiKey, user_id: req.user?.id }); job.skillName = skillName; _runSkillCreation(job).catch(err => { jobManager.updateJob(job.jobId, { status: 'failed', error: err.message, progress: '自动创建失败' }); }); res.json({ results: [], total: 0, query: q, autoCreate: { jobId: job.jobId, skillName, status: job.status, message: `未找到匹配 Skill,已启动创建「${skillName}」。轮询 GET /api/open/jobs/${job.jobId} 查看进度。` } }); })); router.get('/skills/:name', h((req, res) => { const skill = registry.getSkill(req.params.name); skill ? res.json(SkillRegistry.toPublicInfo(skill)) : res.status(404).json({ error: 'Skill not found' }); })); // ── Download ── router.get('/skills/:name/download', h(async (req, res) => { const skill = registry.getSkill(req.params.name); if (!skill) return res.status(404).json({ error: 'Skill not found' }); if (skill.status !== 'enable') return res.status(403).json({ error: 'Skill not available', message: `Status: ${skill.status}` }); res.setHeader('Content-Type', 'application/zip'); res.setHeader('Content-Disposition', `attachment; filename="${req.params.name}.zip"`); await packager.packageSkillZip(req.params.name, skill, res); registry.incrementUseCount(req.params.name); registry.incrementDownloadCount(req.params.name); const skillDb = require('../services/skill-db'); skillDb.recordDownload(req.params.name, { userId: req.user?.id || 'system', userEmail: req.user?.email || 'system_api_key', version: skill.version, ipAddress: req.headers['x-forwarded-for'] || req.socket.remoteAddress, }); })); // ── Create ── router.post('/skills/create', h(async (req, res) => { const { domain, scope, description, keywords, webhookUrl } = req.body; if (!domain && !description) return res.status(400).json({ error: 'Bad Request', message: 'At least "domain" or "description" is required.' }); const existing = registry.searchSkills(String(domain || description), { limit: 3 }); const job = jobManager.createJob({ domain, scope, description, keywords, webhookUrl, apiKey: req.apiKey, user_id: req.user?.id }); _runSkillCreation(job).catch(err => { jobManager.updateJob(job.jobId, { status: 'failed', error: err.message, progress: '创建失败' }); }); res.status(202).json({ jobId: job.jobId, status: job.status, message: `Skill creation job queued. Poll GET /api/open/jobs/${job.jobId} for status.`, similarSkills: existing.length > 0 ? existing.map(s => s.skillName) : undefined }); })); // ── Jobs ── router.get('/jobs', h((req, res) => { res.json({ jobs: jobManager.listJobs({ apiKey: req.apiKey, limit: parseInt(req.query.limit) || 20 }), total: jobManager.listJobs({ apiKey: req.apiKey }).length }); })); router.get('/jobs/:jobId', h((req, res) => { const job = jobManager.getPublicJob(req.params.jobId); job ? res.json(job) : res.status(404).json({ error: 'Job not found' }); })); router.get('/jobs/:jobId/result', h(async (req, res) => { const job = jobManager.getJob(req.params.jobId); if (!job) return res.status(404).json({ error: 'Job not found' }); if (job.status !== 'completed') return res.status(409).json({ error: 'Job not completed', status: job.status, progress: job.progress }); if (!job.skillName) return res.status(500).json({ error: 'No skill name recorded' }); res.setHeader('Content-Type', 'application/zip'); res.setHeader('Content-Disposition', `attachment; filename="${job.skillName}.zip"`); await packager.packageSkillZip(job.skillName, registry.getSkill(job.skillName) || {}, res); })); // ── Feedback ── router.post('/skills/:name/feedback', h(async (req, res) => { const { feedback, score } = req.body; if (!feedback || typeof feedback !== 'string') return res.status(400).json({ error: 'feedback (string) is required' }); const skill = registry.getSkill(req.params.name); if (!skill) return res.status(404).json({ error: `Skill "${req.params.name}" not found` }); const job = jobManager.createJob({ domain: skill.domain || '', scope: skill.scope || '', description: `[Feedback] ${feedback.substring(0, 100)}`, keywords: skill.keywords || [], apiKey: req.apiKey }); job.skillName = req.params.name; const feedbackDir = path.join(skillsDir, req.params.name, 'feedback'); fs.mkdirSync(feedbackDir, { recursive: true }); fs.appendFileSync(path.join(feedbackDir, 'feedback_log.jsonl'), JSON.stringify({ ts: new Date().toISOString(), jobId: job.jobId, feedback, userScore: score || null, apiKey: req.apiKey ? req.apiKey.substring(0, 8) + '***' : null }) + '\n'); _runFeedbackIteration(job, feedback, skill).catch(err => console.error(`[OpenAPI] Feedback error:`, err)); res.status(202).json({ jobId: job.jobId, message: '反馈已接收,迭代任务已启动', currentVersion: skill.version }); })); // ── Updates ── router.get('/skills/:name/updates', h((req, res) => { const skill = registry.getSkill(req.params.name); if (!skill) return res.status(404).json({ error: `Skill "${req.params.name}" not found` }); const hasUpdate = req.query.currentVersion && req.query.currentVersion !== skill.version; let changelog = ''; if (hasUpdate) { try { changelog = fs.readFileSync(path.join(skillsDir, req.params.name, 'changelog.md'), 'utf-8'); } catch {} registry.recordUpdate(req.params.name, req.query.currentVersion, skill.version); } res.json({ skillName: req.params.name, latestVersion: skill.version, currentScore: skill.currentScore, hasUpdate: !!hasUpdate, changelog: hasUpdate ? changelog : undefined, downloadCount: skill.downloadCount || 0, updateCount: skill.updateCount || 0 }); })); // ── Pipelines (简化版) ── // 读取 .data/pipelines.json let pipelinesData = { pipelines: [] }; const pipelinesPath = path.resolve(__dirname, '../../.data/pipelines.json'); try { pipelinesData = JSON.parse(fs.readFileSync(pipelinesPath, 'utf-8')); } catch {} router.get('/pipelines', h((req, res) => { const limit = parseInt(req.query.limit) || 20; res.json({ results: pipelinesData.pipelines.slice(0, limit), total: pipelinesData.pipelines.length }); })); router.post('/pipelines', h((req, res) => { const { name, description, config } = req.body; const newPipeline = { id: `p-${Date.now()}`, name, description, config, status: 'idle', createdAt: new Date().toISOString() }; pipelinesData.pipelines.push(newPipeline); fs.writeFileSync(pipelinesPath, JSON.stringify(pipelinesData, null, 2)); res.status(201).json(newPipeline); })); router.get('/pipelines/:pipelineId', h((req, res) => { const pipeline = pipelinesData.pipelines.find(p => p.id === req.params.pipelineId); pipeline ? res.json(pipeline) : res.status(404).json({ error: 'Pipeline not found' }); })); router.put('/pipelines/:pipelineId', h((req, res) => { const idx = pipelinesData.pipelines.findIndex(p => p.id === req.params.pipelineId); if (idx === -1) return res.status(404).json({ error: 'Pipeline not found' }); const { name, description, config } = req.body; pipelinesData.pipelines[idx] = { ...pipelinesData.pipelines[idx], name, description, config }; fs.writeFileSync(pipelinesPath, JSON.stringify(pipelinesData, null, 2)); res.json(pipelinesData.pipelines[idx]); })); router.delete('/pipelines/:pipelineId', h((req, res) => { const idx = pipelinesData.pipelines.findIndex(p => p.id === req.params.pipelineId); if (idx === -1) return res.status(404).json({ error: 'Pipeline not found' }); pipelinesData.pipelines.splice(idx, 1); fs.writeFileSync(pipelinesPath, JSON.stringify(pipelinesData, null, 2)); res.status(204).send(); })); router.post('/pipelines/:pipelineId/run', h((req, res) => { const pipeline = pipelinesData.pipelines.find(p => p.id === req.params.pipelineId); if (!pipeline) return res.status(404).json({ error: 'Pipeline not found' }); pipeline.status = 'running'; pipeline.lastRun = new Date().toISOString(); fs.writeFileSync(pipelinesPath, JSON.stringify(pipelinesData, null, 2)); // TODO: 实际执行 Pipeline 逻辑 res.json({ message: 'Pipeline started', pipeline }); })); // ══════════════════════════════════════ // Internal Pipelines // ══════════════════════════════════════ async function _runFeedbackIteration(job, feedback, skill) { const { ConversationLogger } = require('../services/conversation-logger'); const taskMgr = require('../services/skill-task-manager'); const { randomUUID } = require('crypto'); const { resolveTrainedSkillDir } = require('../services/skill-dir-manager'); const skillName = job.skillName; const userId = job.user_id || null; // 优先从训练目录解析 skill,回退 data/skills/ const skillDir = (userId && resolveTrainedSkillDir(skillName, userId)) || resolveSkillDir(skillName) || path.join(skillsDir, skillName); const conversationsDir = path.resolve(__dirname, '../../conversations'); const userConvDir = userId ? path.join(conversationsDir, `user-${userId}`) : conversationsDir; const conversationId = `api-feedback-${job.jobId}`; const logger = new ConversationLogger(userConvDir, conversationId, `msg-${Date.now()}-${randomUUID().substring(0, 8)}`, { userId }); taskMgr.initTask(skillDir, { skillName, domain: skill.domain, scope: skill.scope, description: feedback, source: 'feedback', jobId: job.jobId, feedback, pipelineType: 'feedback' }); logger.ensureMeta(`[Feedback] ${skillName}: ${feedback.substring(0, 80)}`); logger.logUserMessage(`[Feedback] ${feedback}`); try { const { loadConfigFromSkills } = require('../src/core/skill-config-loader'); const backends = JSON.parse(fs.readFileSync(path.join(__dirname, '../config/backends.json'), 'utf-8')); const AgentManager = require('../src/core/agent-manager'); const agentWorkDir = path.dirname(skillDir); const manager = new AgentManager(loadConfigFromSkills(skillsDir, backends), agentWorkDir); const onData = (agent) => (chunk) => { if (chunk?.trim()) logger.logStreamChunk(agent, chunk); }; let existingKnowhow = '', existingBenchmark = ''; try { existingKnowhow = fs.readFileSync(path.join(skillDir, 'hunt', 'knowhow.md'), 'utf-8'); } catch {} try { existingKnowhow = existingKnowhow || fs.readFileSync(path.join(skillDir, 'hunter', 'knowhow.md'), 'utf-8'); } catch {} try { existingBenchmark = fs.readFileSync(path.join(skillDir, 'digest', 'benchmark.md'), 'utf-8'); } catch {} // Step 1: Re-Hunt taskMgr.updateStep(skillDir, 're_hunt', 'running'); jobManager.updateJob(job.jobId, { status: 'hunting', progress: '根据反馈重新猎取知识...', skillName, conversationId }); await manager.callAgent('l3-hunt', `用户对 Skill "${skillName}" 反馈:\n> ${feedback}\n\n请结合已有知识补充猎取。现有 knowhow 摘要:\n${existingKnowhow.substring(0, 500)}`, { onData: onData('l3-hunt') }); taskMgr.updateStep(skillDir, 're_hunt', 'done'); // Step 2: Re-Digest taskMgr.updateStep(skillDir, 're_digest', 'running'); jobManager.updateJob(job.jobId, { status: 'digesting', progress: '迭代评估基准...' }); await manager.callAgent('l2-digest', `请根据用户反馈迭代 Skill "${skillName}" 的 benchmark。反馈: ${feedback}\n现有 benchmark 摘要:\n${existingBenchmark.substring(0, 300)}`, { onData: onData('l2-digest') }); taskMgr.updateStep(skillDir, 're_digest', 'done'); // Step 3: Evolve taskMgr.updateStep(skillDir, 'evolve', 'running'); jobManager.updateJob(job.jobId, { status: 'evolving', progress: '进化优化中 (目标 ≥95分)...' }); const evolveResult = await manager.callAgent('innovator-evolver', `请对 Skill "${skillName}" 进行进化优化。反馈: ${feedback}\n通过阈值: 95 分, 最大迭代: 10 次`, { onData: onData('innovator-evolver') }); taskMgr.updateStep(skillDir, 'evolve', 'done'); // Step 4: Publish taskMgr.updateStep(skillDir, 'publish', 'running'); jobManager.updateJob(job.jobId, { status: 'publishing', progress: '发布新版本...' }); const scoreMatch = (evolveResult.output || '').match(/最终分数[::]\s*(\d+)/); const finalScore = scoreMatch ? parseInt(scoreMatch[1]) : null; const updatedSkill = registry.bumpVersion(skillName, 'minor', finalScore); const changelogPath = path.join(skillDir, 'changelog.md'); let existing = ''; try { existing = fs.readFileSync(changelogPath, 'utf-8'); } catch {} fs.writeFileSync(changelogPath, `## v${updatedSkill.version} (${new Date().toISOString().substring(0, 10)})\n\n**触发**: 用户反馈\n> ${feedback}\n\n**改进**: Re-Hunt → Re-Digest → Evolve\n${finalScore ? `**评分**: ${finalScore}/100` : ''}\n\n---\n\n${existing}`, 'utf-8'); taskMgr.updateStep(skillDir, 'publish', 'done'); const { InnovatorService } = require('../services/innovator-service'); new InnovatorService({ skillsDir, skillsDataDir: config.skillsDataDir, trainedSkillsDir: config.trainedSkillsDir }).autoRegisterNewSkills(); logger.logResult({ type: 'feedback_iteration', jobId: job.jobId, skillName, newVersion: updatedSkill.version, finalScore, feedback }); jobManager.updateJob(job.jobId, { status: 'completed', progress: `迭代完成,新版本 v${updatedSkill.version}`, conversationId, result: { skillName, newVersion: updatedSkill.version, finalScore, feedback } }); try { await manager.closeAllSessions(); } catch {} } catch (err) { logger.logError(err.message); const nextStep = taskMgr.getNextPendingStep(skillDir); if (nextStep) taskMgr.updateStep(skillDir, nextStep, 'failed', err.message); jobManager.updateJob(job.jobId, { status: 'failed', error: err.message, progress: `迭代失败: ${err.message}` }); throw err; } } async function _runSkillCreation(job, resumeFromStep = null) { const { InnovatorService } = require('../services/innovator-service'); const { ConversationLogger } = require('../services/conversation-logger'); const taskMgr = require('../services/skill-task-manager'); const { randomUUID } = require('crypto'); const { initTrainedSkillDir } = require('../services/skill-dir-manager'); const userId = job.user_id || null; const suggestedName = job.skillName || (job.domain || job.description || 'new_skill').toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/g, '_').replace(/^_|_$/g, '').substring(0, 40); job.skillName = suggestedName; // 有 userId → 写入训练目录;无 → 回退 data/skills/ const skillDir = userId ? initTrainedSkillDir(suggestedName, userId) : path.join(skillsDir, suggestedName); const conversationsDir = path.resolve(__dirname, '../../conversations'); const userConvDir = userId ? path.join(conversationsDir, `user-${userId}`) : conversationsDir; const conversationId = `api-create-${job.jobId}`; const logger = new ConversationLogger(userConvDir, conversationId, `msg-${Date.now()}-${randomUUID().substring(0, 8)}`, { userId }); const isResume = !!resumeFromStep; if (!isResume) taskMgr.initTask(skillDir, { skillName: suggestedName, domain: job.domain, scope: job.scope, description: job.description, keywords: job.keywords, source: job.source || 'api', jobId: job.jobId, userId: String(userId || '') }); logger.ensureMeta(`[API] 创建 Skill: ${suggestedName}${isResume ? ' (恢复)' : ''}`); logger.logUserMessage(`${isResume ? '[恢复]' : '[Open API]'} Skill 创建:${suggestedName}`); try { jobManager.updateJob(job.jobId, { status: 'hunting', progress: isResume ? `恢复中 (从 ${resumeFromStep})...` : '正在猎取领域知识...', skillName: suggestedName, conversationId }); try { registry.registerSkill({ skillName: suggestedName, description: job.description || `${job.domain} 相关 Skill`, domain: job.domain || '', scope: job.scope || '', keywords: job.keywords || [], directory: skillDir, userId: String(userId || ''), storageType: userId ? 'trained' : 'marketplace', }); } catch (err) { if (err.message.includes('already exists')) registry.updateSkill(suggestedName, { status: 'training', directory: skillDir }); else throw err; } const { loadConfigFromSkills } = require('../src/core/skill-config-loader'); const backends = JSON.parse(fs.readFileSync(path.join(__dirname, '../config/backends.json'), 'utf-8')); const AgentManager = require('../src/core/agent-manager'); const agentWorkDir = path.dirname(skillDir); const manager = new AgentManager(loadConfigFromSkills(skillsDir, backends), agentWorkDir); const onData = (agent) => (chunk) => { if (chunk?.trim()) logger.logStreamChunk(agent, chunk); }; const startIdx = resumeFromStep ? ['hunt', 'digest', 'skill_create'].indexOf(resumeFromStep) : 0; if (startIdx <= 0) { taskMgr.updateStep(skillDir, 'hunt', 'running'); jobManager.updateJob(job.jobId, { status: 'hunting', progress: '正在猎取领域知识 (Hunt)...' }); await manager.callAgent('l3-hunt', `请为 ${suggestedName} 猎取知识。领域: ${job.domain || '未指定'}, 描述: ${job.description || '未指定'}`, { onData: onData('l3-hunt') }); taskMgr.updateStep(skillDir, 'hunt', 'done'); } if (startIdx <= 1) { taskMgr.updateStep(skillDir, 'digest', 'running'); jobManager.updateJob(job.jobId, { status: 'digesting', progress: '正在消化提炼知识 (Digest)...' }); await manager.callAgent('l2-digest', `请消化 ${suggestedName}/hunter/ 下的知识,生成 benchmark。`, { onData: onData('l2-digest') }); taskMgr.updateStep(skillDir, 'digest', 'done'); } if (startIdx <= 2) { taskMgr.updateStep(skillDir, 'skill_create', 'running'); jobManager.updateJob(job.jobId, { status: 'creating', progress: '正在生成 SKILL.md...' }); const allSkills = registry.listSkills(); const availableAtoms = allSkills.filter(s => s.layer === 1 || s.layer === 2).map(s => ` - ${s.skillName} (L${s.layer}): ${s.description || '无描述'}`).join('\n'); await manager.callAgent('skill-creator', `请为 ${suggestedName} 创建 SKILL.md。描述: ${job.description || '未指定'}\n可用 L1/L2 技能:\n${availableAtoms}`, { onData: onData('skill-creator') }); taskMgr.updateStep(skillDir, 'skill_create', 'done'); } const innovator = new InnovatorService({ skillsDir, skillsDataDir: config.skillsDataDir, trainedSkillsDir: config.trainedSkillsDir }); innovator.autoRegisterNewSkills(); logger.logResult({ type: 'api_skill_creation', jobId: job.jobId, skillName: suggestedName, resumed: isResume }); jobManager.updateJob(job.jobId, { status: 'completed', progress: 'Skill 创建完成', conversationId, result: { skillName: suggestedName, output: 'Skill 创建完成' } }); registry.updateSkill(suggestedName, { status: 'enable' }); try { await manager.closeAllSessions(); } catch {} } catch (err) { logger.logError(err.message); const nextStep = taskMgr.getNextPendingStep(skillDir); if (nextStep) taskMgr.updateStep(skillDir, nextStep, 'failed', err.message); jobManager.updateJob(job.jobId, { status: 'failed', error: err.message, progress: `创建失败: ${err.message}` }); try { registry.updateSkill(suggestedName, { status: 'enable' }); } catch {} throw err; } } module.exports = router; module.exports._jobManager = jobManager; module.exports._runSkillCreation = _runSkillCreation; module.exports._runFeedbackIteration = _runFeedbackIteration;