Files
GoClaw/drizzle/schema.ts
¨NW¨ 0f23dffc26 feat(agents): restore agent-worker container architecture + fix chat scroll and parallel chats
- 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
2026-04-10 15:43:33 +01:00

346 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;