diff --git a/.gitignore b/.gitignore index 68c78b3..592add5 100644 --- a/.gitignore +++ b/.gitignore @@ -14,13 +14,18 @@ bot.log .logs/ .data/ -# Data (runtime generated, per-agent) -data/agents/*/conversations/ -data/agents/*/logs/ -data/agents/*/memory/short-term/ -data/agents/*/workspace/ +# Data (runtime generated, per-agent — covers both data/agents/ and data/system/agents/) +data/**/agents/*/conversations/ +data/**/agents/*/logs/ +data/**/agents/*/memory/ +data/**/agents/*/workspace/ +data/**/agents/.tasks/ data/agents/job-*/ data/trained_skills/ +data/companies/ + +# API access logs (runtime generated by api-access-logger middleware) +apiconversation/ # Legacy directories (removed from architecture) conversations/ diff --git a/data/system/agents/master/plan.md b/data/system/agents/master/plan.md index b6d1476..a2f1167 100644 --- a/data/system/agents/master/plan.md +++ b/data/system/agents/master/plan.md @@ -1,3 +1,48 @@ # 工作计划 -_(清空,等待新的战略规划)_ +## 活跃公司 + +### ABC Inc. (创建于 2026-03-27) +- **状态**: ✅ 运营中 +- **CEO**: abc:ceo - ✅ 已完成五步战略工作流 +- **CTO**: abc:cto - ✅ 已完成技术架构设计 (T2.1),已完成 MVP 开发 Phase 1 +- **COO**: abc:coo - ✅ 已完成运营接管 +- **HR**: abc:hr - ✅ 已完成招聘及培训 (T4.1-T4.3 完成,S4 3/3) +- **执行层**: developer (核心 API 开发中), qa (training), qa-judge (active), writer (training) +- **Pipeline**: S1 完成 ✅, S2 完成 ✅ (92/100 A级), S3 进行中 (T3.2 监控指标), S4 完成 ✅ + +--- + +## 🔄 系统巡检日志 +- 2026-03-27 10:45: ABC Inc. Phase 1 完成 → 已分发 Phase 2 任务 T-mn8rtmpl 给 abc:developer +- 2026-03-27 10:51: COO (abc:coo) 启动 Phase 2 完成 → T-mn8rrrlv ✅ +- 2026-03-27 10:55: 定时巡检 - MVP 测试完成 92/100 A级, S2 全部完成, S3 进行中 +- 2026-03-27 11:00: 定时巡检 - developer 活跃中 (10:42),Phase 2 进行中 +- **产出**: strategy.md, okr.md, pipeline.json, operations_sop.md, metrics_dashboard.md, tech_architecture.md +- 2026-03-27 11:05: 定时巡检 - 全部 Agent 活跃 (master/abc:cto/abc:coo/abc:hr/abc:qa-judge), developer/qa/writer training中,系统运行正常 +- 2026-03-27 11:10: 定时巡检 - 全部 Agent 状态正常,无卡住/错误 +- 2026-03-27 11:15: 定时巡检 - cto/coo 活跃,系统正常 +- 2026-03-27 11:20: 定时巡检 - 全部 Agent 正常,qa-judge/cto/coo 活跃 +- 2026-03-27 14:40: 定时巡检 - qa-judge/cto/coo 14:36 活跃,hr 10:51,training 状态正常 +- 2026-03-27 15:00: 定时巡检 - cto/coo 活跃,系统正常 +- 2026-03-27 15:10: CTO (abc:cto) 完成热点新闻爬虫架构设计 → T-mn913wpv ✅ +- 2026-03-27 15:05: 定时巡检 - 系统正常,13 Agent 运行中,5 名爬虫 Agent (training),cto/coo/hr/qa-judge (active) +- 2026-03-27 15:10: CTO (abc:cto) 完成热点新闻爬虫架构设计 → T-mn913wpv ✅ +- 2026-03-27 15:10: 定时巡检 - hr 15:04/cto 15:04/coo 15:04/qa-judge 15:03 活跃,系统正常 +- 2026-03-27 15:15: 定时巡检 - cto/coo/hr/qa-judge 活跃,系统正常 +- 2026-03-27 15:16: FDE 训练任务已分发给 HR (abc:hr) → T-mn91ic7i +- 2026-03-27 15:16: [TASK_CALLBACK] CTO 任务完成 → FDE 训练分发成功,HR 执行中 +- 2026-03-27 15:20: 定时巡检 - hr/cto/coo/qa-judge 活跃,FDE 训练进行中,系统正常 +- 2026-03-27 15:25: 定时巡检 - hr 15:14/cto 15:14/coo 15:13/qa-judge 15:13 活跃,系统正常 +- 2026-03-27 15:30: 定时巡检 - master 15:17活跃,hr/cto/coo/qa-judge 活跃,5名爬虫 Agent (training),系统正常 +- 2026-03-27 15:32: [TASK_CALLBACK] HR (abc:hr) FDE 训练完成 → 5名爬虫 Agent 技能装配完成,bilibili-hunter 已执行首轮采集 +- 2026-03-27 15:35: 定时巡检 - master 15:18/cto 15:18/coo 15:18/hr 15:18 活跃,qa-judge 15:13,所有 Agent 正常 +- 2026-03-27 15:40: 定时巡检 - qa-judge 15:23/cto 15:23/coo 15:23 活跃,hr 15:18,training 状态正常,系统正常 +- 2026-03-27 15:45: 定时巡检 - cto/coo/qa-judge/hr 活跃,系统正常 +- 2026-03-27 15:50: 定时巡检 - cto 15:34/coo 15:33/qa-judge 15:33 活跃,hr 15:18,系统正常 +- 2026-03-27 15:55: 定时巡检 - cto 15:37/coo 15:39 活跃,qa-judge 15:33,hr 15:18,系统正常 +- 2026-03-27 16:00: 定时巡检 - cto 15:43/coo 15:43/qa-judge 15:43 活跃,hr 15:18,系统正常 + +--- + +_(等待新的战略指令)_ diff --git a/data/system/agents/master/todo.md b/data/system/agents/master/todo.md index 7604e43..2eae921 100644 --- a/data/system/agents/master/todo.md +++ b/data/system/agents/master/todo.md @@ -1,3 +1,6 @@ # 待办事项 _(清空,等待新的指令)_ +| T-mn916ucd | 请协调为 ABC 公司新招聘的 5 名热点新闻爬虫系统执行层员工(bilibili-hunter、google-trends-hunter、reddit-hunter、douban-hunter、hot-aggregator)启动 FDE 训练。每个员工需要掌握的技能:scrapling_crawler + 各自平台 API (bilibili-api, google-trends-api, praw, douban-api)。数据存储路径:data/companies/abc/agents//workspace/data/热点库/。目标是优化到 90 分以上。 | ✅ 完成 | abc:hr | P1 | 2026-03-27 15:03 | - | +| T-mn91hik9 | 【FDE 训练任务】请为 ABC 公司的 5 名热点新闻爬虫系统执行层员工启动 FDE 训练。

