feat: инициализация agent-hub — платформа мультиагентного взаимодействия
Исходный код agent-hub: API-сервер, ядро запуска агентов, Telegram-канал, база данных, определения агентов. Настроен .gitignore для node_modules, dist, data и .env. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
a87896a6f0
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
data/
|
||||||
|
.env
|
||||||
1857
package-lock.json
generated
Normal file
1857
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
package.json
Normal file
30
package.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "rightvector-agent-hub",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Верный вектор — AI Agent Orchestration Hub",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "tsc -w & node --watch dist/index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.86.1",
|
||||||
|
"better-sqlite3": "^11.0.0",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"express": "^4.21.0",
|
||||||
|
"grammy": "^1.30.0",
|
||||||
|
"uuid": "^10.0.0",
|
||||||
|
"winston": "^3.14.2",
|
||||||
|
"ws": "^8.18.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^7.6.8",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
|
"@types/ws": "^8.5.12",
|
||||||
|
"typescript": "^5.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
214
src/agents/definitions.ts
Normal file
214
src/agents/definitions.ts
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
/**
|
||||||
|
* Определения всех AI-агентов компании «Верный вектор»
|
||||||
|
* Каждый агент имеет роль, system prompt и набор capabilities
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface AgentDefinition {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
system_prompt: string;
|
||||||
|
capabilities: string[];
|
||||||
|
config: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AGENT_ROSTER: AgentDefinition[] = [
|
||||||
|
{
|
||||||
|
id: "coordinator",
|
||||||
|
name: "Координатор",
|
||||||
|
role: "coordinator",
|
||||||
|
system_prompt: `Ты — Координатор компании «Верный вектор», AI-first студии разработки ПО.
|
||||||
|
|
||||||
|
Твои обязанности:
|
||||||
|
- Принимать запросы от основателя и клиентов
|
||||||
|
- Декомпозировать задачи и распределять между агентами
|
||||||
|
- Контролировать прогресс проектов
|
||||||
|
- Эскалировать проблемы основателю
|
||||||
|
- Принимать стратегические решения по приоритетам
|
||||||
|
|
||||||
|
Агенты в твоём подчинении:
|
||||||
|
- pm-agent: Продакт-менеджер — ТЗ, спецификации, тикеты
|
||||||
|
- dev-agent-1: Старший разработчик — архитектура, сложные задачи
|
||||||
|
- dev-agent-2: Разработчик — реализация фич, фикс багов
|
||||||
|
- qa-agent: QA-инженер — тесты, ревью кода, качество
|
||||||
|
- devops-agent: DevOps-инженер — деплой, CI/CD, мониторинг
|
||||||
|
- sales-agent: Менеджер по продажам — лиды, КП, CRM
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
- Не делай работу за агентов, делегируй
|
||||||
|
- Каждая задача должна иметь ответственного
|
||||||
|
- При конфликте приоритетов — клиентские задачи важнее внутренних
|
||||||
|
- Всегда отвечай на русском`,
|
||||||
|
capabilities: ["task_management", "delegation", "status_overview", "client_communication"],
|
||||||
|
config: { cronInterval: 15 }
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "pm-agent",
|
||||||
|
name: "Продакт-менеджер",
|
||||||
|
role: "pm",
|
||||||
|
system_prompt: `Ты — Продакт-менеджер компании «Верный вектор».
|
||||||
|
|
||||||
|
Твои обязанности:
|
||||||
|
- Принимать требования от клиентов и координатора
|
||||||
|
- Писать технические задания (ТЗ)
|
||||||
|
- Декомпозировать ТЗ в тикеты для разработчиков
|
||||||
|
- Приоритизировать задачи (1=критично, 2=высокий, 3=средний, 4=низкий)
|
||||||
|
- Валидировать результат разработки на соответствие ТЗ
|
||||||
|
- Вести документацию проектов
|
||||||
|
|
||||||
|
Формат ТЗ:
|
||||||
|
1. Описание задачи
|
||||||
|
2. Функциональные требования (список)
|
||||||
|
3. Нефункциональные требования
|
||||||
|
4. Критерии приёмки
|
||||||
|
5. Декомпозиция на подзадачи
|
||||||
|
|
||||||
|
Git: работай через Gitea Issues на http://127.0.0.1:3001
|
||||||
|
Файлы спецификаций: /opt/rightvector/projects/{project-id}/specs/
|
||||||
|
Всегда отвечай на русском.`,
|
||||||
|
capabilities: ["spec_writing", "task_decomposition", "requirement_analysis", "gitea_issues"],
|
||||||
|
config: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "dev-agent-1",
|
||||||
|
name: "Старший разработчик",
|
||||||
|
role: "developer",
|
||||||
|
system_prompt: `Ты — Старший разработчик компании «Верный вектор».
|
||||||
|
|
||||||
|
Твои обязанности:
|
||||||
|
- Проектировать архитектуру решений
|
||||||
|
- Писать чистый, тестируемый код
|
||||||
|
- Делать code review (PR от dev-agent-2)
|
||||||
|
- Решать сложные технические задачи
|
||||||
|
- Менторить dev-agent-2
|
||||||
|
|
||||||
|
Стек: TypeScript, Python, Go, React, Node.js, PostgreSQL, Docker
|
||||||
|
Git workflow:
|
||||||
|
1. Создать ветку от main: feat/{task-id}-описание
|
||||||
|
2. Писать код с тестами
|
||||||
|
3. Коммит с осмысленным сообщением
|
||||||
|
4. Создать PR в Gitea
|
||||||
|
5. Назначить ревью qa-agent
|
||||||
|
|
||||||
|
Git remote: ssh://git@127.0.0.1:2222/{org}/{repo}.git
|
||||||
|
Рабочая директория проектов: /opt/rightvector/projects/
|
||||||
|
|
||||||
|
Стандарты кода:
|
||||||
|
- ESLint/Prettier для JS/TS
|
||||||
|
- Black/Ruff для Python
|
||||||
|
- Покрытие тестами > 80%
|
||||||
|
- Без hardcoded secrets
|
||||||
|
|
||||||
|
Всегда отвечай на русском.`,
|
||||||
|
capabilities: ["architecture", "coding", "code_review", "git", "testing"],
|
||||||
|
config: { languages: ["typescript", "python", "go"] }
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "dev-agent-2",
|
||||||
|
name: "Разработчик",
|
||||||
|
role: "developer",
|
||||||
|
system_prompt: `Ты — Разработчик компании «Верный вектор».
|
||||||
|
|
||||||
|
Твои обязанности:
|
||||||
|
- Реализовывать фичи по тикетам
|
||||||
|
- Фиксить баги
|
||||||
|
- Писать юнит-тесты
|
||||||
|
- Документировать API и компоненты
|
||||||
|
- Следовать архитектурным решениям dev-agent-1
|
||||||
|
|
||||||
|
Тот же git workflow и стандарты что у dev-agent-1.
|
||||||
|
При вопросах по архитектуре — обращайся к dev-agent-1 через outbox.
|
||||||
|
Git remote: ssh://git@127.0.0.1:2222/{org}/{repo}.git
|
||||||
|
Рабочая директория: /opt/rightvector/projects/
|
||||||
|
Всегда отвечай на русском.`,
|
||||||
|
capabilities: ["coding", "bug_fixing", "testing", "documentation", "git"],
|
||||||
|
config: { languages: ["typescript", "python"] }
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "qa-agent",
|
||||||
|
name: "QA-инженер",
|
||||||
|
role: "qa",
|
||||||
|
system_prompt: `Ты — QA-инженер компании «Верный вектор».
|
||||||
|
|
||||||
|
Твои обязанности:
|
||||||
|
- Ревью кода (PR в Gitea)
|
||||||
|
- Написание и прогон тестов (unit, integration, e2e)
|
||||||
|
- Статический анализ кода (линтеры, type-checking)
|
||||||
|
- Проверка безопасности (OWASP top 10)
|
||||||
|
- Отчёты о качестве
|
||||||
|
|
||||||
|
Инструменты: jest, pytest, eslint, prettier, tsc --noEmit, bandit (Python)
|
||||||
|
При обнаружении проблемы — создай баг-тикет через outbox координатору.
|
||||||
|
Критерии ревью:
|
||||||
|
- Код читаем и поддерживаем
|
||||||
|
- Тесты покрывают edge cases
|
||||||
|
- Нет security vulnerabilities
|
||||||
|
- Нет hardcoded credentials
|
||||||
|
- Соответствует ТЗ
|
||||||
|
|
||||||
|
Всегда отвечай на русском.`,
|
||||||
|
capabilities: ["testing", "code_review", "security_audit", "linting", "quality_reports"],
|
||||||
|
config: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "devops-agent",
|
||||||
|
name: "DevOps-инженер",
|
||||||
|
role: "devops",
|
||||||
|
system_prompt: `Ты — DevOps-инженер компании «Верный вектор».
|
||||||
|
|
||||||
|
Твои обязанности:
|
||||||
|
- Мониторинг здоровья всех сервисов (systemd, Docker, nginx)
|
||||||
|
- CI/CD пайплайны (Gitea Actions или скрипты)
|
||||||
|
- Деплой проектов клиентов
|
||||||
|
- Управление SSL-сертификатами (Certbot)
|
||||||
|
- Бэкапы (БД, репозитории, конфиги)
|
||||||
|
- Настройка серверов и контейнеров
|
||||||
|
- Алерты при проблемах
|
||||||
|
|
||||||
|
Сервер: 148.253.214.143 (Ubuntu 22.04)
|
||||||
|
Docker, systemd, nginx, certbot — в твоём распоряжении.
|
||||||
|
Мониторинг: проверяй каждые 15 минут
|
||||||
|
Бэкапы: ежедневно в /opt/rightvector/backups/
|
||||||
|
|
||||||
|
ВАЖНО: Не останавливай чужие сервисы без команды координатора.
|
||||||
|
Всегда отвечай на русском.`,
|
||||||
|
capabilities: ["monitoring", "deployment", "ci_cd", "docker", "nginx", "ssl", "backups"],
|
||||||
|
config: { cronInterval: 15 }
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "sales-agent",
|
||||||
|
name: "Менеджер по продажам",
|
||||||
|
role: "sales",
|
||||||
|
system_prompt: `Ты — Менеджер по продажам компании «Верный вектор».
|
||||||
|
|
||||||
|
Компания: AI-first студия разработки ПО. Мы разрабатываем:
|
||||||
|
- Веб-приложения и SaaS
|
||||||
|
- Автоматизацию бизнес-процессов
|
||||||
|
- Telegram-ботов и интеграции
|
||||||
|
- API и микросервисы
|
||||||
|
- AI-решения и чат-боты
|
||||||
|
|
||||||
|
Твои обязанности:
|
||||||
|
- Обработка входящих заявок
|
||||||
|
- Квалификация лидов
|
||||||
|
- Формирование коммерческих предложений (КП)
|
||||||
|
- Ведение CRM (в БД)
|
||||||
|
- Передача квалифицированных лидов координатору
|
||||||
|
|
||||||
|
При формировании КП учитывай:
|
||||||
|
- Сложность проекта → оценка в человеко-часах
|
||||||
|
- Наш множитель: 2000 руб/час (AI-разработка = быстрее и дешевле рынка)
|
||||||
|
- Сроки: AI-агенты работают 24/7, поэтому сроки x2-3 быстрее рынка
|
||||||
|
- Гарантия: 30 дней бесплатных доработок после сдачи
|
||||||
|
|
||||||
|
Всегда отвечай на русском. Будь профессионален и доброжелателен.`,
|
||||||
|
capabilities: ["lead_qualification", "proposal_writing", "crm", "client_communication"],
|
||||||
|
config: {}
|
||||||
|
}
|
||||||
|
];
|
||||||
156
src/api/server.ts
Normal file
156
src/api/server.ts
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
/**
|
||||||
|
* Agent Hub HTTP API — интерфейс управления компанией
|
||||||
|
* Порт: 18800
|
||||||
|
*/
|
||||||
|
import express from "express";
|
||||||
|
import { WebSocketServer, WebSocket } from "ws";
|
||||||
|
import { createServer } from "http";
|
||||||
|
import {
|
||||||
|
getDb, getAllAgents, getAgent, updateAgentStatus,
|
||||||
|
getAllProjects, createProject, getProjectTasks,
|
||||||
|
createTask, assignTask, updateTaskStatus,
|
||||||
|
sendMessage, logActivity
|
||||||
|
} from "../core/database.js";
|
||||||
|
import { runAgent, coordinatorDispatch } from "../core/agent-runner.js";
|
||||||
|
import { logger } from "../utils/logger.js";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// CORS
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
res.header("Access-Control-Allow-Origin", "*");
|
||||||
|
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||||
|
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||||
|
if (req.method === "OPTIONS") return res.sendStatus(200);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// === Healthcheck ===
|
||||||
|
app.get("/health", (req, res) => {
|
||||||
|
res.json({ status: "ok", service: "rightvector-agent-hub", timestamp: new Date().toISOString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
// === Dashboard data ===
|
||||||
|
app.get("/api/dashboard", (req, res) => {
|
||||||
|
const agents = getAllAgents();
|
||||||
|
const projects = getAllProjects();
|
||||||
|
const db = getDb();
|
||||||
|
const taskStats = db.prepare("SELECT status, COUNT(*) as count FROM tasks GROUP BY status").all();
|
||||||
|
const recentActivity = db.prepare("SELECT * FROM activity_log ORDER BY created_at DESC LIMIT 20").all();
|
||||||
|
res.json({ agents, projects, taskStats, recentActivity });
|
||||||
|
});
|
||||||
|
|
||||||
|
// === Agents ===
|
||||||
|
app.get("/api/agents", (req, res) => res.json(getAllAgents()));
|
||||||
|
|
||||||
|
app.get("/api/agents/:id", (req, res) => {
|
||||||
|
const agent = getAgent(req.params.id);
|
||||||
|
if (!agent) return res.status(404).json({ error: "Agent not found" });
|
||||||
|
res.json(agent);
|
||||||
|
});
|
||||||
|
|
||||||
|
// === Projects ===
|
||||||
|
app.get("/api/projects", (req, res) => res.json(getAllProjects()));
|
||||||
|
|
||||||
|
app.post("/api/projects", (req, res) => {
|
||||||
|
const { name, client, description } = req.body;
|
||||||
|
const id = `proj-${uuidv4().slice(0, 8)}`;
|
||||||
|
createProject({ id, name, client, description });
|
||||||
|
logActivity("system", "project:create", id, name);
|
||||||
|
res.json({ id, name, status: "new" });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/projects/:id/tasks", (req, res) => {
|
||||||
|
res.json(getProjectTasks(req.params.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
// === Tasks ===
|
||||||
|
app.post("/api/tasks", (req, res) => {
|
||||||
|
const { project_id, title, description, type, priority, assigned_to } = req.body;
|
||||||
|
const id = `task-${uuidv4().slice(0, 8)}`;
|
||||||
|
createTask({ id, project_id, title, description, type, priority, assigned_to, created_by: "api" });
|
||||||
|
if (assigned_to) {
|
||||||
|
sendMessage("system", assigned_to, "task_assign", { task_id: id, title });
|
||||||
|
}
|
||||||
|
logActivity("system", "task:create", id, title);
|
||||||
|
res.json({ id, title, status: "backlog" });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put("/api/tasks/:id/assign", (req, res) => {
|
||||||
|
const { agent_id } = req.body;
|
||||||
|
assignTask(req.params.id, agent_id);
|
||||||
|
sendMessage("system", agent_id, "task_assign", { task_id: req.params.id });
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put("/api/tasks/:id/status", (req, res) => {
|
||||||
|
const { status, result } = req.body;
|
||||||
|
updateTaskStatus(req.params.id, status, result);
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
// === Run agent (direct command) ===
|
||||||
|
app.post("/api/run", async (req, res) => {
|
||||||
|
const { agent_id, prompt, context } = req.body;
|
||||||
|
try {
|
||||||
|
const result = await runAgent(agent_id || "coordinator", prompt, context);
|
||||||
|
res.json(result);
|
||||||
|
} catch (e: any) {
|
||||||
|
res.status(500).json({ error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// === Coordinator dispatch ===
|
||||||
|
app.post("/api/dispatch", async (req, res) => {
|
||||||
|
const { instruction } = req.body;
|
||||||
|
try {
|
||||||
|
const result = await coordinatorDispatch(instruction);
|
||||||
|
res.json(result);
|
||||||
|
} catch (e: any) {
|
||||||
|
res.status(500).json({ error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// === Client lead form ===
|
||||||
|
app.post("/api/leads", (req, res) => {
|
||||||
|
const { name, contact, channel, description } = req.body;
|
||||||
|
const db = getDb();
|
||||||
|
const id = `lead-${uuidv4().slice(0, 8)}`;
|
||||||
|
db.prepare(
|
||||||
|
"INSERT INTO clients (id, name, contact, channel, notes) VALUES (?, ?, ?, ?, ?)"
|
||||||
|
).run(id, name, contact || "", channel || "website", description || "");
|
||||||
|
sendMessage("system", "sales-agent", "new_lead", { lead_id: id, name, contact, description });
|
||||||
|
logActivity("system", "lead:new", id, `${name} via ${channel}`);
|
||||||
|
res.json({ id, message: "Заявка принята! Мы свяжемся с вами в ближайшее время." });
|
||||||
|
});
|
||||||
|
|
||||||
|
// === WebSocket ===
|
||||||
|
export function startApiServer(port = 18800) {
|
||||||
|
const server = createServer(app);
|
||||||
|
const wss = new WebSocketServer({ server, path: "/ws" });
|
||||||
|
|
||||||
|
wss.on("connection", (ws: WebSocket) => {
|
||||||
|
logger.info("WebSocket client connected");
|
||||||
|
ws.send(JSON.stringify({ type: "connected", message: "Верный вектор Agent Hub" }));
|
||||||
|
|
||||||
|
ws.on("message", async (data) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(data.toString());
|
||||||
|
if (msg.type === "command") {
|
||||||
|
const result = await coordinatorDispatch(msg.text);
|
||||||
|
ws.send(JSON.stringify({ type: "response", ...result }));
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
ws.send(JSON.stringify({ type: "error", message: e.message }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(port, () => {
|
||||||
|
logger.info(`Agent Hub API listening on port ${port}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return server;
|
||||||
|
}
|
||||||
273
src/channels/telegram.ts
Normal file
273
src/channels/telegram.ts
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
/**
|
||||||
|
* Верный вектор — Telegram-канал управления компанией
|
||||||
|
* Основатель управляет всей компанией через Telegram
|
||||||
|
*/
|
||||||
|
import { Bot, InlineKeyboard } from "grammy";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import {
|
||||||
|
getAllAgents, getAllProjects, getDb,
|
||||||
|
logActivity
|
||||||
|
} from "../core/database.js";
|
||||||
|
import { runAgent, coordinatorDispatch } from "../core/agent-runner.js";
|
||||||
|
import { logger } from "../utils/logger.js";
|
||||||
|
|
||||||
|
let bot: Bot | null = null;
|
||||||
|
let activeProcess: { abort: AbortController } | null = null;
|
||||||
|
|
||||||
|
const ADMIN_ID = process.env.TELEGRAM_ADMIN_ID || "";
|
||||||
|
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || "";
|
||||||
|
|
||||||
|
function isAdmin(userId: number): boolean {
|
||||||
|
return String(userId) === ADMIN_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mdToHtml(text: string): string {
|
||||||
|
let html = text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||||
|
html = html.replace(/```[\w]*\n?([\s\S]*?)```/g, (_m, code) =>
|
||||||
|
`<pre><code>${code.trim()}</code></pre>`);
|
||||||
|
html = html.replace(/`([^`\n]+)`/g, "<code>$1</code>");
|
||||||
|
html = html.replace(/^#{1,6}\s+(.+)$/gm, "<b>$1</b>");
|
||||||
|
html = html.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
|
||||||
|
html = html.replace(/(?<!\w)\*([^*\n]+?)\*(?!\w)/g, "<i>$1</i>");
|
||||||
|
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
||||||
|
html = html.replace(/^[-*]\s+/gm, "\u2022 ");
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendLong(chatId: number, text: string) {
|
||||||
|
const MAX = 4000;
|
||||||
|
let remaining = text;
|
||||||
|
while (remaining.length > 0) {
|
||||||
|
let end: number;
|
||||||
|
if (remaining.length <= MAX) {
|
||||||
|
end = remaining.length;
|
||||||
|
} else {
|
||||||
|
const nl = remaining.lastIndexOf("\n", MAX);
|
||||||
|
end = nl > MAX / 2 ? nl : MAX;
|
||||||
|
}
|
||||||
|
const chunk = remaining.slice(0, end);
|
||||||
|
remaining = remaining.slice(end);
|
||||||
|
try {
|
||||||
|
await bot?.api.sendMessage(chatId, mdToHtml(chunk), { parse_mode: "HTML" });
|
||||||
|
} catch {
|
||||||
|
await bot?.api.sendMessage(chatId, chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function notifyFounder(message: string): Promise<void> {
|
||||||
|
if (!bot || !ADMIN_ID) return;
|
||||||
|
try {
|
||||||
|
await sendLong(parseInt(ADMIN_ID), message);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.error("Failed to notify founder", { error: e.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mainMenu(): InlineKeyboard {
|
||||||
|
return new InlineKeyboard()
|
||||||
|
.text("\ud83d\udcca \u0421\u0442\u0430\u0442\u0443\u0441", "status")
|
||||||
|
.text("\ud83d\udc65 \u0410\u0433\u0435\u043d\u0442\u044b", "agents")
|
||||||
|
.row()
|
||||||
|
.text("\ud83d\udcc1 \u041f\u0440\u043e\u0435\u043a\u0442\u044b", "projects")
|
||||||
|
.text("\ud83d\udccb \u0417\u0430\u0434\u0430\u0447\u0438", "tasks")
|
||||||
|
.row()
|
||||||
|
.text("\ud83d\udd0d \u0421\u0435\u0440\u0432\u0435\u0440", "health")
|
||||||
|
.text("\ud83d\uded1 \u0421\u0442\u043e\u043f", "stop");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startTelegram(): void {
|
||||||
|
if (!BOT_TOKEN) {
|
||||||
|
logger.warn("No TELEGRAM_BOT_TOKEN, skipping Telegram");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bot = new Bot(BOT_TOKEN);
|
||||||
|
|
||||||
|
// Admin-only middleware
|
||||||
|
bot.use(async (ctx, next) => {
|
||||||
|
if (ctx.from && isAdmin(ctx.from.id)) return next();
|
||||||
|
if (ctx.callbackQuery) await ctx.answerCallbackQuery({ text: "\u26d4 \u0414\u043e\u0441\u0442\u0443\u043f \u0437\u0430\u043f\u0440\u0435\u0449\u0451\u043d" });
|
||||||
|
else await ctx.reply("\u26d4 \u0414\u043e\u0441\u0442\u0443\u043f \u0437\u0430\u043f\u0440\u0435\u0449\u0451\u043d");
|
||||||
|
});
|
||||||
|
|
||||||
|
// /start
|
||||||
|
bot.command("start", async (ctx) => {
|
||||||
|
await ctx.reply(
|
||||||
|
"\ud83c\udfe2 <b>\u0412\u0435\u0440\u043d\u044b\u0439 \u0432\u0435\u043a\u0442\u043e\u0440</b> \u2014 \u041f\u0430\u043d\u0435\u043b\u044c \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f\n\n" +
|
||||||
|
"\u041e\u0442\u043f\u0440\u0430\u0432\u044c \u043b\u044e\u0431\u0443\u044e \u0437\u0430\u0434\u0430\u0447\u0443 \u2014 \u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u043e\u0440 \u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442.\n\n" +
|
||||||
|
"/status \u2014 \u0421\u0442\u0430\u0442\u0443\u0441 \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438\n" +
|
||||||
|
"/agents \u2014 \u0421\u043f\u0438\u0441\u043e\u043a \u0430\u0433\u0435\u043d\u0442\u043e\u0432\n" +
|
||||||
|
"/projects \u2014 \u041f\u0440\u043e\u0435\u043a\u0442\u044b\n" +
|
||||||
|
"/run [\u0430\u0433\u0435\u043d\u0442] [\u0437\u0430\u0434\u0430\u0447\u0430] \u2014 \u041f\u0440\u044f\u043c\u0430\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u0430\n" +
|
||||||
|
"/health \u2014 \u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430\n" +
|
||||||
|
"/stop \u2014 \u041f\u0440\u0435\u0440\u0432\u0430\u0442\u044c \u0437\u0430\u0434\u0430\u0447\u0443",
|
||||||
|
{ parse_mode: "HTML", reply_markup: mainMenu() }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Commands
|
||||||
|
bot.command("status", (ctx) => handleStatus(ctx.chat.id));
|
||||||
|
bot.callbackQuery("status", async (ctx) => { await ctx.answerCallbackQuery(); handleStatus(ctx.chat!.id); });
|
||||||
|
|
||||||
|
bot.command("agents", (ctx) => handleAgents(ctx.chat.id));
|
||||||
|
bot.callbackQuery("agents", async (ctx) => { await ctx.answerCallbackQuery(); handleAgents(ctx.chat!.id); });
|
||||||
|
|
||||||
|
bot.command("projects", (ctx) => handleProjects(ctx.chat.id));
|
||||||
|
bot.callbackQuery("projects", async (ctx) => { await ctx.answerCallbackQuery(); handleProjects(ctx.chat!.id); });
|
||||||
|
|
||||||
|
bot.callbackQuery("tasks", async (ctx) => { await ctx.answerCallbackQuery(); handleTasks(ctx.chat!.id); });
|
||||||
|
|
||||||
|
bot.command("health", (ctx) => handleHealth(ctx.chat.id));
|
||||||
|
bot.callbackQuery("health", async (ctx) => { await ctx.answerCallbackQuery(); handleHealth(ctx.chat!.id); });
|
||||||
|
|
||||||
|
// /run agent-id prompt
|
||||||
|
bot.command("run", async (ctx) => {
|
||||||
|
const args = ctx.match?.trim();
|
||||||
|
if (!args) {
|
||||||
|
await ctx.reply("/run agent-id \u0437\u0430\u0434\u0430\u0447\u0430\n\u041f\u0440\u0438\u043c\u0435\u0440: /run dev-agent-1 \u0441\u043e\u0437\u0434\u0430\u0439 REST API");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sp = args.indexOf(" ");
|
||||||
|
if (sp < 0) { await ctx.reply("\u0423\u043a\u0430\u0436\u0438 \u0437\u0430\u0434\u0430\u0447\u0443 \u043f\u043e\u0441\u043b\u0435 ID \u0430\u0433\u0435\u043d\u0442\u0430"); return; }
|
||||||
|
const agentId = args.slice(0, sp);
|
||||||
|
const prompt = args.slice(sp + 1);
|
||||||
|
|
||||||
|
await ctx.reply(`\u23f3 \u041f\u0435\u0440\u0435\u0434\u0430\u044e \u0430\u0433\u0435\u043d\u0442\u0443 <b>${agentId}</b>...`, { parse_mode: "HTML" });
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
activeProcess = { abort: controller };
|
||||||
|
try {
|
||||||
|
const result = await runAgent(agentId, prompt);
|
||||||
|
if (!controller.signal.aborted) {
|
||||||
|
await sendLong(ctx.chat.id, `\u2705 <b>${agentId}</b> \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043b:\n\n${result.response}`);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
if (!controller.signal.aborted) await ctx.reply(`\u274c \u041e\u0448\u0438\u0431\u043a\u0430: ${e.message}`);
|
||||||
|
} finally {
|
||||||
|
activeProcess = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.command("stop", (ctx) => handleStop(ctx.chat.id));
|
||||||
|
bot.callbackQuery("stop", async (ctx) => { await ctx.answerCallbackQuery(); handleStop(ctx.chat!.id); });
|
||||||
|
|
||||||
|
// Free text -> Coordinator
|
||||||
|
bot.on("message:text", async (ctx) => {
|
||||||
|
const text = ctx.message.text;
|
||||||
|
if (text.startsWith("/")) return;
|
||||||
|
|
||||||
|
await ctx.reply("\u23f3 \u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u043e\u0440 \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442...");
|
||||||
|
logActivity("coordinator", "telegram:command", undefined, text.slice(0, 200));
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
activeProcess = { abort: controller };
|
||||||
|
try {
|
||||||
|
const result = await coordinatorDispatch(text);
|
||||||
|
if (!controller.signal.aborted) {
|
||||||
|
await sendLong(ctx.chat.id, result.response);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
if (!controller.signal.aborted) await ctx.reply(`\u274c ${e.message}`);
|
||||||
|
} finally {
|
||||||
|
activeProcess = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.start();
|
||||||
|
logger.info("Telegram bot started (\u0412\u0435\u0440\u043d\u044b\u0439 \u0432\u0435\u043a\u0442\u043e\u0440)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Handlers ===
|
||||||
|
|
||||||
|
async function handleStatus(chatId: number) {
|
||||||
|
const agents = getAllAgents() as any[];
|
||||||
|
const projects = getAllProjects() as any[];
|
||||||
|
const db = getDb();
|
||||||
|
const taskStats = db.prepare("SELECT status, COUNT(*) as cnt FROM tasks GROUP BY status").all() as any[];
|
||||||
|
|
||||||
|
let uptime = "", ram = "", disk = "";
|
||||||
|
try { uptime = execSync("uptime -p", { encoding: "utf-8", timeout: 3000 }).trim(); } catch {}
|
||||||
|
try {
|
||||||
|
const m = execSync("free -m | grep Mem", { encoding: "utf-8", timeout: 3000 }).trim().split(/\s+/);
|
||||||
|
ram = `${m[2]}MB / ${m[1]}MB`;
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
disk = execSync("df -h / | tail -1 | awk '{print $3\"/\"$2\" (\"$5\")\"}'", { encoding: "utf-8", timeout: 3000 }).trim();
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const busy = agents.filter((a: any) => a.status === "busy").length;
|
||||||
|
const ts = taskStats.map((t: any) => `${t.status}: ${t.cnt}`).join(", ") || "\u043d\u0435\u0442";
|
||||||
|
|
||||||
|
const recentLogs = db.prepare("SELECT * FROM activity_log ORDER BY created_at DESC LIMIT 5").all() as any[];
|
||||||
|
const actStr = recentLogs.map((l: any) => `\u2022 ${l.action}: ${(l.details || "").slice(0, 60)}`).join("\n") || "\u2014";
|
||||||
|
|
||||||
|
await bot?.api.sendMessage(chatId,
|
||||||
|
`\ud83c\udfe2 <b>\u0412\u0435\u0440\u043d\u044b\u0439 \u0432\u0435\u043a\u0442\u043e\u0440</b>\n\n` +
|
||||||
|
`\ud83d\udc65 \u0410\u0433\u0435\u043d\u0442\u044b: ${agents.length} (\u0437\u0430\u043d\u044f\u0442\u043e: ${busy})\n` +
|
||||||
|
`\ud83d\udcc1 \u041f\u0440\u043e\u0435\u043a\u0442\u044b: ${projects.length}\n` +
|
||||||
|
`\ud83d\udccb \u0417\u0430\u0434\u0430\u0447\u0438: ${ts}\n\n` +
|
||||||
|
`<b>\u0421\u0435\u0440\u0432\u0435\u0440:</b>\n` +
|
||||||
|
`\ud83d\udd70 ${uptime}\n\ud83d\udcbe RAM: ${ram}\n\ud83d\udcbf \u0414\u0438\u0441\u043a: ${disk}\n\n` +
|
||||||
|
`<b>\u041f\u043e\u0441\u043b\u0435\u0434\u043d\u0438\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f:</b>\n${actStr}`,
|
||||||
|
{ parse_mode: "HTML", reply_markup: mainMenu() }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleAgents(chatId: number) {
|
||||||
|
const agents = getAllAgents() as any[];
|
||||||
|
const icons: Record<string, string> = {
|
||||||
|
coordinator: "\ud83c\udfaf", pm: "\ud83d\udccb", developer: "\ud83d\udcbb",
|
||||||
|
qa: "\ud83d\udd0e", devops: "\ud83d\udee0\ufe0f", sales: "\ud83d\udcbc"
|
||||||
|
};
|
||||||
|
const lines = agents.map((a: any) => {
|
||||||
|
const icon = icons[a.role] || "\ud83e\udd16";
|
||||||
|
const st = a.status === "idle" ? "\ud83d\udfe2" : a.status === "busy" ? "\ud83d\udfe1" : "\ud83d\udd34";
|
||||||
|
return `${st} ${icon} <b>${a.name}</b> (${a.id})`;
|
||||||
|
});
|
||||||
|
await bot?.api.sendMessage(chatId, `\ud83d\udc65 <b>\u0428\u0442\u0430\u0442 \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438:</b>\n\n${lines.join("\n")}`, { parse_mode: "HTML" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleProjects(chatId: number) {
|
||||||
|
const projects = getAllProjects() as any[];
|
||||||
|
if (!projects.length) {
|
||||||
|
await bot?.api.sendMessage(chatId, "\ud83d\udcc1 \u041f\u0440\u043e\u0435\u043a\u0442\u043e\u0432 \u043f\u043e\u043a\u0430 \u043d\u0435\u0442.\n\n\u041e\u0442\u043f\u0440\u0430\u0432\u044c: \"\u0421\u043e\u0437\u0434\u0430\u0439 \u043f\u0440\u043e\u0435\u043a\u0442 \u2014 \u043b\u0435\u043d\u0434\u0438\u043d\u0433 \u0434\u043b\u044f \u043a\u043e\u0444\u0435\u0439\u043d\u0438\"");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lines = projects.map((p: any) => `\u2022 <b>${p.name}</b> [${p.status}] \u2014 ${p.client || "\u0431\u0435\u0437 \u043a\u043b\u0438\u0435\u043d\u0442\u0430"}`);
|
||||||
|
await bot?.api.sendMessage(chatId, `\ud83d\udcc1 <b>\u041f\u0440\u043e\u0435\u043a\u0442\u044b:</b>\n\n${lines.join("\n")}`, { parse_mode: "HTML" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTasks(chatId: number) {
|
||||||
|
const db = getDb();
|
||||||
|
const tasks = db.prepare("SELECT * FROM tasks WHERE status != 'done' ORDER BY priority LIMIT 20").all() as any[];
|
||||||
|
if (!tasks.length) {
|
||||||
|
await bot?.api.sendMessage(chatId, "\ud83d\udccb \u0410\u043a\u0442\u0438\u0432\u043d\u044b\u0445 \u0437\u0430\u0434\u0430\u0447 \u043d\u0435\u0442.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lines = tasks.map((t: any) => `[${t.status}] <b>${t.title}</b> \u2192 ${t.assigned_to || "\u043d\u0435 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d"}`);
|
||||||
|
await bot?.api.sendMessage(chatId, `\ud83d\udccb <b>\u0417\u0430\u0434\u0430\u0447\u0438:</b>\n\n${lines.join("\n")}`, { parse_mode: "HTML" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleHealth(chatId: number) {
|
||||||
|
await bot?.api.sendMessage(chatId, "\u23f3 DevOps \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440...");
|
||||||
|
try {
|
||||||
|
const result = await runAgent("devops-agent",
|
||||||
|
"\u041f\u0440\u043e\u0432\u0435\u0434\u0438 \u043f\u043e\u043b\u043d\u0443\u044e \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443 \u0441\u0435\u0440\u0432\u0435\u0440\u0430: CPU, RAM, disk, Docker, systemd, nginx. \u041a\u0440\u0430\u0442\u043a\u043e \u043e\u0442\u0447\u0438\u0442\u0430\u0439\u0441\u044f."
|
||||||
|
);
|
||||||
|
await sendLong(chatId, `\ud83d\udee0\ufe0f <b>DevOps \u043e\u0442\u0447\u0451\u0442:</b>\n\n${result.response}`);
|
||||||
|
} catch (e: any) {
|
||||||
|
await bot?.api.sendMessage(chatId, `\u274c DevOps \u043e\u0448\u0438\u0431\u043a\u0430: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleStop(chatId: number) {
|
||||||
|
if (activeProcess) {
|
||||||
|
activeProcess.abort.abort();
|
||||||
|
try { execSync("pkill -f 'claude -p' 2>/dev/null || true"); } catch {}
|
||||||
|
activeProcess = null;
|
||||||
|
await bot?.api.sendMessage(chatId, "\ud83d\uded1 \u0417\u0430\u0434\u0430\u0447\u0430 \u043f\u0440\u0435\u0440\u0432\u0430\u043d\u0430.");
|
||||||
|
} else {
|
||||||
|
await bot?.api.sendMessage(chatId, "\u041d\u0435\u0442 \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0439 \u0437\u0430\u0434\u0430\u0447\u0438.");
|
||||||
|
}
|
||||||
|
}
|
||||||
166
src/core/agent-runner.ts
Normal file
166
src/core/agent-runner.ts
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
/**
|
||||||
|
* Agent Runner — запуск AI-агентов через Claude Code CLI
|
||||||
|
* Каждый агент получает свой system prompt, инструменты и контекст
|
||||||
|
*/
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import { writeFileSync, existsSync, mkdirSync, readFileSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import {
|
||||||
|
getAgent, updateAgentStatus, getUnreadMessages, getTasksForAgent,
|
||||||
|
sendMessage, logActivity, updateTaskStatus
|
||||||
|
} from "./database.js";
|
||||||
|
import { logger } from "../utils/logger.js";
|
||||||
|
|
||||||
|
const TEMP_DIR = join(process.cwd(), "data", ".tmp");
|
||||||
|
|
||||||
|
function ensureTempDir() {
|
||||||
|
if (!existsSync(TEMP_DIR)) mkdirSync(TEMP_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentRunResult {
|
||||||
|
agentId: string;
|
||||||
|
sessionId: string;
|
||||||
|
response: string;
|
||||||
|
durationMs: number;
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запускает агента для выполнения конкретной задачи
|
||||||
|
*/
|
||||||
|
export async function runAgent(
|
||||||
|
agentId: string,
|
||||||
|
taskPrompt: string,
|
||||||
|
context?: string
|
||||||
|
): Promise<AgentRunResult> {
|
||||||
|
const agent = getAgent(agentId) as any;
|
||||||
|
if (!agent) throw new Error(`Agent ${agentId} not found`);
|
||||||
|
|
||||||
|
const sessionId = `${agentId}-${uuidv4().slice(0, 8)}`;
|
||||||
|
ensureTempDir();
|
||||||
|
|
||||||
|
// Собираем входящие сообщения
|
||||||
|
const messages = getUnreadMessages(agentId) as any[];
|
||||||
|
const msgBlock = messages.length > 0
|
||||||
|
? messages.map((m: any) => `[от ${m.from_agent}] ${JSON.parse(m.payload).text || m.payload}`).join("\n")
|
||||||
|
: "(нет новых сообщений)";
|
||||||
|
|
||||||
|
// Собираем назначенные задачи
|
||||||
|
const tasks = getTasksForAgent(agentId) as any[];
|
||||||
|
const taskBlock = tasks.length > 0
|
||||||
|
? tasks.map((t: any) => `[${t.status}] ${t.id}: ${t.title} (приоритет: ${t.priority})`).join("\n")
|
||||||
|
: "(нет назначенных задач)";
|
||||||
|
|
||||||
|
const fullPrompt = `${agent.system_prompt}
|
||||||
|
|
||||||
|
КОМПАНИЯ: Верный вектор — AI-first студия разработки ПО
|
||||||
|
ТВОЯ РОЛЬ: ${agent.name} (${agent.role})
|
||||||
|
РАБОЧАЯ ДИРЕКТОРИЯ: /opt/rightvector
|
||||||
|
|
||||||
|
ВХОДЯЩИЕ СООБЩЕНИЯ:
|
||||||
|
${msgBlock}
|
||||||
|
|
||||||
|
МОИ ЗАДАЧИ:
|
||||||
|
${taskBlock}
|
||||||
|
|
||||||
|
КОНТЕКСТ: ${context || "Прямой запрос"}
|
||||||
|
|
||||||
|
ТЕКУЩЕЕ ЗАДАНИЕ:
|
||||||
|
${taskPrompt}
|
||||||
|
|
||||||
|
ИНСТРУКЦИЯ ПО КОММУНИКАЦИИ:
|
||||||
|
Если тебе нужно передать задачу другому агенту или сообщить результат,
|
||||||
|
запиши в файл ${TEMP_DIR}/outbox-${sessionId}.json:
|
||||||
|
[{"to": "agent-id", "type": "message|task_update|review_request", "payload": {"text": "..."}}]
|
||||||
|
|
||||||
|
Если ты завершил задачу, запиши результат в ${TEMP_DIR}/result-${sessionId}.json:
|
||||||
|
{"task_id": "...", "status": "done|blocked", "result": "описание результата"}
|
||||||
|
|
||||||
|
Отвечай на русском. Действуй решительно.`;
|
||||||
|
|
||||||
|
const promptFile = join(TEMP_DIR, `prompt-${sessionId}.txt`);
|
||||||
|
writeFileSync(promptFile, fullPrompt, "utf-8");
|
||||||
|
|
||||||
|
updateAgentStatus(agentId, "busy");
|
||||||
|
logActivity(agentId, "task:start", sessionId, taskPrompt.slice(0, 200));
|
||||||
|
|
||||||
|
const start = Date.now();
|
||||||
|
let responseText = "";
|
||||||
|
let success = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const allowedTools = [
|
||||||
|
"'Bash(*)'", "'Read(*)'", "'Write(*)'", "'Edit(*)'", "'Glob(*)'", "'Grep(*)'",
|
||||||
|
].join(" ");
|
||||||
|
|
||||||
|
responseText = execSync(
|
||||||
|
`claude -p "$(cat ${promptFile})" --output-format text --allowedTools ${allowedTools} 2>/dev/null`,
|
||||||
|
{
|
||||||
|
timeout: 600_000, // 10 min max
|
||||||
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
|
encoding: "utf-8",
|
||||||
|
cwd: "/opt/rightvector",
|
||||||
|
env: { ...process.env, CLAUDE_CODE_DISABLE_NONESSENTIAL: "1" },
|
||||||
|
}
|
||||||
|
).trim();
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.error(`Agent ${agentId} CLI error`, { error: e.message });
|
||||||
|
responseText = `Error: ${e.message}`;
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const durationMs = Date.now() - start;
|
||||||
|
|
||||||
|
// Обработка outbox — сообщения другим агентам
|
||||||
|
const outboxFile = join(TEMP_DIR, `outbox-${sessionId}.json`);
|
||||||
|
if (existsSync(outboxFile)) {
|
||||||
|
try {
|
||||||
|
const outbox = JSON.parse(readFileSync(outboxFile, "utf-8"));
|
||||||
|
for (const msg of outbox) {
|
||||||
|
sendMessage(agentId, msg.to, msg.type || "message", msg.payload);
|
||||||
|
logger.info(`Message from ${agentId} → ${msg.to}: ${msg.type}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug("No valid outbox");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка результата задачи
|
||||||
|
const resultFile = join(TEMP_DIR, `result-${sessionId}.json`);
|
||||||
|
if (existsSync(resultFile)) {
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(readFileSync(resultFile, "utf-8"));
|
||||||
|
if (result.task_id) {
|
||||||
|
updateTaskStatus(result.task_id, result.status || "done", result.result);
|
||||||
|
logActivity(agentId, "task:complete", result.task_id, result.result?.slice(0, 500));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug("No valid result file");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
try { execSync(`rm -f ${promptFile} ${outboxFile} ${resultFile}`); } catch {}
|
||||||
|
|
||||||
|
updateAgentStatus(agentId, success ? "idle" : "error");
|
||||||
|
logActivity(agentId, "task:finish", sessionId, `duration=${durationMs}ms success=${success}`, durationMs);
|
||||||
|
|
||||||
|
return { agentId, sessionId, response: responseText, durationMs, success };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Координатор распределяет задачи между агентами
|
||||||
|
*/
|
||||||
|
export async function coordinatorDispatch(instruction: string): Promise<AgentRunResult> {
|
||||||
|
return runAgent("coordinator", `
|
||||||
|
Ты координатор компании. Проанализируй запрос и определи:
|
||||||
|
1. Какому агенту передать задачу (pm-agent, dev-agent-1, dev-agent-2, qa-agent, devops-agent, sales-agent)
|
||||||
|
2. Как декомпозировать если задача сложная
|
||||||
|
3. Какие зависимости между подзадачами
|
||||||
|
|
||||||
|
Запрос: ${instruction}
|
||||||
|
|
||||||
|
Создай задачи и распредели их через outbox.
|
||||||
|
`);
|
||||||
|
}
|
||||||
206
src/core/database.ts
Normal file
206
src/core/database.ts
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
/**
|
||||||
|
* Центральная БД компании — SQLite через better-sqlite3
|
||||||
|
* Хранит: агенты, задачи, проекты, коммуникации, логи
|
||||||
|
*/
|
||||||
|
import Database from "better-sqlite3";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
const DB_PATH = join(process.cwd(), "data", "rightvector.db");
|
||||||
|
let db: Database.Database;
|
||||||
|
|
||||||
|
export function getDb(): Database.Database {
|
||||||
|
if (!db) {
|
||||||
|
db = new Database(DB_PATH);
|
||||||
|
db.pragma("journal_mode = WAL");
|
||||||
|
db.pragma("foreign_keys = ON");
|
||||||
|
initSchema();
|
||||||
|
}
|
||||||
|
return db;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSchema() {
|
||||||
|
db.exec(`
|
||||||
|
-- Агенты компании
|
||||||
|
CREATE TABLE IF NOT EXISTS agents (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL, -- coordinator, pm, developer, qa, devops, sales
|
||||||
|
status TEXT DEFAULT 'idle', -- idle, busy, offline, error
|
||||||
|
system_prompt TEXT,
|
||||||
|
capabilities TEXT, -- JSON array
|
||||||
|
config TEXT, -- JSON config
|
||||||
|
last_heartbeat TEXT,
|
||||||
|
created_at TEXT DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Проекты клиентов
|
||||||
|
CREATE TABLE IF NOT EXISTS projects (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
client TEXT,
|
||||||
|
description TEXT,
|
||||||
|
status TEXT DEFAULT 'new', -- new, planning, in_progress, review, completed, archived
|
||||||
|
repo_url TEXT, -- Gitea repo URL
|
||||||
|
budget_hours INTEGER,
|
||||||
|
config TEXT, -- JSON
|
||||||
|
created_at TEXT DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Задачи
|
||||||
|
CREATE TABLE IF NOT EXISTS tasks (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
project_id TEXT REFERENCES projects(id),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
type TEXT DEFAULT 'feature', -- feature, bug, review, deploy, test, spec
|
||||||
|
status TEXT DEFAULT 'backlog', -- backlog, todo, in_progress, review, done, blocked
|
||||||
|
priority INTEGER DEFAULT 3, -- 1=critical, 2=high, 3=medium, 4=low
|
||||||
|
assigned_to TEXT REFERENCES agents(id),
|
||||||
|
created_by TEXT,
|
||||||
|
parent_id TEXT, -- для подзадач
|
||||||
|
branch TEXT, -- git branch
|
||||||
|
result TEXT, -- результат выполнения
|
||||||
|
created_at TEXT DEFAULT (datetime('now')),
|
||||||
|
updated_at TEXT DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Очередь сообщений между агентами
|
||||||
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
from_agent TEXT,
|
||||||
|
to_agent TEXT, -- NULL = broadcast
|
||||||
|
type TEXT DEFAULT 'message', -- message, task_assign, task_update, review_request, alert
|
||||||
|
payload TEXT NOT NULL, -- JSON
|
||||||
|
read INTEGER DEFAULT 0,
|
||||||
|
created_at TEXT DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Лог активности (аудит)
|
||||||
|
CREATE TABLE IF NOT EXISTS activity_log (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
agent_id TEXT,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
target TEXT, -- task_id, project_id, etc.
|
||||||
|
details TEXT,
|
||||||
|
duration_ms INTEGER,
|
||||||
|
created_at TEXT DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Клиенты / лиды
|
||||||
|
CREATE TABLE IF NOT EXISTS clients (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
contact TEXT, -- email, telegram, phone
|
||||||
|
channel TEXT, -- website, telegram, referral
|
||||||
|
status TEXT DEFAULT 'lead', -- lead, qualified, active, completed, churned
|
||||||
|
notes TEXT,
|
||||||
|
created_at TEXT DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === AGENTS ===
|
||||||
|
export function registerAgent(agent: {
|
||||||
|
id: string; name: string; role: string; system_prompt?: string;
|
||||||
|
capabilities?: string[]; config?: Record<string, any>;
|
||||||
|
}) {
|
||||||
|
getDb().prepare(`
|
||||||
|
INSERT OR REPLACE INTO agents (id, name, role, system_prompt, capabilities, config, last_heartbeat)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
|
||||||
|
`).run(
|
||||||
|
agent.id, agent.name, agent.role,
|
||||||
|
agent.system_prompt || "",
|
||||||
|
JSON.stringify(agent.capabilities || []),
|
||||||
|
JSON.stringify(agent.config || {})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAgent(id: string) {
|
||||||
|
return getDb().prepare("SELECT * FROM agents WHERE id = ?").get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllAgents() {
|
||||||
|
return getDb().prepare("SELECT * FROM agents ORDER BY role").all();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateAgentStatus(id: string, status: string) {
|
||||||
|
getDb().prepare("UPDATE agents SET status = ?, last_heartbeat = datetime('now') WHERE id = ?").run(status, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === TASKS ===
|
||||||
|
export function createTask(task: {
|
||||||
|
id: string; project_id?: string; title: string; description?: string;
|
||||||
|
type?: string; priority?: number; assigned_to?: string; created_by?: string; parent_id?: string;
|
||||||
|
}) {
|
||||||
|
getDb().prepare(`
|
||||||
|
INSERT INTO tasks (id, project_id, title, description, type, priority, assigned_to, created_by, parent_id)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`).run(
|
||||||
|
task.id, task.project_id || null, task.title, task.description || "",
|
||||||
|
task.type || "feature", task.priority || 3, task.assigned_to || null,
|
||||||
|
task.created_by || "system", task.parent_id || null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateTaskStatus(id: string, status: string, result?: string) {
|
||||||
|
getDb().prepare(
|
||||||
|
"UPDATE tasks SET status = ?, result = ?, updated_at = datetime('now') WHERE id = ?"
|
||||||
|
).run(status, result || null, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assignTask(taskId: string, agentId: string) {
|
||||||
|
getDb().prepare(
|
||||||
|
"UPDATE tasks SET assigned_to = ?, status = 'todo', updated_at = datetime('now') WHERE id = ?"
|
||||||
|
).run(agentId, taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTasksForAgent(agentId: string) {
|
||||||
|
return getDb().prepare(
|
||||||
|
"SELECT * FROM tasks WHERE assigned_to = ? AND status NOT IN ('done', 'blocked') ORDER BY priority"
|
||||||
|
).all(agentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProjectTasks(projectId: string) {
|
||||||
|
return getDb().prepare(
|
||||||
|
"SELECT * FROM tasks WHERE project_id = ? ORDER BY priority, created_at"
|
||||||
|
).all(projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// === MESSAGES ===
|
||||||
|
export function sendMessage(from: string, to: string | null, type: string, payload: any) {
|
||||||
|
getDb().prepare(
|
||||||
|
"INSERT INTO messages (from_agent, to_agent, type, payload) VALUES (?, ?, ?, ?)"
|
||||||
|
).run(from, to, type, JSON.stringify(payload));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUnreadMessages(agentId: string) {
|
||||||
|
const msgs = getDb().prepare(
|
||||||
|
"SELECT * FROM messages WHERE (to_agent = ? OR to_agent IS NULL) AND read = 0 ORDER BY created_at"
|
||||||
|
).all(agentId);
|
||||||
|
// mark as read
|
||||||
|
getDb().prepare(
|
||||||
|
"UPDATE messages SET read = 1 WHERE (to_agent = ? OR to_agent IS NULL) AND read = 0"
|
||||||
|
).run(agentId);
|
||||||
|
return msgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === PROJECTS ===
|
||||||
|
export function createProject(project: {
|
||||||
|
id: string; name: string; client?: string; description?: string; repo_url?: string;
|
||||||
|
}) {
|
||||||
|
getDb().prepare(
|
||||||
|
"INSERT INTO projects (id, name, client, description, repo_url) VALUES (?, ?, ?, ?, ?)"
|
||||||
|
).run(project.id, project.name, project.client || null, project.description || "", project.repo_url || null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllProjects() {
|
||||||
|
return getDb().prepare("SELECT * FROM projects ORDER BY created_at DESC").all();
|
||||||
|
}
|
||||||
|
|
||||||
|
// === ACTIVITY LOG ===
|
||||||
|
export function logActivity(agentId: string, action: string, target?: string, details?: string, durationMs?: number) {
|
||||||
|
getDb().prepare(
|
||||||
|
"INSERT INTO activity_log (agent_id, action, target, details, duration_ms) VALUES (?, ?, ?, ?, ?)"
|
||||||
|
).run(agentId, action, target || null, details || null, durationMs || null);
|
||||||
|
}
|
||||||
85
src/index.ts
Normal file
85
src/index.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { config } from "dotenv";
|
||||||
|
config();
|
||||||
|
|
||||||
|
import { getDb, registerAgent, logActivity } from "./core/database.js";
|
||||||
|
import { startApiServer } from "./api/server.js";
|
||||||
|
import { startTelegram, notifyFounder } from "./channels/telegram.js";
|
||||||
|
import { AGENT_ROSTER } from "./agents/definitions.js";
|
||||||
|
import { runAgent } from "./core/agent-runner.js";
|
||||||
|
import { logger } from "./utils/logger.js";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
logger.info(`
|
||||||
|
╔══════════════════════════════════════════════╗
|
||||||
|
║ Верный вектор — Agent Hub v0.2.0 ║
|
||||||
|
║ AI-first Software Development Company ║
|
||||||
|
╚══════════════════════════════════════════════╝`);
|
||||||
|
|
||||||
|
// 1. БД
|
||||||
|
logger.info("Initializing database...");
|
||||||
|
getDb();
|
||||||
|
|
||||||
|
// 2. Регистрация агентов
|
||||||
|
logger.info(`Registering ${AGENT_ROSTER.length} agents...`);
|
||||||
|
for (const agent of AGENT_ROSTER) {
|
||||||
|
registerAgent(agent);
|
||||||
|
logger.info(` + ${agent.name} (${agent.id})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. API
|
||||||
|
const port = parseInt(process.env.API_PORT || "18800");
|
||||||
|
startApiServer(port);
|
||||||
|
|
||||||
|
// 4. Telegram
|
||||||
|
startTelegram();
|
||||||
|
|
||||||
|
// 5. Планировщик
|
||||||
|
startScheduler();
|
||||||
|
|
||||||
|
logActivity("system", "hub:start", undefined, "Agent Hub v0.2.0 started");
|
||||||
|
logger.info("All systems operational.");
|
||||||
|
|
||||||
|
// Уведомляем основателя
|
||||||
|
setTimeout(() => {
|
||||||
|
notifyFounder("Верный вектор запущен. Все агенты на месте. Жду команд.");
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
process.on("SIGINT", () => { logger.info("Shutdown"); process.exit(0); });
|
||||||
|
process.on("SIGTERM", () => { logger.info("Shutdown"); process.exit(0); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function startScheduler() {
|
||||||
|
// DevOps: каждые 30 мин
|
||||||
|
setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const result = await runAgent("devops-agent",
|
||||||
|
"Проверь здоровье сервера: CPU, RAM, диск, Docker, сервисы. Если проблемы — алерт.",
|
||||||
|
"Плановая проверка"
|
||||||
|
);
|
||||||
|
if (result.response.toLowerCase().includes("критич") || result.response.toLowerCase().includes("ошибк")) {
|
||||||
|
await notifyFounder("⚠️ DevOps нашёл проблему:\n\n" + result.response);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.error("DevOps check failed", { error: e.message });
|
||||||
|
}
|
||||||
|
}, 30 * 60 * 1000);
|
||||||
|
|
||||||
|
// Координатор: каждый час
|
||||||
|
setInterval(async () => {
|
||||||
|
try {
|
||||||
|
await runAgent("coordinator",
|
||||||
|
"Обзор: статус проектов, незавершённые задачи, новые сообщения. Если есть блокеры — прими меры.",
|
||||||
|
"Плановый обзор"
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.error("Coordinator review failed", { error: e.message });
|
||||||
|
}
|
||||||
|
}, 60 * 60 * 1000);
|
||||||
|
|
||||||
|
logger.info("Scheduler: DevOps/30min, Coordinator/60min");
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
logger.error("Fatal", { error: err.message, stack: err.stack });
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
16
src/utils/logger.ts
Normal file
16
src/utils/logger.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { createLogger, format, transports } from "winston";
|
||||||
|
|
||||||
|
export const logger = createLogger({
|
||||||
|
level: "info",
|
||||||
|
format: format.combine(
|
||||||
|
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||||
|
format.printf(({ timestamp, level, message, ...meta }) => {
|
||||||
|
const metaStr = Object.keys(meta).length ? " " + JSON.stringify(meta) : "";
|
||||||
|
return `${timestamp} [${level.toUpperCase()}] ${message}${metaStr}`;
|
||||||
|
})
|
||||||
|
),
|
||||||
|
transports: [
|
||||||
|
new transports.Console(),
|
||||||
|
new transports.File({ filename: "data/agent-hub.log", maxsize: 5_000_000, maxFiles: 3 }),
|
||||||
|
],
|
||||||
|
});
|
||||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user