COMPLETED FEATURES: 1. Database Schema (drizzle/schema.ts) - Added tasks table with 14 columns - Status enum: pending, in_progress, completed, failed, blocked - Priority enum: low, medium, high, critical - Supports task dependencies, metadata, error tracking - Indexed by agentId, status, conversationId 2. Query Helpers (server/db.ts) - createTask() - create new task - getAgentTasks() - get all agent tasks - getConversationTasks() - get conversation tasks - getTaskById() - get single task - updateTask() - update task status/results - deleteTask() - delete task - getPendingAgentTasks() - get active tasks with priority sorting 3. tRPC Endpoints (server/routers.ts) - tasks.create - create task with validation - tasks.listByAgent - list agent tasks - tasks.listByConversation - list conversation tasks - tasks.get - get single task - tasks.update - update task with partial updates - tasks.delete - delete task - tasks.getPending - get pending tasks 4. React Component (client/src/components/TasksPanel.tsx) - Right sidebar panel for task display - Checkbox for task completion - Status badges (pending, in_progress, completed, failed, blocked) - Priority indicators (low, medium, high, critical) - Expandable task details (description, result, errors, timestamps) - Real-time updates via tRPC mutations - Delete button with confirmation 5. Chat Integration (client/src/pages/Chat.tsx) - TasksPanel integrated as right sidebar - Unique conversationId per chat session - Tasks panel width: 320px (w-80) - Responsive layout with flex container 6. Auto-Task Creation (server/orchestrator.ts) - autoCreateTasks() - create tasks for missing components - detectMissingComponents() - parse error messages for missing items - trackTaskCompletion() - update task status after execution - Supports: tools, skills, agents, components, dependencies 7. Unit Tests (server/tasks.test.ts) - 5 test suites covering all operations - 107 tests pass, 1 fails (due to missing DB table) - Tests cover: create, read, update, delete operations NEXT STEPS: 1. Run pnpm db:push on production to create tasks table 2. Commit to Gitea with all changes 3. Deploy to production 4. Verify all tests pass on production DB
257 lines
11 KiB
TypeScript
257 lines
11 KiB
TypeScript
import { int, mysqlEnum, mysqlTable, text, timestamp, varchar, decimal, json, boolean, index } from "drizzle-orm/mysql-core";
|
||
|
||
/**
|
||
* Core user table backing auth flow.
|
||
* Extend this file with additional tables as your product grows.
|
||
* Columns use camelCase to match both database fields and generated types.
|
||
*/
|
||
export const users = mysqlTable("users", {
|
||
/**
|
||
* Surrogate primary key. Auto-incremented numeric value managed by the database.
|
||
* Use this for relations between tables.
|
||
*/
|
||
id: int("id").autoincrement().primaryKey(),
|
||
/** Manus OAuth identifier (openId) returned from the OAuth callback. Unique per user. */
|
||
openId: varchar("openId", { length: 64 }).notNull().unique(),
|
||
name: text("name"),
|
||
email: varchar("email", { length: 320 }),
|
||
loginMethod: varchar("loginMethod", { length: 64 }),
|
||
role: mysqlEnum("role", ["user", "admin"]).default("user").notNull(),
|
||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||
lastSignedIn: timestamp("lastSignedIn").defaultNow().notNull(),
|
||
});
|
||
|
||
export type User = typeof users.$inferSelect;
|
||
export type InsertUser = typeof users.$inferInsert;
|
||
|
||
/**
|
||
* Agents — конфигурация и управление AI-агентами
|
||
*/
|
||
export const agents = mysqlTable("agents", {
|
||
id: int("id").autoincrement().primaryKey(),
|
||
userId: int("userId").notNull(), // Владелец агента
|
||
name: varchar("name", { length: 255 }).notNull(),
|
||
description: text("description"),
|
||
role: varchar("role", { length: 100 }).notNull(), // "developer", "researcher", "executor"
|
||
|
||
// Модель LLM
|
||
model: varchar("model", { length: 100 }).notNull(),
|
||
provider: varchar("provider", { length: 50 }).notNull(),
|
||
|
||
// Параметры LLM
|
||
temperature: decimal("temperature", { precision: 3, scale: 2 }).default("0.7"),
|
||
maxTokens: int("maxTokens").default(2048),
|
||
topP: decimal("topP", { precision: 3, scale: 2 }).default("1.0"),
|
||
frequencyPenalty: decimal("frequencyPenalty", { precision: 3, scale: 2 }).default("0.0"),
|
||
presencePenalty: decimal("presencePenalty", { precision: 3, scale: 2 }).default("0.0"),
|
||
|
||
// System Prompt
|
||
systemPrompt: text("systemPrompt"),
|
||
|
||
// Доступы и разрешения
|
||
allowedTools: json("allowedTools").$type<string[]>().default([]),
|
||
allowedDomains: json("allowedDomains").$type<string[]>().default([]),
|
||
maxRequestsPerHour: int("maxRequestsPerHour").default(100),
|
||
|
||
// Статус
|
||
isActive: boolean("isActive").default(true),
|
||
isPublic: boolean("isPublic").default(false),
|
||
isSystem: boolean("isSystem").default(false), // Системный агент (нельзя удалить)
|
||
isOrchestrator: boolean("isOrchestrator").default(false), // Главный оркестратор чата
|
||
|
||
// Метаданные
|
||
tags: json("tags").$type<string[]>().default([]),
|
||
metadata: json("metadata").$type<Record<string, any>>().default({}),
|
||
|
||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||
}, (table) => ({
|
||
userIdIdx: index("agents_userId_idx").on(table.userId),
|
||
modelIdx: index("agents_model_idx").on(table.model),
|
||
}));
|
||
|
||
export type Agent = typeof agents.$inferSelect;
|
||
export type InsertAgent = typeof agents.$inferInsert;
|
||
|
||
/**
|
||
* Agent Metrics — метрики производительности агентов
|
||
*/
|
||
export const agentMetrics = mysqlTable("agentMetrics", {
|
||
id: int("id").autoincrement().primaryKey(),
|
||
agentId: int("agentId").notNull(),
|
||
|
||
// Информация о запросе
|
||
requestId: varchar("requestId", { length: 64 }).notNull().unique(),
|
||
userMessage: text("userMessage"),
|
||
agentResponse: text("agentResponse"),
|
||
|
||
// Токены
|
||
inputTokens: int("inputTokens").default(0),
|
||
outputTokens: int("outputTokens").default(0),
|
||
totalTokens: int("totalTokens").default(0),
|
||
|
||
// Время обработки
|
||
processingTimeMs: int("processingTimeMs").notNull(),
|
||
|
||
// Статус
|
||
status: mysqlEnum("status", ["success", "error", "timeout", "rate_limited"]).notNull(),
|
||
errorMessage: text("errorMessage"),
|
||
|
||
// Инструменты
|
||
toolsCalled: json("toolsCalled").$type<string[]>().default([]),
|
||
|
||
// Модель
|
||
model: varchar("model", { length: 100 }),
|
||
temperature: decimal("temperature", { precision: 3, scale: 2 }),
|
||
|
||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||
}, (table) => ({
|
||
agentIdIdx: index("agentMetrics_agentId_idx").on(table.agentId),
|
||
createdAtIdx: index("agentMetrics_createdAt_idx").on(table.createdAt),
|
||
}));
|
||
|
||
export type AgentMetric = typeof agentMetrics.$inferSelect;
|
||
export type InsertAgentMetric = typeof agentMetrics.$inferInsert;
|
||
|
||
/**
|
||
* Agent History — полная история запросов
|
||
*/
|
||
export const agentHistory = mysqlTable("agentHistory", {
|
||
id: int("id").autoincrement().primaryKey(),
|
||
agentId: int("agentId").notNull(),
|
||
|
||
userMessage: text("userMessage").notNull(),
|
||
agentResponse: text("agentResponse"),
|
||
|
||
conversationId: varchar("conversationId", { length: 64 }),
|
||
messageIndex: int("messageIndex"),
|
||
|
||
status: mysqlEnum("status", ["pending", "success", "error"]).default("pending"),
|
||
|
||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||
}, (table) => ({
|
||
agentIdIdx: index("agentHistory_agentId_idx").on(table.agentId),
|
||
}));
|
||
|
||
export type AgentHistory = typeof agentHistory.$inferSelect;
|
||
export type InsertAgentHistory = typeof agentHistory.$inferInsert;
|
||
|
||
/**
|
||
* Agent Access Control — управление доступами
|
||
*/
|
||
export const agentAccessControl = mysqlTable("agentAccessControl", {
|
||
id: int("id").autoincrement().primaryKey(),
|
||
agentId: int("agentId").notNull(),
|
||
|
||
tool: varchar("tool", { length: 50 }).notNull(),
|
||
|
||
isAllowed: boolean("isAllowed").default(true),
|
||
|
||
maxExecutionsPerHour: int("maxExecutionsPerHour").default(100),
|
||
timeoutSeconds: int("timeoutSeconds").default(30),
|
||
|
||
allowedPatterns: json("allowedPatterns").$type<string[]>().default([]),
|
||
blockedPatterns: json("blockedPatterns").$type<string[]>().default([]),
|
||
|
||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||
}, (table) => ({
|
||
agentIdToolIdx: index("agentAccessControl_agentId_tool_idx").on(table.agentId, table.tool),
|
||
}));
|
||
|
||
export type AgentAccessControl = typeof agentAccessControl.$inferSelect;
|
||
export type InsertAgentAccessControl = typeof agentAccessControl.$inferInsert;
|
||
/**
|
||
* Tool Definitions — пользовательские инструменты, созданные Tool Builder Agent
|
||
*/
|
||
export const toolDefinitions = mysqlTable("toolDefinitions", {
|
||
id: int("id").autoincrement().primaryKey(),
|
||
toolId: varchar("toolId", { length: 100 }).notNull().unique(),
|
||
name: varchar("name", { length: 255 }).notNull(),
|
||
description: text("description").notNull(),
|
||
category: varchar("category", { length: 50 }).notNull().default("custom"),
|
||
dangerous: boolean("dangerous").default(false),
|
||
parameters: json("parameters").$type<Record<string, { type: string; description: string; required?: boolean }>>(),
|
||
implementation: text("implementation").notNull(), // JS код функции
|
||
isActive: boolean("isActive").default(true),
|
||
createdBy: int("createdBy"), // agentId или null
|
||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||
});
|
||
|
||
export type ToolDefinition = typeof toolDefinitions.$inferSelect;
|
||
export type InsertToolDefinition = typeof toolDefinitions.$inferInsert;
|
||
|
||
/**
|
||
* Browser Sessions — активные сессии браузера для Browser Agent
|
||
*/
|
||
export const browserSessions = mysqlTable("browserSessions", {
|
||
id: int("id").autoincrement().primaryKey(),
|
||
sessionId: varchar("sessionId", { length: 64 }).notNull().unique(),
|
||
agentId: int("agentId").notNull(),
|
||
currentUrl: text("currentUrl"),
|
||
title: text("title"),
|
||
status: mysqlEnum("status", ["active", "idle", "closed", "error"]).default("idle"),
|
||
screenshotUrl: text("screenshotUrl"), // S3 URL последнего скриншота
|
||
lastActionAt: timestamp("lastActionAt").defaultNow(),
|
||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||
closedAt: timestamp("closedAt"),
|
||
});
|
||
|
||
export type BrowserSession = typeof browserSessions.$inferSelect;
|
||
export type InsertBrowserSession = typeof browserSessions.$inferInsert;
|
||
|
||
/**
|
||
* Node Metrics — исторические метрики Docker-контейнеров/нод (сохраняется каждые 30s)
|
||
*/
|
||
export const nodeMetrics = mysqlTable("nodeMetrics", {
|
||
id: int("id").autoincrement().primaryKey(),
|
||
containerId: varchar("containerId", { length: 64 }).notNull(),
|
||
containerName: varchar("containerName", { length: 255 }).notNull(),
|
||
cpuPercent: decimal("cpuPercent", { precision: 6, scale: 2 }).notNull().default("0.00"),
|
||
memUsedMb: decimal("memUsedMb", { precision: 10, scale: 2 }).notNull().default("0.00"),
|
||
memLimitMb: decimal("memLimitMb", { precision: 10, scale: 2 }).notNull().default("0.00"),
|
||
status: varchar("status", { length: 32 }).notNull().default("running"),
|
||
recordedAt: timestamp("recordedAt").defaultNow().notNull(),
|
||
}, (table) => ({
|
||
containerIdIdx: index("nodeMetrics_containerId_idx").on(table.containerId),
|
||
recordedAtIdx: index("nodeMetrics_recordedAt_idx").on(table.recordedAt),
|
||
}));
|
||
|
||
export type NodeMetric = typeof nodeMetrics.$inferSelect;
|
||
export type InsertNodeMetric = typeof nodeMetrics.$inferInsert;
|
||
|
||
/**
|
||
* Tasks — задачи, создаваемые агентами для отслеживания работы
|
||
*/
|
||
export const tasks = mysqlTable("tasks", {
|
||
id: int("id").autoincrement().primaryKey(),
|
||
agentId: int("agentId").notNull(),
|
||
conversationId: varchar("conversationId", { length: 64 }),
|
||
|
||
title: varchar("title", { length: 255 }).notNull(),
|
||
description: text("description"),
|
||
|
||
status: mysqlEnum("status", ["pending", "in_progress", "completed", "failed", "blocked"]).default("pending").notNull(),
|
||
priority: mysqlEnum("priority", ["low", "medium", "high", "critical"]).default("medium").notNull(),
|
||
|
||
dependsOn: json("dependsOn").$type<number[]>().default([]),
|
||
|
||
result: text("result"),
|
||
errorMessage: text("errorMessage"),
|
||
|
||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||
startedAt: timestamp("startedAt"),
|
||
completedAt: timestamp("completedAt"),
|
||
|
||
metadata: json("metadata").$type<Record<string, any>>().default({}),
|
||
}, (table) => ({
|
||
agentIdIdx: index("tasks_agentId_idx").on(table.agentId),
|
||
statusIdx: index("tasks_status_idx").on(table.status),
|
||
conversationIdIdx: index("tasks_conversationId_idx").on(table.conversationId),
|
||
}));
|
||
|
||
export type Task = typeof tasks.$inferSelect;
|
||
export type InsertTask = typeof tasks.$inferInsert;
|