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:
Верный вектор 2026-04-10 18:56:19 +07:00
commit a87896a6f0
11 changed files with 3023 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
dist/
data/
.env

1857
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
package.json Normal file
View 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
View 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
View 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
View 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
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
View 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
View 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
View 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
View 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
View 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/**/*"]
}