Files
GoClaw/server/routers.ts
Manus 0dcae37a78 Checkpoint: Phase 12: Real-time Docker Swarm monitoring for /nodes page
Реализовано:
- gateway/internal/docker/client.go: Docker API клиент через unix socket (/var/run/docker.sock)
  - IsSwarmActive(), GetSwarmInfo(), ListNodes(), ListContainers(), GetContainerStats()
  - CalcCPUPercent() для расчёта CPU%
- gateway/internal/api/handlers.go: новые endpoints
  - GET /api/nodes: список Swarm нод или standalone Docker хост
  - GET /api/nodes/stats: live CPU/RAM статистика контейнеров
  - POST /api/tools/execute: выполнение инструментов
- gateway/cmd/gateway/main.go: зарегистрированы новые маршруты
- server/gateway-proxy.ts: добавлены getGatewayNodes() и getGatewayNodeStats()
- server/routers.ts: добавлен nodes router (nodes.list, nodes.stats)
- client/src/pages/Nodes.tsx: полностью переписан на реальные данные
  - Auto-refresh: 10s для нод, 15s для статистики контейнеров
  - Swarm mode: показывает все ноды кластера
  - Standalone mode: показывает локальный Docker хост + контейнеры
  - CPU/RAM gauges из реальных docker stats
  - Error state при недоступном Gateway
  - Loading skeleton
- server/nodes.test.ts: 14 новых vitest тестов
- Все 51 тест пройдены
2026-03-20 20:12:57 -04:00

