- Restore agent-worker from commit 153399f: autonomous HTTP server per agent
(main.go 597 lines, main_test.go 438 lines, Dockerfile.agent-worker)
- Add container fields to agents table (serviceName, servicePort, containerImage, containerStatus)
- Update executor.go: real delegateToAgent() with HTTP POST to agent containers
- Update db.go: GetAgentByID, UpdateContainerStatus, GetAgentHistory, SaveHistory
- Update orchestrator.go: inject DB into executor for container address resolution
- Add tRPC endpoints: agents.deployContainer, agents.stopContainer, agents.containerStatus
- Add Docker Swarm deploy/stop logic in server/agents.ts
- Add Start/Stop container buttons to Agents.tsx with status badges
- Fix chat auto-scroll: replace ScrollArea with overflow-y-auto for direct scrollTop control
- Fix parallel chats: make isThinking per-conversation (thinkingConvId) instead of global
so switching between chats works while one is processing
346 lines
12 KiB
TypeScript
346 lines
12 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), // Главный оркестратор чата
|
||
|
||
// Docker Swarm / Container fields (Phase A)
|
||
serviceName: varchar("serviceName", { length: 100 }), // Docker Swarm service name: goclaw-agent-{id}
|
||
servicePort: int("servicePort"), // HTTP API port inside overlay network (8001-8999)
|
||
containerImage: varchar("containerImage", { length: 255 }).default(
|
||
"goclaw-agent-worker:latest"
|
||
), // Docker image to run
|
||
containerStatus: mysqlEnum("containerStatus", [
|
||
"stopped",
|
||
"deploying",
|
||
"running",
|
||
"error",
|
||
]).default("stopped"), // Container lifecycle state
|
||
|
||
// Метаданные
|
||
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;
|