diff --git a/CLAUDE.md b/CLAUDE.md index ca9c8b0..607d3c4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,7 +89,7 @@ npm run okr:migrate # goals → okr 存量迁移 | `system/agents/` | **全部 Agent 本体的唯一目录**。`_base/` 是元 Agent 骨架,`_registry.json` 只用于按 id 补 `benchmark_score`(**目录才是数据源头**,`graph-seed-service.seedAgents()` 直接扫目录)。`_` 前缀为保留 id,扫描时跳过 | | `system/agents/devloop-*/` | 六个 DevLoop Agent(pm, architect, coder, tester, integrator, triager),每个含 `config.json` + `prompt.md` + 一份指路用的薄 `agent.md` | | `server/src/tools/` | Master Agent 可调用的工具(call-agent, marketplace, orchestrate, train-skill, …) | -| `web/src/views/` | 页面:AgentChat, AgentList, CompanyDashboard, GraphConsole, JobsPanel, OkrDashboard, PipelineEditor, QuotaMonitor, SkillsMarket, TaskTimeline, TemplateAgents + DevLoop 四页(Dashboard / Projects / NewRequirement / Ops) | +| `web/src/views/` | 页面:AgentChat, AgentList, CompanyDashboard, GraphConsole, JobsPanel, OkrDashboard, PipelineEditor, QuotaMonitor, SkillsMarket, TaskTimeline + DevLoop 四页(Dashboard / Projects / NewRequirement / Ops)。TemplateAgents(手工模板管理页)已于 2026-08-23 随「system/templates/ 并入 system/agents/」一起下线,agent 本体现在完全走自动化招聘产生 | | `packages/` | `acp-core`, `agent-core`, `evolution`, `plugin-bootstrap` | | `/../data/skills/` | Skill 仓库。**在仓库外面一层**,不是 `/data/skills/`(后者不存在)。路径由 `config.skillsDataDir` 控制,见下「数据存储」的两个 data 根 | | `scripts/` | 运维脚本(含 `sync-skills.js`) | diff --git a/bin/lemon.js b/bin/lemon.js index a02dbd6..9184851 100755 --- a/bin/lemon.js +++ b/bin/lemon.js @@ -74,25 +74,6 @@ async function run() { const res = await request('POST', `/company/${values.id}/fire`, { role: values.role, archive: values.archive }); console.log(JSON.stringify(res.data, null, 2)); } - else if (command === 'template create') { - const { values } = parseArgs({ args: args.slice(2), options: { - id: { type: 'string' }, name: { type: 'string' }, desc: { type: 'string' }, type: { type: 'string', default: 'specialist' } - }}); - // 1. Create the template skeleton - await request('POST', '/company/templates', { id: values.id, name: values.name, description: values.desc, type: values.type }); - // 2. Dispatch to agent-creator via the /generate endpoint - const res = await request('POST', `/company/templates/${values.id}/generate`); - console.log(JSON.stringify(res.data, null, 2)); - } - else if (command === 'template train') { - const { values } = parseArgs({ args: args.slice(2), options: { - id: { type: 'string' }, instructions: { type: 'string' } - }}); - const payload = {}; - if (values.instructions) payload.instructions = values.instructions; - const res = await request('POST', `/company/templates/${values.id}/train`, payload); - console.log(JSON.stringify(res.data, null, 2)); - } else if (command === 'company templates') { const res = await request('GET', '/company/templates'); console.log('\n📦 可用 Agent 模板列表:\n'); diff --git a/server/routes/company.js b/server/routes/company.js index eadb691..493fe6c 100644 --- a/server/routes/company.js +++ b/server/routes/company.js @@ -142,586 +142,6 @@ router.get('/templates', (req, res) => { res.json(companyService.listTemplates()); }); -// PATCH /api/company/templates/:templateId — 修改模板元信息 (名称, 类型, 描述) -router.patch('/templates/:templateId', (req, res) => { - try { - const { templateId } = req.params; - const { name, type, description } = req.body; - if (name !== undefined && !name.trim()) return res.status(400).json({ error: 'name 为必填项' }); - - const path = require('path'); - const fs = require('fs'); - const TEMPLATES_DIR = locator.AGENTS_ROOT; - const templateDir = path.join(TEMPLATES_DIR, templateId); - - if (!fs.existsSync(templateDir)) { - return res.status(404).json({ error: `模板 "${templateId}" 不存在` }); - } - - // 1. 更新 config.json 中的字段 - const configPath = path.join(templateDir, 'config.json'); - if (fs.existsSync(configPath)) { - const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - if (name !== undefined) config.name = name.trim(); - if (type !== undefined) config.type = type.trim(); - if (description !== undefined) config.description = description.trim(); - fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); - } - - // 2. 更新 _registry.json 中的字段 - const registryPath = path.join(TEMPLATES_DIR, '_registry.json'); - if (fs.existsSync(registryPath)) { - try { - const registry = JSON.parse(fs.readFileSync(registryPath, 'utf-8')); - const entry = registry.find(t => t.id === templateId); - if (entry) { - if (name !== undefined) entry.name = name.trim(); - if (type !== undefined) entry.type = type.trim(); - if (description !== undefined) entry.description = description.trim(); - fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2) + '\n', 'utf-8'); - } - } catch {} - } - - console.log(`[Template] ✏️ 模板 ${templateId} 元信息已更新`); - res.json({ success: true, id: templateId, name: name?.trim(), type: type?.trim(), description: description?.trim() }); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -// DELETE /api/company/templates/:templateId — 删除模板 -router.delete('/templates/:templateId', (req, res) => { - try { - const { templateId } = req.params; - const path = require('path'); - const fs = require('fs'); - const TEMPLATES_DIR = locator.AGENTS_ROOT; - const templateDir = path.join(TEMPLATES_DIR, templateId); - - if (!fs.existsSync(templateDir)) { - return res.status(404).json({ error: `模板 "${templateId}" 不存在` }); - } - - // 1. 删除模版文件夹 - fs.rmSync(templateDir, { recursive: true, force: true }); - - // 2. 更新 registry - const registryPath = path.join(TEMPLATES_DIR, '_registry.json'); - if (fs.existsSync(registryPath)) { - try { - let registry = JSON.parse(fs.readFileSync(registryPath, 'utf-8')); - registry = registry.filter(t => t.id !== templateId); - fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2) + '\n', 'utf-8'); - } catch {} - } - - console.log(`[Template] 🗑️ 模板 ${templateId} 已被删除`); - res.json({ success: true, message: `模板 ${templateId} 已删除` }); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -// POST /api/company/templates — 创建新模板 Agent -router.post('/templates', async (req, res) => { - try { - const { id, name, type, description } = req.body; - if (!id || !name) return res.status(400).json({ error: 'id 和 name 为必填项' }); - if (!/^[a-z0-9][a-z0-9-]*$/.test(id)) { - return res.status(400).json({ error: 'id 格式无效 (只允许小写字母、数字、-)' }); - } - - const path = require('path'); - const fs = require('fs'); - // 28 批次 5-21:baseDir 只认 agent-locator 这一份定义(原来这里指向已不存在的 system/templates/) - const TEMPLATES_DIR = locator.AGENTS_ROOT; - const templateDir = path.join(TEMPLATES_DIR, id); - - if (fs.existsSync(templateDir)) { - return res.status(409).json({ error: `模板 "${id}" 已存在` }); - } - - // 使用 AgentScaffold 创建标准骨架 - const { AgentScaffold } = await import('../../packages/agent-core/AgentScaffold.js'); - const scaffold = new AgentScaffold(TEMPLATES_DIR); - await scaffold.create({ - id, - type: type || 'skill', - name: name, - parentId: null, - }); - - // 如果有 description,追加到 config.json - if (description) { - const configPath = path.join(templateDir, 'config.json'); - const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - config.description = description; - fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); - } - - console.log(`[Template] ✅ 新模板已创建: ${id} (${type || 'skill'})`); - res.status(201).json({ success: true, id, name, type: type || 'skill' }); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -// POST /api/company/templates/:templateId/generate — 调用 Agent Creator 生成定义文件 -router.post('/templates/:templateId/generate', async (req, res) => { - try { - const { templateId } = req.params; - const { description } = req.body; - const path = require('path'); - const fs = require('fs'); - const config = require('../config'); - const TEMPLATES_DIR = locator.AGENTS_ROOT; - const templateDir = path.join(TEMPLATES_DIR, templateId); - - if (!fs.existsSync(templateDir)) { - return res.status(404).json({ error: `模板 "${templateId}" 不存在` }); - } - - const PORT = config.port || 3000; - // 与 /hire 那处同一份产出契约(28 批次 5-23)—— 文本单点定义在 company-service, - // 两处各抄一份的时候,运行期布局那半段只补进一处就等于没补。 - const prompt = buildAgentDefinitionBrief({ agentId: templateId, bodyPath: templateDir, description }); - - // 异步调用 agent-creator - fetch(`http://127.0.0.1:${PORT}/api/agent/agent-creator/chat`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - message: prompt, - conversationId: `tpl-gen-${templateId}-${Date.now().toString(36)}`, - }), - }).catch(e => console.error('[Template] Agent Creator 请求失败:', e.message)); - - console.log(`[Template] 🤖 已触发 Agent Creator 为 ${templateId} 生成定义`); - res.json({ success: true, message: `已触发 Agent Creator 为 "${templateId}" 生成定义文件`, status: 'generating' }); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -// POST /api/company/templates/:templateId/train — 调用 Innovator 训练模板 -router.post('/templates/:templateId/train', async (req, res) => { - try { - const { templateId } = req.params; - const { instructions } = req.body; - const path = require('path'); - const fs = require('fs'); - const config = require('../config'); - const TEMPLATES_DIR = locator.AGENTS_ROOT; - const templateDir = path.join(TEMPLATES_DIR, templateId); - - if (!fs.existsSync(templateDir)) { - return res.status(404).json({ error: `模板 "${templateId}" 不存在` }); - } - - const PORT = config.port || 3000; - let prompt = `请你对模板 Agent "${templateId}" 进行评估和训练。 -模板位于 \`../../system/agents/${templateId}\` 目录。 -请使用 FDE pipeline 对该模板的技能进行评测和进化训练,目标: Benchmark Score ≥ 95。 -训练过程包括 Hunt(验证数据), Digest(知识消化), Evolver(技能进化) 等。`; - - if (instructions) { - prompt += `\n**【额外训练要求/指令】**\n${instructions}\n`; - } - - prompt += `\n请立即开始评估和训练,训练完成后请汇报最终分数。`; - - // 异步调用 innovator - fetch(`http://127.0.0.1:${PORT}/api/agent/innovator/chat`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - message: prompt, - conversationId: `tpl-train-${templateId}-${Date.now().toString(36)}`, - }), - }).catch(e => console.error('[Template] Innovator 请求失败:', e.message)); - - console.log(`[Template] 🎓 已触发 Innovator 训练 ${templateId}`); - res.json({ success: true, message: `已触发 Innovator 训练 "${templateId}"`, status: 'training' }); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -// POST /api/company/templates/:templateId/skills/upload — 上传 Skill 文件/ZIP 包 -const multer = require('multer'); -const AdmZip = require('adm-zip'); - -const skillUploadStorage = multer.diskStorage({ - destination: (req, file, cb) => { - const os = require('os'); - const tmpDir = path.join(os.tmpdir(), `skill-upload-${Date.now()}`); - fs.mkdirSync(tmpDir, { recursive: true }); - cb(null, tmpDir); - }, - filename: (req, file, cb) => { - // Sanitize filename - const safe = file.originalname.replace(/[^a-zA-Z0-9._-]/g, '_'); - cb(null, safe); - }, -}); -const skillUpload = multer({ - storage: skillUploadStorage, - limits: { fileSize: 50 * 1024 * 1024, files: 50 }, // 50MB per file, 50 files max -}); - -router.post('/templates/:templateId/skills/upload', skillUpload.array('files', 50), async (req, res) => { - try { - const { templateId } = req.params; - const targetSubdir = req.body.targetDir || '.claude/skills'; // default to .claude/skills/ (Claude Code ACP reads from here) - const TEMPLATES_DIR = locator.AGENTS_ROOT; - const templateDir = path.join(TEMPLATES_DIR, templateId); - - if (!fs.existsSync(templateDir)) { - return res.status(404).json({ error: `模板 "${templateId}" 不存在` }); - } - - const destDir = path.join(templateDir, targetSubdir); - fs.mkdirSync(destDir, { recursive: true }); - - const uploadedFiles = req.files || []; - if (uploadedFiles.length === 0) { - return res.status(400).json({ error: '没有上传任何文件' }); - } - - let extractedCount = 0; - let copiedCount = 0; - const details = []; - - for (const file of uploadedFiles) { - const ext = path.extname(file.originalname).toLowerCase(); - - if (ext === '.zip') { - // ── ZIP 解压 ── - try { - const zip = new AdmZip(file.path); - const entries = zip.getEntries(); - - // 检测是否整个 ZIP 包在一个顶级目录下(常见的 GitHub 下载格式) - const topDirs = new Set(); - for (const entry of entries) { - const firstSegment = entry.entryName.split('/')[0]; - topDirs.add(firstSegment); - } - const hasWrapper = topDirs.size === 1 && entries.some(e => e.isDirectory && e.entryName === [...topDirs][0] + '/'); - const stripPrefix = hasWrapper ? [...topDirs][0] + '/' : ''; - - for (const entry of entries) { - if (entry.isDirectory) continue; - // 安全:跳过 __MACOSX、.DS_Store 等系统文件 - if (entry.entryName.includes('__MACOSX') || entry.entryName.endsWith('.DS_Store')) continue; - - let relativePath = entry.entryName; - if (stripPrefix && relativePath.startsWith(stripPrefix)) { - relativePath = relativePath.substring(stripPrefix.length); - } - if (!relativePath) continue; - - const outputPath = path.join(destDir, relativePath); - // 防止路径穿越 - if (!outputPath.startsWith(destDir)) continue; - - fs.mkdirSync(path.dirname(outputPath), { recursive: true }); - fs.writeFileSync(outputPath, entry.getData()); - extractedCount++; - details.push({ action: 'extracted', file: `${targetSubdir}/${relativePath}` }); - } - } catch (zipErr) { - details.push({ action: 'error', file: file.originalname, error: `ZIP 解压失败: ${zipErr.message}` }); - } - } else { - // ── 普通文件直接复制 ── - // 如果上传了带有 webkitRelativePath 的文件夹结构,保留相对路径 - const relativePath = file.originalname; - const outputPath = path.join(destDir, relativePath); - - // 防止路径穿越 - if (!outputPath.startsWith(destDir)) continue; - - fs.mkdirSync(path.dirname(outputPath), { recursive: true }); - fs.copyFileSync(file.path, outputPath); - copiedCount++; - details.push({ action: 'copied', file: `${targetSubdir}/${relativePath}` }); - } - - // 清理临时文件 - try { fs.unlinkSync(file.path); } catch {} - } - - // 清理临时上传目录 - for (const file of uploadedFiles) { - try { fs.rmdirSync(path.dirname(file.path)); } catch {} - } - - console.log(`[Template] 📥 ${templateId}: Skill 导入完成 - ${extractedCount} extracted, ${copiedCount} copied`); - res.json({ - success: true, - templateId, - targetDir: targetSubdir, - stats: { extractedFromZip: extractedCount, copiedFiles: copiedCount, totalFiles: extractedCount + copiedCount }, - details, - }); - } catch (err) { - console.error('[Template] Skill upload error:', err); - res.status(500).json({ error: err.message }); - } -}); - -/** - * POST /api/company/templates/:templateId/provision - * 系统级依赖安装 — 绕过 ACP,直接 shell 执行 - * - * Body: { commands: ["pip install rembg", "npm install ..."], agentDir?: string } - * - commands: 要执行的 shell 命令数组 - * - agentDir: 可选,指定公司 agent 目录(不传则在 template 目录执行) - * - * 特点: - * 1. 不走 Claude CLI / ACP,不受 45s idle timeout 限制 - * 2. 用 child_process.exec 同步执行,每个命令独立超时 10 分钟 - * 3. 返回每个命令的 stdout/stderr 供前端展示 - */ -router.post('/templates/:templateId/provision', async (req, res) => { - try { - const { templateId } = req.params; - const { commands, agentDir } = req.body; - const { exec } = require('child_process'); - const { promisify } = require('util'); - const execAsync = promisify(exec); - - if (!commands || !Array.isArray(commands) || commands.length === 0) { - return res.status(400).json({ error: '请提供 commands 数组' }); - } - - // 确定工作目录 - // 原来写的是 `../../data/system/templates` —— 一个从来不存在的路径(`data/` 多余), - // 所以不带 agentDir 的调用一直是 404。顺手改正到合并后的本体目录。 - const TEMPLATES_DIR = locator.AGENTS_ROOT; - let workDir; - if (agentDir) { - // 公司 agent 目录 - workDir = path.resolve(__dirname, '../../', agentDir); - } else { - workDir = path.join(TEMPLATES_DIR, templateId); - } - - if (!fs.existsSync(workDir)) { - return res.status(404).json({ error: `目录不存在: ${workDir}` }); - } - - // 安全检查:禁止危险命令 - const BLOCKED_PATTERNS = [/rm\s+-rf\s+\//, /mkfs/, /dd\s+if=/, />\s*\/dev\//]; - for (const cmd of commands) { - if (BLOCKED_PATTERNS.some(p => p.test(cmd))) { - return res.status(400).json({ error: `命令被安全策略拒绝: ${cmd}` }); - } - } - - console.log(`[Provision] 🔧 ${templateId}: 开始执行 ${commands.length} 个系统命令`); - - const results = []; - for (const cmd of commands) { - console.log(`[Provision] ▶ ${cmd}`); - try { - const { stdout, stderr } = await execAsync(cmd, { - cwd: workDir, - timeout: 10 * 60 * 1000, // 每个命令最多 10 分钟 - maxBuffer: 10 * 1024 * 1024, // 10MB output buffer - env: { ...process.env, PIP_PROGRESS_BAR: 'off' }, - }); - results.push({ command: cmd, status: 'success', stdout: stdout.slice(-2000), stderr: stderr.slice(-1000) }); - console.log(`[Provision] ✅ ${cmd} 完成`); - } catch (err) { - results.push({ command: cmd, status: 'error', error: err.message, stdout: err.stdout?.slice(-2000), stderr: err.stderr?.slice(-1000) }); - console.error(`[Provision] ❌ ${cmd} 失败:`, err.message); - } - } - - const allOk = results.every(r => r.status === 'success'); - res.json({ - success: allOk, - templateId, - workDir, - results, - }); - } catch (err) { - console.error('[Provision] Error:', err); - res.status(500).json({ error: err.message }); - } -}); - -// GET /api/company/templates/:templateId/logs — 获取模板相关的 agent-creator / innovator 执行日志 -router.get('/templates/:templateId/logs', (req, res) => { - try { - const path = require('path'); - const fs = require('fs'); - const templateId = req.params.templateId; - const locator = require('../services/agent-locator'); - const logs = []; - - // 扫描 agent-creator 和 innovator 的对话,匹配 tpl-gen-{id} 和 tpl-train-{id} - const scanAgents = [ - { agentId: 'agent-creator', prefix: `tpl-gen-${templateId}`, type: 'generate', label: '🤖 Agent Creator' }, - { agentId: 'org-innovator', prefix: `tpl-train-${templateId}`, type: 'train', label: '🎓 Innovator' }, - ]; - - for (const scan of scanAgents) { - // 全局 agent 的会话落在保留公司 id `_global` 下(28 §5.1) - const convDir = path.join(locator.companyDir(scan.agentId), 'conversations'); - if (!fs.existsSync(convDir)) continue; - - const dirs = fs.readdirSync(convDir, { withFileTypes: true }) - .filter(d => d.isDirectory() && d.name.startsWith(scan.prefix)); - - for (const d of dirs) { - const metaPath = path.join(convDir, d.name, 'meta.json'); - const histPath = path.join(convDir, d.name, 'history.json'); - let meta = {}, history = []; - try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')); } catch {} - try { history = JSON.parse(fs.readFileSync(histPath, 'utf-8')); } catch {} - - // 提取最后一条 assistant 消息作为摘要 - const lastAssistant = [...history].reverse().find(m => m.role === 'assistant'); - - logs.push({ - conversationId: d.name, - agentId: scan.agentId, - type: scan.type, - label: scan.label, - createdAt: meta.createdAt || null, - messageCount: history.length, - summary: lastAssistant ? (lastAssistant.content || '').slice(0, 500) : '(执行中...)', - status: lastAssistant ? 'done' : 'running', - }); - } - } - - // 按时间倒序 - logs.sort((a, b) => new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime()); - res.json(logs); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -// GET /api/company/templates/:templateId/files -router.get('/templates/:templateId/files', (req, res) => { - try { - const path = require('path'); - const fs = require('fs'); - const DATA_DIR = locator.AGENTS_ROOT; - const templateDir = path.join(DATA_DIR, req.params.templateId); - - if (!fs.existsSync(templateDir)) { - return res.json({ tree: [], totalFiles: 0 }); - } - - function scanDir(dirPath, relativePath = '') { - const entries = fs.readdirSync(dirPath, { withFileTypes: true }); - const nodes = []; - for (const entry of entries) { - const fullPath = path.join(dirPath, entry.name); - const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name; - - if ((entry.name.startsWith('.') && entry.name !== '.claude') || entry.name === 'node_modules') continue; - - if (entry.isDirectory()) { - const children = scanDir(fullPath, relPath); - nodes.push({ - name: entry.name, - path: relPath, - type: 'directory', - children, - fileCount: children.reduce((sum, c) => sum + (c.type === 'file' ? 1 : (c.fileCount || 0)), 0), - }); - } else { - const stat = fs.statSync(fullPath); - nodes.push({ - name: entry.name, - path: relPath, - type: 'file', - size: stat.size, - modifiedAt: stat.mtime.toISOString(), - }); - } - } - return nodes.sort((a, b) => { - if (a.type !== b.type) return a.type === 'directory' ? -1 : 1; - return a.name.localeCompare(b.name); - }); - } - - const tree = scanDir(templateDir); - const countFiles = (nodes) => nodes.reduce((sum, n) => sum + (n.type === 'file' ? 1 : countFiles(n.children || [])), 0); - - res.json({ tree, totalFiles: countFiles(tree) }); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -// GET /api/company/templates/:templateId/files/content -router.get('/templates/:templateId/files/content', (req, res) => { - try { - const path = require('path'); - const fs = require('fs'); - const DATA_DIR = locator.AGENTS_ROOT; - const filePath = req.query.path; - - if (!filePath) return res.status(400).json({ error: 'path 参数为必填' }); - - const absPath = resolveInside(path.join(DATA_DIR, req.params.templateId), filePath); - if (!absPath) { - return res.status(403).json({ error: '路径越界' }); - } - - if (!fs.existsSync(absPath)) { - return res.status(404).json({ error: '文件不存在' }); - } - - const stat = fs.statSync(absPath); - if (stat.size > 512 * 1024) { - return res.json({ content: '(文件过大,跳过预览)', truncated: true, size: stat.size }); - } - - const content = fs.readFileSync(absPath, 'utf-8'); - res.json({ content, size: stat.size, path: filePath }); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - -// PUT /api/company/templates/:templateId/files/content — 保存模板文件内容 -router.put('/templates/:templateId/files/content', (req, res) => { - try { - const path = require('path'); - const fs = require('fs'); - const DATA_DIR = locator.AGENTS_ROOT; - const filePath = req.body.path; - - if (!filePath) return res.status(400).json({ error: 'path 参数为必填' }); - - const absPath = resolveInside(path.join(DATA_DIR, req.params.templateId), filePath); - if (!absPath) { - return res.status(403).json({ error: '路径越界' }); - } - - // 确保目录存在 - const dir = path.dirname(absPath); - if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); - - fs.writeFileSync(absPath, req.body.content || '', 'utf-8'); - console.log(`[Template] 💾 已保存模板文件 ${req.params.templateId}/${filePath}`); - res.json({ success: true, path: filePath }); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}); - // GET /api/company/:companyId router.get('/:companyId', (req, res) => { const company = companyService.get(req.params.companyId); @@ -1069,8 +489,10 @@ router.post('/:companyId/hire', async (req, res) => { } // 3. 延时 5 秒触发 Innovator 进行评估与训练 (等 creator 写入完毕) + // 同上:`innovator` 改名 `org-innovator` 之后没同步改这里,招聘后自动训练 + // 静默 404 失效(36 号文档阶段二实测发现,见 train 路由那处同款注释)。 setTimeout(() => { - fetch(`http://127.0.0.1:${PORT}/api/agent/innovator/chat`, { + fetch(`http://127.0.0.1:${PORT}/api/agent/org-innovator/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/server/services/company-service.js b/server/services/company-service.js index 9b00aa1..b8f590b 100644 --- a/server/services/company-service.js +++ b/server/services/company-service.js @@ -50,8 +50,9 @@ const TEMPLATE_USAGE_PATH = path.join(ROOT_DATA_DIR, 'template-usage.json'); const BASE_META_AGENT_ID = '_base'; /** - * agent-creator 的产出契约(28 批次 5-23)—— **两处派单共用这一份** - * (`company.js` 的 `POST /:companyId/hire` 与 `POST /templates/:templateId/generate`)。 + * agent-creator 的产出契约(28 批次 5-23)—— 单点定义,`company.js` 的 + * `POST /:companyId/hire` 是唯一调用方(另一个调用方 `POST /templates/:templateId/generate` + * 随手工模板管理页一起于 2026-08-23 下线)。 * * 为什么这段文本要连**运行期布局**一起交代:agent-creator 是个 LLM,它写 agent.md 的办法 * 是照着仓库里已有的本体依样画葫芦,而那些老本体写的是旧布局的 `../../workspace/…` diff --git a/server/services/graph-service.js b/server/services/graph-service.js index c6ecb3f..23afa99 100644 --- a/server/services/graph-service.js +++ b/server/services/graph-service.js @@ -104,7 +104,8 @@ function ensureSystemAgentRow(companyId, systemAgentId, roleName, opts = {}) { * * 为什么是**按需校验**而不是「agent-creator 写完就升」:填 agent.md / soul.md / SKILL.md 的 * 是 agent-creator 自己的 Claude CLI 子进程,触发它的是一次 fire-and-forget 的 HTTP 自调用 - * (`company.js` 的 /hire 与 /templates/:id/generate)—— 那条链路上**没有**「写完了」这个 + * (`company.js` 的 /hire,唯一调用方——`/templates/:id/generate` 已于 2026-08-23 随手工 + * 模板管理页一起下线)—— 那条链路上**没有**「写完了」这个 * 回调点可挂。所以判据只认磁盘:谁来问上岗资格,就现场数一次文件。 * 幂等且便宜(三次 readFileSync),可以放在 hire() 的热路径上。 * diff --git a/server/tests/unit/work-layer-body-paths.test.js b/server/tests/unit/work-layer-body-paths.test.js index bc138cc..6026262 100644 --- a/server/tests/unit/work-layer-body-paths.test.js +++ b/server/tests/unit/work-layer-body-paths.test.js @@ -100,8 +100,11 @@ describe('work 层本体的路径口径(cwd 深度 3)', () => { * * 这三份 agent.md 是 untracked 的运行期产物 —— 只改盘上文件,下一次 agent-creator * 被触发时照样按它看到的老样板重写一遍。唯一能持久的修法是把运行期布局写进**派单文本**, - * 而派单文本有两处(`POST /:companyId/hire` 与 `POST /templates/:templateId/generate`), - * 所以它必须单点定义、两处共用。 + * 由 `buildAgentDefinitionBrief` 单点定义。 + * + * 曾经有第二处调用方(`POST /templates/:templateId/generate`,手工模板管理页的 + * 「生成定义」按钮)——那条手工 UI 已随「system/templates/ 并入 system/agents/」一起下线 + * (agent 本体现在完全走自动化招聘产生),路由已删除,`POST /:companyId/hire` 是唯一调用方。 */ describe('装配来源:agent-creator 的产出契约必须交代运行期布局', () => { const brief = buildAgentDefinitionBrief({ @@ -135,10 +138,9 @@ describe('装配来源:agent-creator 的产出契约必须交代运行期布å± expect(brief).toContain(GRAPH_WRITE_BOUNDARY_HEADER); }); - it('两处派单都走这一份契约:路由里不再留第二份内联清单', () => { - // 内联那份只补进一处,就等于另一处没补 —— 而两处写出来的都是同一批本体。 + it('唯一的派单点走这一份契约:路由里不再留内联清单', () => { const routes = fs.readFileSync(path.join(ROOT, 'server', 'routes', 'company.js'), 'utf-8'); expect(routes).not.toContain('(人格与判断准则)'); - expect(routes.match(/buildAgentDefinitionBrief\(/g) || []).toHaveLength(2); + expect(routes.match(/buildAgentDefinitionBrief\(/g) || []).toHaveLength(1); }); }); diff --git a/web/src/components/AppNav.vue b/web/src/components/AppNav.vue index 4e1ff30..b9bcab4 100644 --- a/web/src/components/AppNav.vue +++ b/web/src/components/AppNav.vue @@ -9,10 +9,6 @@ 🤖 System Agents - - 📦 Templates - - 📜 Task Logs diff --git a/web/src/router/index.js b/web/src/router/index.js index dc296f1..d7a391c 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -25,12 +25,6 @@ const routes = [ props: true, }, { - path: '/templates/:agentId?', - name: 'TemplateAgents', - component: () => import('@/views/TemplateAgents.vue'), - props: true, - }, - { path: '/company', name: 'CompanyList', component: () => import('@/views/CompanyDashboard.vue'), diff --git a/web/src/views/TemplateAgents.vue b/web/src/views/TemplateAgents.vue deleted file mode 100644 index 97c6f2c..0000000 --- a/web/src/views/TemplateAgents.vue +++ /dev/null @@ -1,1019 +0,0 @@ - - - - -