## 目标 Agent 列表
1. bilibili-hunter (abc:bilibili-hunter)
2. google-trends-hunter (abc:google-trends-hunter)
3. reddit-hunter (abc:reddit-hunter)
4. douban-hunter (abc:douban-hunter)
5. hot-aggregator (abc:hot-aggregator)

## 每个 Agent 需掌握的技能
- bilibili-hunter: scrapling_crawler + bilibili-api
- google-trends-hunter: scrapling_crawler + google-trends-api
- reddit-hunter: scrapling_crawler + praw (Reddit API)
- douban-hunter: scrapling_crawler + douban-api
- hot-aggregator: scrapling_crawler + 聚合能力

## 数据存储路径
data/companies/abc/agents//workspace/data/热点库/

## 优化目标
各 Agent 技能评分优化到 90 分以上

请执行 FDE 训练流程 (Hunt → Digest → Evolve),完成后汇报结果。 | ✅ 完成 | abc:cto | P1 | 2026-03-27 15:11 | - | +| T-mn91kqqo | 【FDE 训练任务】请为以下 5 名热点新闻爬虫系统执行层员工启动 FDE 训练优化:\n\n1. abc:bilibili-hunter - 技能: scrapling + bilibili-api\n2. abc:google-trends-hunter - 技能: scrapling + google-trends-api\n3. abc:reddit-hunter - 技能: scrapling + reddit-api\n4. abc:douban-hunter - 技能: scrapling + douban-api\n5. abc:hot-aggregator - 技能: scrapling + aggregator\n\n目标:各 Agent 技能评分优化到 90 分以上。请执行 FDE 流程(Hunt → Digest → Evolve)。 | ✅ 完成 | abc:hr | P1 | 2026-03-27 15:14 | - | diff --git a/data/system/templates/_registry.json b/data/system/templates/_registry.json index c3c770a..55c31c7 100644 --- a/data/system/templates/_registry.json +++ b/data/system/templates/_registry.json @@ -6,7 +6,7 @@ "description": "公司战略规划、团队管理、OKR 制定、任务委派", "benchmarkScore": 97, "trainedAt": "2026-03-20", - "usedCount": 10 + "usedCount": 11 }, { "id": "qa-judge", @@ -15,7 +15,7 @@ "description": "质量验收、OKR 进度审查、标准化仲裁", "benchmarkScore": 95, "trainedAt": "2026-03-25", - "usedCount": 6 + "usedCount": 7 }, { "id": "base", diff --git a/docs/architecture-v5.md b/docs/architecture-v5.md index 53db0fd..87a2af8 100644 --- a/docs/architecture-v5.md +++ b/docs/architecture-v5.md @@ -477,6 +477,8 @@ Task 进入 → 海马体检索 → 命中? → 小脑快速执行 - [x] MasterChatDrawer.vue (侧边抽屉对话) - [x] Agent Action Controls (chat, skills panel, train trigger) +- [x] CompanyDashboard Goals Tab 升级为完整 OKR 树视图 (2026-03-27) + ### ⬜ Phase 7: 认知架构 (待开始) - [ ] 海马体检索 (关键词匹配 MVP) @@ -502,6 +504,7 @@ Task 进入 → 海马体检索 → 命中? → 小脑快速执行 | 10 | FDE 双路径产出 | workspace/(共享) + agents/(资产) 同时写入 | 03-24 | | 11 | 元 Agent 依赖技能加载 | buildSystemPrompt 扫描所有 .claude/skills/ 子目录 | 03-24 | | 12 | retrain-pipeline 对齐 | l3-hunt→hunter, l2-digest→digester, innovator-evolver→evolver | 03-25 | +| 13 | Thinking 控制 | `--effort low` 禁用 extended thinking,config.json `cli.effort` 按 Agent 配置 | 03-27 | --- diff --git a/packages/acp-core/protocol/ClaudeCliProtocol.js b/packages/acp-core/protocol/ClaudeCliProtocol.js index 8331fd1..7ca3488 100644 --- a/packages/acp-core/protocol/ClaudeCliProtocol.js +++ b/packages/acp-core/protocol/ClaudeCliProtocol.js @@ -66,6 +66,14 @@ class ClaudeCliProtocol extends EventEmitter { } } + // 支持 effort 级别控制 (low/medium/high/max) + // low = 禁用 extended thinking,大幅节省 token + const effort = this.options.effort || process.env.CLAUDE_EFFORT; + if (effort) { + args.push('--effort', effort); + console.log(`[ClaudeCliProtocol] 💡 effort=${effort}`); + } + const processEnv = { ...process.env }; delete processEnv.NODE_OPTIONS; delete processEnv.ELECTRON_RUN_AS_NODE; diff --git a/server/routes/agent-chat.js b/server/routes/agent-chat.js index 5a9b654..a34e0bf 100644 --- a/server/routes/agent-chat.js +++ b/server/routes/agent-chat.js @@ -574,6 +574,13 @@ async function getOrCreateProtocol(agentId, convId) { console.log(`[AgentChat] 🔒 ${identity.name} 启用编排模式 (legacy fallback): 原生工具已全部禁用`); } + // effort 级别控制 (low=禁用thinking | medium | high | max) + // 优先从 config.json 的 cli.effort 读取,其次环境变量 CLAUDE_EFFORT + if (agentConfig?.cli?.effort) { + cliOptions.effort = agentConfig.cli.effort; + console.log(`[AgentChat] 💡 ${identity.name} effort=${cliOptions.effort}`); + } + const protocol = new ClaudeCliProtocol({ name: identity.name }, cliOptions); await protocol.connect(null, agentDir); await protocol.initialize(); diff --git a/server/routes/company.js b/server/routes/company.js index 284ffc1..1b2dcca 100644 --- a/server/routes/company.js +++ b/server/routes/company.js @@ -102,6 +102,93 @@ router.get('/templates', (req, res) => { res.json(companyService.listTemplates()); }); +// 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 = path.resolve(__dirname, '../../data/system/templates'); + 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 === '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 = path.resolve(__dirname, '../../data/system/templates'); + const filePath = req.query.path; + + if (!filePath) return res.status(400).json({ error: 'path 参数为必填' }); + + const absPath = path.resolve(DATA_DIR, req.params.templateId, filePath); + const safeBoundary = path.resolve(DATA_DIR, req.params.templateId); + if (!absPath.startsWith(safeBoundary)) { + 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 }); + } +}); // GET /api/company/:companyId router.get('/:companyId', (req, res) => { const company = companyService.get(req.params.companyId); @@ -725,7 +812,6 @@ router.post('/:companyId/pipelines/runs/:runId/reject', (req, res) => { } }); -// ═══════════════════════════════════════ // Schedule (日程管理) // ═══════════════════════════════════════ @@ -973,4 +1059,145 @@ router.get('/:companyId/messages', (req, res) => { } }); +// ═══════════════════════════════════════ +// Company Stats — 统计 API (skills count, jobs status) +// ═══════════════════════════════════════ + +// GET /api/company/:companyId/stats — 获取公司统计信息 +router.get('/:companyId/stats', (req, res) => { + try { + const path = require('path'); + const fs = require('fs'); + const DATA_DIR = path.resolve(__dirname, '../../data'); + const companyId = req.params.companyId; + + // 1. Skills 统计 - 统计所有 Agent 的 Skill 数量 + let totalSkills = 0; + const agentSkills = []; + const agentsDir = path.join(DATA_DIR, 'companies', companyId, 'agents'); + + if (fs.existsSync(agentsDir)) { + const agentDirs = fs.readdirSync(agentsDir, { withFileTypes: true }).filter(d => d.isDirectory()); + for (const agentDir of agentDirs) { + const skillsDir = path.join(agentsDir, agentDir.name, 'skills'); + if (fs.existsSync(skillsDir)) { + const skillCount = fs.readdirSync(skillsDir, { withFileTypes: true }).filter(d => d.isDirectory()).length; + totalSkills += skillCount; + agentSkills.push({ agentId: agentDir.name, skillCount }); + } + } + } + + // 2. Agents 统计 + const agents = companyService.listAgents(companyId); + const agentCount = agents.length; + + // 3. Jobs 统计 - 从 jobs 持久化文件读取 + let jobsStats = { total: 0, queued: 0, running: 0, completed: 0, failed: 0 }; + const jobsFilePath = path.join(DATA_DIR, '.data', 'jobs-openapi.json'); + if (fs.existsSync(jobsFilePath)) { + try { + const jobsData = JSON.parse(fs.readFileSync(jobsFilePath, 'utf-8')); + if (Array.isArray(jobsData)) { + jobsStats.total = jobsData.length; + for (const job of jobsData) { + if (jobsStats[job.status] !== undefined) { + jobsStats[job.status]++; + } + } + } + } catch (e) { + console.warn('[Stats] 读取 Jobs 文件失败:', e.message); + } + } + + // 4. Pipelines 统计 + const pSvc = new PipelineService(companyId); + const pipelines = pSvc.listPipelines(); + const runs = pSvc.listRuns(); + const pipelinesStats = { + total: pipelines.length, + active: pipelines.filter(p => p.status === 'active').length, + runs: { + total: runs.length, + running: runs.filter(r => r.status === 'running').length, + completed: runs.filter(r => r.status === 'completed').length, + failed: runs.filter(r => r.status === 'failed').length, + } + }; + + res.json({ + companyId, + timestamp: new Date().toISOString(), + skills: { total: totalSkills, byAgent: agentSkills }, + agents: { total: agentCount }, + jobs: jobsStats, + pipelines: pipelinesStats, + }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// GET /api/company/:companyId/jobs — 获取公司相关的 Job 列表 +router.get('/:companyId/jobs', (req, res) => { + try { + const path = require('path'); + const fs = require('fs'); + const DATA_DIR = path.resolve(__dirname, '../../data'); + const jobsFilePath = path.join(DATA_DIR, '.data', 'jobs-openapi.json'); + + if (!fs.existsSync(jobsFilePath)) { + return res.json([]); + } + + const jobsData = JSON.parse(fs.readFileSync(jobsFilePath, 'utf-8')); + if (!Array.isArray(jobsData)) { + return res.json([]); + } + + // 过滤与公司相关的 Jobs (通过 domain 或 conversationId 包含 companyId) + const limit = Math.min(parseInt(req.query.limit) || 50, 100); + const companyId = req.params.companyId; + const filtered = jobsData + .filter(job => !job.domain || job.domain.includes(companyId)) + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + .slice(0, limit); + + res.json(filtered); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// GET /api/company/:companyId/jobs/:jobId — 获取 Job 详情 +router.get('/:companyId/jobs/:jobId', (req, res) => { + try { + const path = require('path'); + const fs = require('fs'); + const DATA_DIR = path.resolve(__dirname, '../../data'); + const jobsFilePath = path.join(DATA_DIR, '.data', 'jobs-openapi.json'); + + if (!fs.existsSync(jobsFilePath)) { + return res.status(404).json({ error: 'Job 不存在' }); + } + + const jobsData = JSON.parse(fs.readFileSync(jobsFilePath, 'utf-8')); + if (!Array.isArray(jobsData)) { + return res.status(404).json({ error: 'Job 不存在' }); + } + + const job = jobsData.find(j => j.jobId === req.params.jobId); + if (!job) { + return res.status(404).json({ error: 'Job 不存在' }); + } + + // 返回安全版本 (隐藏 apiKey, webhookUrl) + const { apiKey, webhookUrl, ...safeJob } = job; + res.json(safeJob); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + module.exports = router; diff --git a/server/routes/open-api.js b/server/routes/open-api.js index a20ab74..1733b95 100644 --- a/server/routes/open-api.js +++ b/server/routes/open-api.js @@ -278,6 +278,57 @@ router.get('/skills/:name/updates', h((req, res) => { 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 // ══════════════════════════════════════ diff --git a/web/src/components/AppNav.vue b/web/src/components/AppNav.vue index 66284b0..55c1237 100644 --- a/web/src/components/AppNav.vue +++ b/web/src/components/AppNav.vue @@ -9,6 +9,10 @@ 🤖 MasterBot + + 📦 Templates + + @@ -55,6 +59,27 @@ 💬 Messages + + 🧪 Skills + + + 📋 Jobs + + + 🔗 Pipeline + + + + diff --git a/web/src/router/index.js b/web/src/router/index.js index e120f44..dbd828d 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -22,12 +22,18 @@ 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'), }, { - path: '/:id/:tab(overview|agents|pipelines|goals|sop|okr|team|files|messages)', + path: '/:id/:tab(overview|agents|pipelines|goals|sop|okr|team|files|messages|skills|jobs)', name: 'CompanyDetail', component: () => import('@/views/CompanyDashboard.vue'), props: true @@ -37,6 +43,22 @@ const routes = [ name: 'OkrDashboard', component: () => import('@/views/OkrDashboard.vue'), }, + // 新增独立页面 + { + path: '/skills', + name: 'SkillsMarket', + component: () => import('@/views/SkillsMarket.vue'), + }, + { + path: '/jobs', + name: 'JobsPanel', + component: () => import('@/views/JobsPanel.vue'), + }, + { + path: '/pipelines', + name: 'PipelineEditor', + component: () => import('@/views/PipelineEditor.vue'), + }, ] const router = createRouter({ diff --git a/web/src/views/CompanyDashboard.vue b/web/src/views/CompanyDashboard.vue index 534320e..55e2c5d 100644 --- a/web/src/views/CompanyDashboard.vue +++ b/web/src/views/CompanyDashboard.vue @@ -212,6 +212,7 @@

SOP 流水线

+
@@ -219,7 +220,10 @@ ⚙️ {{ p.name }} {{ p.stepCount }} 步
- {{ p.status }} +
+ {{ p.status }} + +
暂无流水线
@@ -232,6 +236,7 @@ v-for="(step, idx) in selectedPipeline.steps" :key="idx" class="step-node" + :class="{ 'step-current': selectedPipeline.currentStep === idx, 'step-completed': selectedPipeline.currentStep > idx }" >
{{ idx + 1 }}
@@ -259,27 +264,107 @@ 步骤 {{ run.currentStep + 1 }}/{{ run.totalSteps }} 🚀 {{ run.trigger }}
+
+ +
- +
-
-

OKR 目标

-
-
-
-
- {{ g.id }} - {{ g.status }} +
+ +
+
+ 🎯 OKR 目标 + {{ goals.length }} +
+
+
+
+ {{ g.id }} + {{ okrStatusLabel(g.status) }} +
+
{{ g.title }}
+
+
+ {{ Math.round(g.progress * 100) }}% +
+
+ 👤 {{ g.owner }} + {{ g.doneCount || 0 }}/{{ g.totalCount || 0 }} +
+
+
暂无目标
-

{{ g.title }}

-
- 👤 {{ g.owner }} +
+ + +
+
+
+ {{ selectedGoalDetail.id }} + {{ okrStatusLabel(selectedGoalDetail.status) }} + 👤 {{ selectedGoalDetail.owner }} +
+

{{ selectedGoalDetail.title }}

+

{{ selectedGoalDetail.description || '暂无详细描述' }}

+
+
+ 目标总体进度 + {{ Math.round(selectedGoalProgress.progress * 100) }}% +
+
+
+ 总: {{ selectedGoalProgress.total }} + ✅ {{ selectedGoalProgress.done }} + 🔄 {{ selectedGoalProgress.inProgress }} + ⏳ {{ selectedGoalProgress.pending }} +
+
+
+
+

OKR 拆解树

+
+
+
+
+
+
+ {{ node.id }} + {{ okrLevelName(node.level) }} + {{ okrStatusLabel(node.status) }} +
+
{{ node.title }}
+
+ 👤 {{ node.owner }} + {{ node.priority }} + 🔗 {{ node.taskId }} +
+
摘要: {{ node.summary }}
+
产出: {{ node.output }}
+
+
+
+
暂无 OKR 节点
+
+
+
+
+
+
🎯
+

OKR 目标看板

+

从左侧选择一个目标查看拆解树与进度

-
暂无目标
@@ -355,6 +440,80 @@
暂无消息记录
+ + +
+
+

🧪 Skills 市场

+
+ + +
+
+
暂无 Skills
+
+
+
{{ skill.icon || '⚡' }}
+
+
{{ skill.name }}
+
{{ skill.description || '暂无描述' }}
+
+
+ {{ skill.version || 'v1.0' }} +
+
+
+ + +
+ + +
+
+

📋 任务面板

+
+ + +
+
+
暂无任务
+
+
+ ID + 状态 + 创建时间 + 操作 +
+
+ {{ job.id?.substring(0, 8) || '-' }} + {{ job.status }} + {{ formatDate(job.createdAt) }} + +
+
+
@@ -400,6 +559,9 @@ const pipelines = ref([]) const pipelineRuns = ref([]) const selectedPipeline = ref(null) const goals = ref([]) +const selectedGoalId = ref(null) +const selectedGoalDetail = ref(null) +const selectedGoalProgress = ref(null) const templates = ref([]) const activeTab = ref('overview') const showTemplates = ref(false) @@ -418,6 +580,15 @@ const companyMessages = ref([]) const msgFilterAgent = ref('') const msgFilterRole = ref('') +// Skills tab +const companySkills = ref([]) +const skillsSearchQuery = ref('') +const selectedSkillDetail = ref(null) + +// Jobs tab +const companyJobs = ref([]) +const jobStatusFilter = ref('') + function statusLabel(status) { const map = { creating: '创建中', running: '运行中', paused: '已暂停', shutdown: '已关停' } return map[status] || status @@ -457,6 +628,8 @@ async function loadCompanyData(id, tab) { if (activeTab.value === 'goals') await loadGoals(id) if (activeTab.value === 'files') await loadCompanyFiles(id) if (activeTab.value === 'messages') await loadMessages(id) + if (activeTab.value === 'skills') await loadSkillsList(id) + if (activeTab.value === 'jobs') await loadCompanyJobs(id) } catch (e) { console.error(e) } } @@ -510,13 +683,70 @@ async function loadPipelineDetail(pipelineId) { } catch (e) { console.error(e) } } +async function runPipeline(pipelineId) { + try { + await axios.post(`/api/company/${selectedCompany.value.id}/pipelines/${pipelineId}/run`) + alert('✅ Pipeline 已启动') + loadPipelines(selectedCompany.value.id) + } catch (e) { alert(`❌ 启动失败: ${e.message}`) } +} + +async function retryPipelineRun(runId) { + try { + await axios.post(`/api/company/${selectedCompany.value.id}/pipelines/runs/${runId}/advance`) + alert('✅ 已重新尝试') + loadPipelines(selectedCompany.value.id) + } catch (e) { alert(`❌ 重试失败: ${e.message}`) } +} + async function loadGoals(id) { try { const { data } = await axios.get(`/api/company/${id}/goals`) goals.value = data + // Auto-select first goal + if (data.length > 0 && !selectedGoalId.value) { + selectGoalDetail(data[0].id) + } } catch (e) { console.error(e) } } +async function selectGoalDetail(goalId) { + if (!selectedCompany.value) return + selectedGoalId.value = goalId + try { + const [goalRes, progRes] = await Promise.all([ + axios.get(`/api/company/${selectedCompany.value.id}/goals/${goalId}`), + axios.get(`/api/company/${selectedCompany.value.id}/goals/${goalId}/progress`) + ]) + selectedGoalDetail.value = goalRes.data + selectedGoalProgress.value = progRes.data + } catch (e) { console.error(e) } +} + +function okrStatusLabel(status) { + const map = { active: '运转中', in_progress: '进行中', pending: '待开始', done: '已完成', failed: '失败', blocked: '已阻塞' } + return map[status] || status +} + +function okrLevelName(level) { + const names = ['Goal', 'Objective', 'Key Result', 'Sub-Task', 'Micro-Task'] + return names[Math.min(level - 1, 4)] +} + +const okrSortedNodes = computed(() => { + if (!selectedGoalDetail.value?.nodes) return [] + const nodes = selectedGoalDetail.value.nodes + const keys = Object.keys(nodes).sort((a, b) => { + const partsA = a.replace('G-','').split('.').map(Number) + const partsB = b.replace('G-','').split('.').map(Number) + for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) { + if ((partsA[i] || 0) !== (partsB[i] || 0)) return (partsA[i] || 0) - (partsB[i] || 0) + } + return 0 + }) + return keys.map(id => ({ id, ...nodes[id], level: id.split('.').length })) +}) + async function loadCompanyStatus(id) { try { const { data } = await axios.get(`/api/company/${id}/status`) @@ -624,6 +854,20 @@ async function triggerTrain(agentId) { } } +async function installSkill(skill) { + try { + await axios.post(`/api/open/skills/${skill.name}/install`) + alert(`✅ ${skill.name} 安装成功`) + selectedSkillDetail.value = null + } catch (e) { + alert(`❌ 安装失败: ${e.message}`) + } +} + +function viewJobDetail(job) { + alert(`任务详情:\nID: ${job.id}\n状态: ${job.status}\n创建: ${formatDate(job.createdAt)}`) +} + // switchTab removed, handled by goToTab and router watcher // ═══ Files Tab Functions ═══ @@ -659,6 +903,22 @@ async function loadMessages(id) { } catch (e) { console.error(e) } } +async function loadSkillsList(id) { + try { + const { data } = await axios.get('/api/open/skills/search', { params: { q: skillsSearchQuery.value, limit: 50 } }) + companySkills.value = data.results || [] + } catch (e) { console.error(e) } +} + +async function loadCompanyJobs(id) { + try { + const params = {} + if (jobStatusFilter.value) params.status = jobStatusFilter.value + const { data } = await axios.get(`/api/company/${id}/jobs`, { params }) + companyJobs.value = data + } catch (e) { console.error(e) } +} + const messageAgentList = computed(() => { const names = new Set(companyMessages.value.map(m => m.agentName)) @@ -849,6 +1109,9 @@ function closeDropdown() { .pipeline-header { display: flex; align-items: center; gap: 12px; } .pipeline-name { font-weight: 600; font-size: 14px; } .pipeline-steps { font-size: 12px; color: var(--text-muted); } +.pipeline-actions-row { display: flex; align-items: center; gap: 12px; } +.btn-pipeline-run { padding: 6px 12px; background: var(--accent); color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 12px; } +.btn-pipeline-run:hover { opacity: 0.9; } .pipeline-detail { background: var(--bg-surface); border: 1px solid var(--border-primary); @@ -858,6 +1121,9 @@ function closeDropdown() { .step-flow { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; } .step-node { display: flex; align-items: center; gap: 8px; } +.step-node.step-current .step-index { background: var(--accent); color: white; } +.step-node.step-completed .step-index { background: #3fb950; color: white; } +.step-node.step-completed { opacity: 0.7; } .step-index { width: 28px; height: 28px; border-radius: 50%; background: var(--accent-bg); color: var(--accent); @@ -885,17 +1151,86 @@ function closeDropdown() { .run-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; } .run-id { font-size: 12px; font-weight: 600; color: var(--text-secondary); font-family: monospace; } .run-meta { display: flex; gap: 16px; font-size: 12px; color: var(--text-muted); } - -/* ═══ Goals ═══ */ -.goals-list { display: flex; flex-direction: column; gap: 8px; } -.goal-card { - padding: 16px 20px; background: var(--bg-surface); - border: 1px solid var(--border-primary); border-radius: 12px; -} -.goal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } -.goal-id { font-size: 12px; font-weight: 700; color: var(--accent); font-family: monospace; } -.goal-title { font-size: 15px; font-weight: 600; margin-bottom: 6px; } -.goal-meta { font-size: 12px; color: var(--text-muted); } +.run-actions { margin-top: 8px; } + +/* ═══ OKR Full Layout ═══ */ +.okr-layout { display: flex; height: calc(100vh - 140px); overflow: hidden; } +.okr-sidebar { width: 280px; min-width: 260px; border-right: 1px solid var(--border-primary); background: var(--bg-overlay); display: flex; flex-direction: column; } +.okr-sidebar-header { padding: 12px 16px; font-size: 13px; font-weight: 700; color: var(--text-secondary); border-bottom: 1px solid var(--border-primary); display: flex; align-items: center; justify-content: space-between; background: var(--bg-secondary); } +.okr-count { background: var(--bg-surface-active); color: var(--accent); padding: 2px 8px; border-radius: 12px; font-size: 11px; } +.okr-goal-list { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 8px; } +.okr-goal-card { padding: 12px 14px; border-radius: 10px; cursor: pointer; background: var(--bg-surface); border: 1px solid var(--border-primary); transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); } +.okr-goal-card:hover { transform: translateY(-2px); border-color: var(--accent-light); box-shadow: 0 4px 12px rgba(16,185,129,0.08); } +.okr-goal-card.active { background: var(--accent-bg); border-color: var(--border-active); box-shadow: 0 0 0 1px var(--accent) inset; } +.okr-goal-top { display: flex; justify-content: space-between; margin-bottom: 6px; } +.okr-gid { font-size: 11px; font-family: 'JetBrains Mono', monospace; font-weight: 600; color: var(--text-tertiary); } +.okr-gstatus { font-size: 10px; font-weight: 600; padding: 2px 6px; border-radius: 4px; } +.okr-gstatus.active { background: rgba(59,130,246,0.15); color: #60a5fa; } +.okr-gstatus.done { background: rgba(16,185,129,0.15); color: #34d399; } +.okr-gtitle { font-size: 13px; font-weight: 600; margin-bottom: 10px; color: var(--text-primary); line-height: 1.4; } +.okr-gprogress { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; } +.okr-pbar { flex: 1; height: 6px; background: var(--bg-input-focus); border-radius: 3px; overflow: hidden; } +.okr-pfill { height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent-light)); border-radius: 3px; transition: width 0.5s ease; } +.okr-ppct { font-size: 11px; font-weight: 600; color: var(--text-secondary); width: 32px; text-align: right; } +.okr-gmeta { display: flex; justify-content: space-between; font-size: 10px; color: var(--text-faint); } + +/* OKR Detail Panel */ +.okr-detail { flex: 1; overflow-y: auto; background: var(--bg-primary); display: flex; flex-direction: column; } +.okr-detail.okr-empty { align-items: center; justify-content: center; background: var(--bg-overlay); } +.okr-detail.okr-empty h3 { font-size: 20px; font-weight: 600; margin-bottom: 8px; color: var(--text-secondary); } +.okr-detail.okr-empty p { color: var(--text-muted); font-size: 13px; } + +.okr-hero { padding: 30px 40px; border-bottom: 1px solid var(--border-primary); background: linear-gradient(180deg, var(--accent-bg) 0%, transparent 100%); } +.okr-hero-row { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; } +.okr-hero-id { font-size: 14px; font-family: 'JetBrains Mono', monospace; font-weight: 700; color: var(--accent-light); } +.okr-hero-status { font-size: 12px; font-weight: 600; padding: 3px 8px; border-radius: 6px; } +.okr-hero-status.active { background: rgba(59,130,246,0.2); color: #60a5fa; border: 1px solid rgba(59,130,246,0.3); } +.okr-hero-status.done { background: rgba(16,185,129,0.2); color: #34d399; border: 1px solid rgba(16,185,129,0.3); } +.okr-hero-owner { font-size: 12px; font-weight: 500; color: var(--text-secondary); margin-left: auto; background: var(--bg-surface); padding: 4px 10px; border-radius: 12px; border: 1px solid var(--border-primary); } +.okr-hero-title { font-size: 26px; font-weight: 800; margin: 0 0 10px; letter-spacing: -0.5px; } +.okr-hero-desc { font-size: 14px; color: var(--text-tertiary); line-height: 1.6; max-width: 800px; margin: 0; } + +.okr-hero-progress { margin-top: 24px; max-width: 600px; background: var(--bg-surface); padding: 16px 20px; border-radius: 12px; border: 1px solid var(--border-primary); } +.okr-hp-top { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 10px; font-size: 13px; font-weight: 600; color: var(--text-secondary); } +.okr-hp-pct { font-size: 24px; font-weight: 800; color: var(--accent-light); line-height: 1; } +.okr-hp-bar { height: 8px; background: var(--bg-input-focus); border-radius: 4px; overflow: hidden; margin-bottom: 12px; } +.okr-hp-fill { height: 100%; background: linear-gradient(90deg, #10b981, #34d399); border-radius: 4px; transition: width 0.8s cubic-bezier(0.16, 1, 0.3, 1); } +.okr-hp-stats { display: flex; gap: 16px; font-size: 11px; font-weight: 500; color: var(--text-tertiary); } +.okr-hp-stats .stat-d { color: #34d399; } +.okr-hp-stats .stat-p { color: #60a5fa; } +.okr-hp-stats .stat-w { color: var(--text-muted); } + +/* OKR Tree */ +.okr-tree-area { padding: 30px 40px; max-width: 1000px; } +.okr-tree-title { font-size: 18px; font-weight: 600; margin: 0 0 20px; color: var(--text-primary); } +.okr-tree { display: flex; flex-direction: column; gap: 6px; } +.okr-tnode-line { display: flex; width: 100%; } +.okr-tnode-indent { position: relative; } +.okr-tnode-indent::before { content: ''; position: absolute; top: -16px; bottom: 50%; left: -14px; width: 14px; border-left: 1.5px solid var(--border-active); border-bottom: 1.5px solid var(--border-active); border-bottom-left-radius: 6px; z-index: 1; opacity: 0.3; } +.okr-tnode-card { flex: 1; background: var(--bg-surface); border: 1px solid var(--border-primary); border-radius: 8px; padding: 12px 16px; transition: all 0.2s; } +.okr-tnode-card:hover { border-color: rgba(255,255,255,0.1); background: var(--bg-surface-hover); } +.okr-tnode-card.okr-lv-2 { background: rgba(59,130,246,0.03); border-color: rgba(59,130,246,0.1); } +.okr-tnode-card.okr-lv-3 { background: rgba(16,185,129,0.03); border-color: rgba(16,185,129,0.1); } +.okr-tnode-hdr { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; } +.okr-nid { font-family: 'JetBrains Mono', monospace; font-size: 11px; font-weight: 700; color: var(--text-tertiary); } +.okr-ntype { font-size: 9px; font-weight: 600; padding: 2px 6px; border-radius: 10px; background: rgba(255,255,255,0.05); color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; } +.okr-nstatus { font-size: 10px; font-weight: 600; padding: 2px 6px; border-radius: 4px; border: 1px solid transparent; } +.okr-nstatus.done { background: rgba(16,185,129,0.1); color: #34d399; border-color: rgba(16,185,129,0.2); } +.okr-nstatus.in_progress { background: rgba(59,130,246,0.1); color: #60a5fa; border-color: rgba(59,130,246,0.2); animation: okr-pulse 2s infinite; } +.okr-nstatus.pending { background: rgba(255,255,255,0.05); color: var(--text-muted); border-color: var(--border-primary); } +@keyframes okr-pulse { 0% { box-shadow: 0 0 0 0 rgba(96,165,250,0.2); } 70% { box-shadow: 0 0 0 4px rgba(96,165,250,0); } 100% { box-shadow: 0 0 0 0 rgba(96,165,250,0); } } +.okr-ntitle { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; line-height: 1.4; } +.okr-nfooter { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 4px; } +.okr-nowner { font-size: 11px; font-weight: 500; color: var(--accent-light); display: flex; align-items: center; gap: 4px; background: var(--accent-bg); padding: 2px 8px; border-radius: 10px; } +.okr-nprio { font-size: 10px; font-weight: 700; font-family: 'JetBrains Mono', monospace; } +.okr-nprio.P0 { color: #ef4444; } +.okr-nprio.P1 { color: #f59e0b; } +.okr-nprio.P2 { color: var(--text-muted); } +.okr-ntask { font-size: 10px; font-family: 'JetBrains Mono', monospace; color: var(--blue-text); background: rgba(59,130,246,0.1); padding: 2px 6px; border-radius: 4px; } +.okr-nsummary { margin-top: 8px; padding-top: 8px; border-top: 1px dashed var(--border-primary); font-size: 11px; color: var(--text-tertiary); line-height: 1.5; } +.okr-nsummary strong { color: var(--text-secondary); } + +.empty-state-large { text-align: center; color: var(--text-muted); } /* ═══ Modal ═══ */ .modal-overlay { @@ -1321,4 +1656,39 @@ function closeDropdown() { white-space: pre-wrap; word-break: break-word; max-height: 120px; overflow-y: auto; } + +/* Skills Tab */ +.search-bar-inline { display: flex; gap: 8px; align-items: center; } +.search-bar-inline input { padding: 8px 12px; border: 1px solid var(--border-primary); border-radius: 6px; background: var(--bg-surface); color: var(--text-primary); width: 200px; } +.skills-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; margin-top: 16px; } +.skill-card-mini { display: flex; align-items: center; gap: 12px; padding: 16px; background: var(--bg-surface); border: 1px solid var(--border-primary); border-radius: 10px; cursor: pointer; transition: all 0.2s; } +.skill-card-mini:hover { border-color: var(--accent); transform: translateY(-2px); } +.skill-icon { font-size: 28px; } +.skill-info { flex: 1; } +.skill-name { font-weight: 600; font-size: 14px; margin-bottom: 4px; } +.skill-desc { font-size: 12px; color: var(--text-muted); } +.skill-meta-mini { display: flex; align-items: center; } +.version-badge { font-size: 11px; padding: 3px 8px; background: var(--bg-primary); border-radius: 4px; color: var(--text-muted); } +.taxonomy-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; } +.taxonomy-tag { font-size: 11px; padding: 4px 8px; background: var(--bg-surface); border-radius: 4px; } + +/* Jobs Tab */ +.filter-inline { display: flex; gap: 8px; align-items: center; } +.filter-inline select { padding: 8px 12px; border: 1px solid var(--border-primary); border-radius: 6px; background: var(--bg-surface); color: var(--text-primary); } +.jobs-table-mini { display: flex; flex-direction: column; gap: 1px; background: var(--border-primary); border-radius: 10px; overflow: hidden; margin-top: 16px; } +.table-header-mini { display: grid; grid-template-columns: 120px 100px 180px 100px; gap: 16px; padding: 12px 16px; background: var(--bg-primary); font-weight: 600; font-size: 13px; color: var(--text-muted); } +.table-row-mini { display: grid; grid-template-columns: 120px 100px 180px 100px; gap: 16px; padding: 12px 16px; background: var(--bg-surface); align-items: center; font-size: 13px; } +.job-id { font-family: monospace; color: var(--text-muted); } +.status-badge-mini { font-size: 11px; padding: 4px 8px; border-radius: 4px; display: inline-block; } +.status-badge-mini.pending { background: #ffd33d22; color: #ffd33d; } +.status-badge-mini.running { background: #58a6ff22; color: #58a6ff; } +.status-badge-mini.completed { background: #3fb95022; color: #3fb950; } +.status-badge-mini.failed { background: #f8514922; color: #f85149; } +.btn-action-mini { padding: 6px 12px; background: var(--bg-primary); border: 1px solid var(--border-primary); border-radius: 4px; color: var(--text-secondary); cursor: pointer; font-size: 12px; } +.btn-action-mini:hover { border-color: var(--accent); } +.btn-sm { padding: 8px 12px; background: var(--bg-surface); border: 1px solid var(--border-primary); border-radius: 6px; cursor: pointer; font-size: 13px; } +.btn-sm:hover { border-color: var(--accent); } +.btn-install-large { padding: 10px 20px; background: var(--accent); color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; } +.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; z-index: 1000; } +.modal-content { background: var(--bg-secondary); border: 1px solid var(--border-primary); border-radius: 12px; max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto; } diff --git a/web/src/views/JobsPanel.vue b/web/src/views/JobsPanel.vue new file mode 100644 index 0000000..9be5492 --- /dev/null +++ b/web/src/views/JobsPanel.vue @@ -0,0 +1,711 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/PipelineEditor.vue b/web/src/views/PipelineEditor.vue new file mode 100644 index 0000000..7a63f31 --- /dev/null +++ b/web/src/views/PipelineEditor.vue @@ -0,0 +1,792 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/SkillsMarket.vue b/web/src/views/SkillsMarket.vue new file mode 100644 index 0000000..66523bf --- /dev/null +++ b/web/src/views/SkillsMarket.vue @@ -0,0 +1,572 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/TemplateAgents.vue b/web/src/views/TemplateAgents.vue new file mode 100644 index 0000000..332dc0a --- /dev/null +++ b/web/src/views/TemplateAgents.vue @@ -0,0 +1,332 @@ + + + + +