631 lines
20 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 { COOKIE_NAME } from "@shared/const";
import { z } from "zod";
import { getSessionCookieOptions } from "./_core/cookies";
import { systemRouter } from "./_core/systemRouter";
import { publicProcedure, router, protectedProcedure } from "./_core/trpc";
import { checkOllamaHealth, listModels, chatCompletion } from "./ollama";
import {
checkGatewayHealth,
gatewayChat,
getGatewayOrchestratorConfig,
getGatewayModels,
getGatewayTools,
executeGatewayTool,
isGatewayAvailable,
getGatewayNodes,
getGatewayNodeStats,
} from "./gateway-proxy";
// Shared system user id for non-authenticated agent management
const SYSTEM_USER_ID = 1;
export const appRouter = router({
system: systemRouter,
auth: router({
me: publicProcedure.query(opts => opts.ctx.user),
logout: publicProcedure.mutation(({ ctx }) => {
const cookieOptions = getSessionCookieOptions(ctx.req);
ctx.res.clearCookie(COOKIE_NAME, { ...cookieOptions, maxAge: -1 });
return { success: true } as const;
}),
}),
/**
* Ollama API — серверный прокси для безопасного доступа
* Приоритет: Go Gateway → прямой Ollama
*/
ollama: router({
health: publicProcedure.query(async () => {
// Try Go Gateway first, fall back to direct Ollama
const gwHealth = await checkGatewayHealth();
if (gwHealth.connected) {
return {
connected: true as const,
latencyMs: gwHealth.latencyMs,
source: "gateway" as const,
llm: gwHealth.llm,
error: undefined as string | undefined,
};
}
// Fallback: direct Ollama
const ollamaHealth = await checkOllamaHealth();
return { ...ollamaHealth, source: "direct" as const };
}),
models: publicProcedure.query(async () => {
// Try Go Gateway first
const gwModels = await getGatewayModels();
if (gwModels) {
return {
success: true as const,
models: gwModels.data ?? [],
source: "gateway" as const,
};
}
// Fallback: direct Ollama
try {
const result = await listModels();
return {
success: true as const,
models: result.data ?? [],
source: "direct" as const,
};
} catch (err: any) {
return {
success: false as const,
models: [],
error: err.message,
};
}
}),
chat: publicProcedure
.input(
z.object({
model: z.string(),
messages: z.array(
z.object({
role: z.enum(["system", "user", "assistant"]),
content: z.string(),
})
),
temperature: z.number().optional(),
max_tokens: z.number().optional(),
})
)
.mutation(async ({ input }) => {
try {
const result = await chatCompletion(input.model, input.messages, {
temperature: input.temperature,
max_tokens: input.max_tokens,
});
return {
success: true as const,
response: result.choices[0]?.message?.content ?? "",
model: result.model,
usage: result.usage,
};
} catch (err: any) {
return {
success: false as const,
response: "",
error: err.message,
};
}
}),
}),
/**
* Agents — управление AI-агентами (public — внутренний инструмент)
*/
agents: router({
list: publicProcedure.query(async () => {
const { getUserAgents } = await import("./agents");
return getUserAgents(SYSTEM_USER_ID);
}),
get: publicProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
const { getAgentById } = await import("./agents");
return getAgentById(input.id);
}),
create: publicProcedure
.input(
z.object({
name: z.string().min(1),
description: z.string().optional(),
role: z.string(),
model: z.string(),
provider: z.string(),
temperature: z.number().min(0).max(2).default(0.7),
maxTokens: z.number().default(2048),
topP: z.number().min(0).max(1).default(1.0),
frequencyPenalty: z.number().min(-2).max(2).default(0.0),
presencePenalty: z.number().min(-2).max(2).default(0.0),
systemPrompt: z.string().optional(),
allowedTools: z.array(z.string()).default([]),
allowedDomains: z.array(z.string()).default([]),
tags: z.array(z.string()).default([]),
})
)
.mutation(async ({ input }) => {
const { createAgent } = await import("./agents");
return createAgent(SYSTEM_USER_ID, {
...input,
temperature: input.temperature.toString(),
topP: input.topP.toString(),
frequencyPenalty: input.frequencyPenalty.toString(),
presencePenalty: input.presencePenalty.toString(),
} as any);
}),
update: publicProcedure
.input(
z.object({
id: z.number(),
name: z.string().optional(),
description: z.string().optional(),
model: z.string().optional(),
provider: z.string().optional(),
temperature: z.number().min(0).max(2).optional(),
maxTokens: z.number().optional(),
topP: z.number().min(0).max(1).optional(),
frequencyPenalty: z.number().min(-2).max(2).optional(),
presencePenalty: z.number().min(-2).max(2).optional(),
systemPrompt: z.string().optional(),
allowedTools: z.array(z.string()).optional(),
allowedDomains: z.array(z.string()).optional(),
isActive: z.boolean().optional(),
tags: z.array(z.string()).optional(),
})
)
.mutation(async ({ input }) => {
const { updateAgent } = await import("./agents");
const { id, temperature, topP, frequencyPenalty, presencePenalty, ...rest } = input;
const updates: Record<string, any> = { ...rest };
if (temperature !== undefined) updates.temperature = temperature.toString();
if (topP !== undefined) updates.topP = topP.toString();
if (frequencyPenalty !== undefined) updates.frequencyPenalty = frequencyPenalty.toString();
if (presencePenalty !== undefined) updates.presencePenalty = presencePenalty.toString();
return updateAgent(id, updates as any);
}),
delete: publicProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
const { deleteAgent } = await import("./agents");
return deleteAgent(input.id);
}),
stats: publicProcedure.input(z.object({ id: z.number(), hoursBack: z.number().default(24) })).query(async ({ input }) => {
const { getAgentStats } = await import("./agents");
return getAgentStats(input.id, input.hoursBack);
}),
metrics: publicProcedure.input(z.object({ id: z.number(), hoursBack: z.number().default(24) })).query(async ({ input }) => {
const { getAgentMetrics } = await import("./agents");
return getAgentMetrics(input.id, input.hoursBack);
}),
history: publicProcedure.input(z.object({ id: z.number(), limit: z.number().default(50) })).query(async ({ input }) => {
const { getAgentHistory } = await import("./agents");
return getAgentHistory(input.id, input.limit);
}),
accessControl: publicProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
const { getAgentAccessControl } = await import("./agents");
return getAgentAccessControl(input.id);
}),
updateToolAccess: publicProcedure
.input(
z.object({
agentId: z.number(),
tool: z.string(),
isAllowed: z.boolean(),
maxExecutionsPerHour: z.number().optional(),
timeoutSeconds: z.number().optional(),
allowedPatterns: z.array(z.string()).optional(),
blockedPatterns: z.array(z.string()).optional(),
})
)
.mutation(async ({ input }) => {
const { updateToolAccess } = await import("./agents");
const { agentId, ...updates } = input;
return updateToolAccess(agentId, input.tool, updates);
}),
/**
* Chat with a specific agent using its configuration
*/
chat: publicProcedure
.input(
z.object({
agentId: z.number(),
message: z.string(),
conversationId: z.string().optional(),
})
)
.mutation(async ({ input }) => {
const { getAgentById, saveHistory } = await import("./agents");
const agent = await getAgentById(input.agentId);
if (!agent) {
return { success: false as const, response: "", error: "Agent not found" };
}
const messages: Array<{ role: "system" | "user" | "assistant"; content: string }> = [];
if (agent.systemPrompt) {
messages.push({ role: "system", content: agent.systemPrompt });
}
messages.push({ role: "user", content: input.message });
const startTime = Date.now();
try {
const result = await chatCompletion(
agent.model,
messages,
{
temperature: agent.temperature ? parseFloat(agent.temperature as string) : 0.7,
max_tokens: agent.maxTokens ?? 2048,
}
);
const processingTimeMs = Date.now() - startTime;
const response = result.choices[0]?.message?.content ?? "";
// Save to history
await saveHistory(input.agentId, {
userMessage: input.message,
agentResponse: response,
conversationId: input.conversationId,
status: "success",
});
return {
success: true as const,
response,
model: result.model,
usage: result.usage,
processingTimeMs,
};
} catch (err: any) {
await saveHistory(input.agentId, {
userMessage: input.message,
agentResponse: null,
conversationId: input.conversationId,
status: "error",
});
return {
success: false as const,
response: "",
error: err.message,
};
}
}),
}),
/**
* Tools — управление инструментами агентов
*/
tools: router({
list: publicProcedure.query(async () => {
const { getAllTools } = await import("./tools");
return getAllTools();
}),
execute: publicProcedure
.input(
z.object({
agentId: z.number(),
tool: z.string(),
params: z.record(z.string(), z.unknown()),
})
)
.mutation(async ({ input }) => {
const { executeTool } = await import("./tools");
return executeTool(input.agentId, input.tool, input.params);
}),
}),
/**
* Browser Agent — управление браузерными сессиями через Puppeteer
*/
browser: router({
createSession: publicProcedure
.input(z.object({ agentId: z.number() }))
.mutation(async ({ input }) => {
const { createBrowserSession } = await import("./browser-agent");
return createBrowserSession(input.agentId);
}),
execute: publicProcedure
.input(
z.object({
sessionId: z.string(),
action: z.object({
type: z.enum(["navigate", "click", "type", "extract", "screenshot", "scroll", "wait", "evaluate", "close"]),
params: z.record(z.string(), z.unknown()),
}),
})
)
.mutation(async ({ input }) => {
const { executeBrowserAction } = await import("./browser-agent");
return executeBrowserAction(input.sessionId, input.action as any);
}),
getSessions: publicProcedure
.input(z.object({ agentId: z.number() }))
.query(async ({ input }) => {
const { getAgentSessions } = await import("./browser-agent");
return getAgentSessions(input.agentId);
}),
closeSession: publicProcedure
.input(z.object({ sessionId: z.string() }))
.mutation(async ({ input }) => {
const { executeBrowserAction } = await import("./browser-agent");
return executeBrowserAction(input.sessionId, { type: "close", params: {} });
}),
closeAllSessions: publicProcedure
.input(z.object({ agentId: z.number() }))
.mutation(async ({ input }) => {
const { closeAllAgentSessions } = await import("./browser-agent");
await closeAllAgentSessions(input.agentId);
return { success: true };
}),
}),
/**
* Tool Builder — генерация и установка новых инструментов через LLM
*/
toolBuilder: router({
generate: publicProcedure
.input(
z.object({
name: z.string().min(1),
description: z.string().min(10),
category: z.string().optional(),
exampleInput: z.string().optional(),
exampleOutput: z.string().optional(),
dangerous: z.boolean().optional(),
})
)
.mutation(async ({ input }) => {
const { generateTool } = await import("./tool-builder");
return generateTool(input);
}),
install: publicProcedure
.input(
z.object({
toolId: z.string(),
name: z.string(),
description: z.string(),
category: z.string(),
dangerous: z.boolean(),
parameters: z.record(z.string(), z.object({
type: z.string(),
description: z.string(),
required: z.boolean().optional(),
})),
implementation: z.string(),
})
)
.mutation(async ({ input }) => {
const { installTool } = await import("./tool-builder");
return installTool(input);
}),
listCustom: publicProcedure.query(async () => {
const { getCustomTools } = await import("./tool-builder");
return getCustomTools();
}),
delete: publicProcedure
.input(z.object({ toolId: z.string() }))
.mutation(async ({ input }) => {
const { deleteTool } = await import("./tool-builder");
return deleteTool(input.toolId);
}),
test: publicProcedure
.input(
z.object({
toolId: z.string(),
params: z.record(z.string(), z.unknown()),
})
)
.mutation(async ({ input }) => {
const { testTool } = await import("./tool-builder");
return testTool(input.toolId, input.params);
}),
}),
/**
* Agent Compiler — компиляция агентов по ТЗ через LLM
*/
agentCompiler: router({
compile: publicProcedure
.input(
z.object({
specification: z.string().min(20),
name: z.string().optional(),
preferredProvider: z.string().optional(),
preferredModel: z.string().optional(),
})
)
.mutation(async ({ input }) => {
const { compileAgentConfig } = await import("./agent-compiler");
return compileAgentConfig({ ...input, userId: SYSTEM_USER_ID });
}),
deploy: publicProcedure
.input(
z.object({
config: z.object({
name: z.string(),
description: z.string(),
role: z.string(),
model: z.string(),
provider: z.string(),
temperature: z.number(),
maxTokens: z.number(),
topP: z.number(),
frequencyPenalty: z.number(),
presencePenalty: z.number(),
systemPrompt: z.string(),
allowedTools: z.array(z.string()),
allowedDomains: z.array(z.string()),
maxRequestsPerHour: z.number(),
tags: z.array(z.string()),
reasoning: z.string(),
}),
})
)
.mutation(async ({ input }) => {
const { deployCompiledAgent } = await import("./agent-compiler");
return deployCompiledAgent(input.config, SYSTEM_USER_ID);
}),
compileAndDeploy: publicProcedure
.input(
z.object({
specification: z.string().min(20),
name: z.string().optional(),
preferredProvider: z.string().optional(),
preferredModel: z.string().optional(),
})
)
.mutation(async ({ input }) => {
const { compileAndDeployAgent } = await import("./agent-compiler");
return compileAndDeployAgent({ ...input, userId: SYSTEM_USER_ID });
}),
}),
/**
* Orchestrator — main AI agent with tool-use loop
* Приоритет: Go Gateway → Node.js fallback
*/
orchestrator: router({
// Get orchestrator config — Go Gateway first, then Node.js DB
getConfig: publicProcedure.query(async () => {
const gwConfig = await getGatewayOrchestratorConfig();
if (gwConfig) {
return { ...gwConfig, source: "gateway" as const };
}
// Fallback: Node.js orchestrator reads from DB directly
const { getOrchestratorConfig } = await import("./orchestrator");
const config = await getOrchestratorConfig();
return { ...config, source: "direct" as const };
}),
chat: publicProcedure
.input(
z.object({
messages: z.array(
z.object({
role: z.enum(["user", "assistant", "system"]),
content: z.string(),
})
),
model: z.string().optional(),
maxIterations: z.number().min(1).max(20).optional(),
})
)
.mutation(async ({ input }) => {
// Try Go Gateway first (preferred — full Go tool-use loop)
const gwAvailable = await isGatewayAvailable();
if (gwAvailable) {
const result = await gatewayChat(
input.messages,
input.model,
input.maxIterations ?? 10
);
return { ...result, source: "gateway" as const };
}
// Fallback: Node.js orchestrator
const { orchestratorChat } = await import("./orchestrator");
const result = await orchestratorChat(
input.messages,
input.model,
input.maxIterations ?? 10
);
return { ...result, source: "direct" as const };
}),
// List available tools — Go Gateway first
tools: publicProcedure.query(async () => {
const gwTools = await getGatewayTools();
if (gwTools) {
return gwTools.map((t) => ({
name: t.name,
description: t.description,
parameters: t.parameters,
source: "gateway" as const,
}));
}
// Fallback: Node.js tool definitions
const { ORCHESTRATOR_TOOLS } = await import("./orchestrator");
return ORCHESTRATOR_TOOLS.map((t) => ({
name: t.function.name,
description: t.function.description,
parameters: t.function.parameters,
source: "direct" as const,
}));
}),
// Gateway health check
gatewayHealth: publicProcedure.query(async () => {
return checkGatewayHealth();
}),
// Execute a single tool via Go Gateway
executeTool: publicProcedure
.input(
z.object({
tool: z.string(),
args: z.record(z.string(), z.unknown()),
})
)
.mutation(async ({ input }) => {
return executeGatewayTool(input.tool, input.args);
}),
}),
/**
* Nodes — Docker Swarm / standalone Docker monitoring via Go Gateway
*/
nodes: router({
/**
* List all Swarm nodes (or standalone Docker host if Swarm not active).
* Returns node info: hostname, role, status, resources, labels, etc.
*/
list: publicProcedure.query(async () => {
const result = await getGatewayNodes();
if (!result) {
return {
nodes: [] as import("./gateway-proxy").GatewayNodeInfo[],
count: 0,
swarmActive: false,
fetchedAt: new Date().toISOString(),
error: "Gateway unavailable — is the Go Gateway running?",
};
}
return result;
}),
/**
* Get live container stats (CPU%, RAM) for all running containers.
*/
stats: publicProcedure.query(async () => {
const result = await getGatewayNodeStats();
if (!result) {
return {
stats: [] as import("./gateway-proxy").GatewayContainerStat[],
count: 0,
fetchedAt: new Date().toISOString(),
error: "Gateway unavailable",
};
}
return result;
}),
}),
});
export type AppRouter = typeof appRouter;