Files
GoClaw/server/tools.ts
2026-03-20 17:34:20 -04:00

359 lines
11 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 { getAgentById, getAgentAccessControl } from "./agents";
/**
* Определение инструмента
*/
export interface ToolDefinition {
id: string;
name: string;
description: string;
category: "browser" | "shell" | "file" | "docker" | "http" | "system" | "custom" | "web" | "data" | "ai";
icon?: string;
parameters: Record<string, { type: string; description: string; required?: boolean }>;
dangerous: boolean;
/** Optional custom execute function for dynamically registered tools */
execute?: (params: Record<string, any>) => Promise<any>;
}
/**
* Реестр доступных инструментов
*/
export const TOOL_REGISTRY: ToolDefinition[] = [
{
id: "http_get",
name: "HTTP GET",
description: "Выполнить GET-запрос к URL",
category: "http",
icon: "Globe",
parameters: {
url: { type: "string", description: "URL для запроса", required: true },
headers: { type: "object", description: "Заголовки запроса" },
},
dangerous: false,
},
{
id: "http_post",
name: "HTTP POST",
description: "Выполнить POST-запрос к URL с телом",
category: "http",
icon: "Send",
parameters: {
url: { type: "string", description: "URL для запроса", required: true },
body: { type: "object", description: "Тело запроса" },
headers: { type: "object", description: "Заголовки запроса" },
},
dangerous: false,
},
{
id: "shell_exec",
name: "Shell Execute",
description: "Выполнить bash-команду в изолированной среде",
category: "shell",
icon: "Terminal",
parameters: {
command: { type: "string", description: "Команда для выполнения", required: true },
timeout: { type: "number", description: "Таймаут в секундах (по умолчанию 30)" },
},
dangerous: true,
},
{
id: "file_read",
name: "File Read",
description: "Прочитать содержимое файла",
category: "file",
icon: "FileText",
parameters: {
path: { type: "string", description: "Путь к файлу", required: true },
},
dangerous: false,
},
{
id: "file_write",
name: "File Write",
description: "Записать содержимое в файл",
category: "file",
icon: "FilePlus",
parameters: {
path: { type: "string", description: "Путь к файлу", required: true },
content: { type: "string", description: "Содержимое файла", required: true },
},
dangerous: true,
},
{
id: "docker_list",
name: "Docker List",
description: "Получить список контейнеров Docker",
category: "docker",
icon: "Box",
parameters: {
all: { type: "boolean", description: "Показать все контейнеры (включая остановленные)" },
},
dangerous: false,
},
{
id: "docker_exec",
name: "Docker Exec",
description: "Выполнить команду в контейнере Docker",
category: "docker",
icon: "Play",
parameters: {
container: { type: "string", description: "ID или имя контейнера", required: true },
command: { type: "string", description: "Команда для выполнения", required: true },
},
dangerous: true,
},
{
id: "docker_logs",
name: "Docker Logs",
description: "Получить логи контейнера",
category: "docker",
icon: "FileText",
parameters: {
container: { type: "string", description: "ID или имя контейнера", required: true },
tail: { type: "number", description: "Количество последних строк (по умолчанию 100)" },
},
dangerous: false,
},
{
id: "browser_navigate",
name: "Browser Navigate",
description: "Открыть URL в браузере и получить содержимое",
category: "browser",
icon: "Globe",
parameters: {
url: { type: "string", description: "URL для открытия", required: true },
},
dangerous: false,
},
{
id: "browser_screenshot",
name: "Browser Screenshot",
description: "Сделать скриншот текущей страницы",
category: "browser",
icon: "Camera",
parameters: {},
dangerous: false,
},
];
/**
* Получить все доступные инструменты
*/
export function getAllTools(): ToolDefinition[] {
return TOOL_REGISTRY;
}
/**
* Получить инструмент по ID
*/
export function getToolById(id: string): ToolDefinition | undefined {
return TOOL_REGISTRY.find((t) => t.id === id);
}
/**
* Выполнить инструмент от имени агента
*/
export async function executeTool(
agentId: number,
toolId: string,
params: Record<string, any>
): Promise<{ success: boolean; result?: any; error?: string; executionTimeMs: number }> {
const startTime = Date.now();
// Проверяем существование агента
const agent = await getAgentById(agentId);
if (!agent) {
return { success: false, error: "Agent not found", executionTimeMs: 0 };
}
// Проверяем доступность инструмента
const tool = getToolById(toolId);
if (!tool) {
return { success: false, error: `Unknown tool: ${toolId}`, executionTimeMs: 0 };
}
// Проверяем разрешения агента
const accessControls = await getAgentAccessControl(agentId);
const toolAccess = accessControls.find((ac) => ac.tool === toolId);
if (toolAccess && !toolAccess.isAllowed) {
return {
success: false,
error: `Tool '${toolId}' is blocked for this agent`,
executionTimeMs: Date.now() - startTime,
};
}
// Проверяем, есть ли инструмент в allowedTools агента
const allowedTools = (agent.allowedTools as string[]) || [];
if (allowedTools.length > 0 && !allowedTools.includes(toolId)) {
return {
success: false,
error: `Tool '${toolId}' is not in agent's allowed tools list`,
executionTimeMs: Date.now() - startTime,
};
}
try {
const result = await executeToolImpl(toolId, params, toolAccess);
return {
success: true,
result,
executionTimeMs: Date.now() - startTime,
};
} catch (err: any) {
return {
success: false,
error: err.message,
executionTimeMs: Date.now() - startTime,
};
}
}
/**
* Реализация выполнения инструментов
*/
async function executeToolImpl(
toolId: string,
params: Record<string, any>,
_accessControl?: any
): Promise<any> {
switch (toolId) {
case "http_get": {
const response = await fetch(params.url, {
headers: params.headers || {},
signal: AbortSignal.timeout(30000),
});
const text = await response.text();
return {
status: response.status,
statusText: response.statusText,
body: text.slice(0, 10000), // Limit response size
headers: Object.fromEntries(response.headers.entries()),
};
}
case "http_post": {
const response = await fetch(params.url, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(params.headers || {}),
},
body: JSON.stringify(params.body || {}),
signal: AbortSignal.timeout(30000),
});
const text = await response.text();
return {
status: response.status,
statusText: response.statusText,
body: text.slice(0, 10000),
headers: Object.fromEntries(response.headers.entries()),
};
}
case "shell_exec": {
const { exec } = await import("child_process");
const { promisify } = await import("util");
const execAsync = promisify(exec);
const timeout = (params.timeout || 30) * 1000;
// Safety: block dangerous commands
const blockedPatterns = ["rm -rf /", "mkfs", "dd if=", ":(){ :|:& };:"];
for (const pattern of blockedPatterns) {
if (params.command.includes(pattern)) {
throw new Error(`Command blocked for safety: contains '${pattern}'`);
}
}
const { stdout, stderr } = await execAsync(params.command, { timeout });
return { stdout: stdout.slice(0, 10000), stderr: stderr.slice(0, 2000) };
}
case "file_read": {
const { readFile } = await import("fs/promises");
const content = await readFile(params.path, "utf-8");
return { content: content.slice(0, 50000), size: content.length };
}
case "file_write": {
const { writeFile, mkdir } = await import("fs/promises");
const { dirname } = await import("path");
await mkdir(dirname(params.path), { recursive: true });
await writeFile(params.path, params.content, "utf-8");
return { written: params.content.length, path: params.path };
}
case "docker_list": {
const { exec } = await import("child_process");
const { promisify } = await import("util");
const execAsync = promisify(exec);
const flag = params.all ? "-a" : "";
const { stdout } = await execAsync(`docker ps ${flag} --format json`);
const containers = stdout
.trim()
.split("\n")
.filter(Boolean)
.map((line) => {
try {
return JSON.parse(line);
} catch {
return line;
}
});
return { containers };
}
case "docker_exec": {
const { exec } = await import("child_process");
const { promisify } = await import("util");
const execAsync = promisify(exec);
const { stdout, stderr } = await execAsync(
`docker exec ${params.container} ${params.command}`,
{ timeout: 30000 }
);
return { stdout: stdout.slice(0, 10000), stderr: stderr.slice(0, 2000) };
}
case "docker_logs": {
const { exec } = await import("child_process");
const { promisify } = await import("util");
const execAsync = promisify(exec);
const tail = params.tail || 100;
const { stdout, stderr } = await execAsync(
`docker logs --tail ${tail} ${params.container}`,
{ timeout: 15000 }
);
return { logs: (stdout + stderr).slice(0, 20000) };
}
case "browser_navigate": {
// Simple HTTP fetch as browser substitute (no JS rendering)
const response = await fetch(params.url, {
headers: {
"User-Agent":
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
},
signal: AbortSignal.timeout(30000),
});
const html = await response.text();
// Strip HTML tags for readable output
const text = html
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "")
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "")
.replace(/<[^>]+>/g, " ")
.replace(/\s+/g, " ")
.trim()
.slice(0, 10000);
return { url: params.url, status: response.status, text };
}
case "browser_screenshot": {
return { error: "Screenshot requires headless browser (not available in this environment)" };
}
default:
throw new Error(`Tool '${toolId}' not implemented`);
}
}