arcadiasuite/server/ide/routes.ts

448 lines
13 KiB
TypeScript

import { Router, Request, Response, NextFunction } from "express";
import * as fs from "fs/promises";
import * as path from "path";
import { exec } from "child_process";
import { promisify } from "util";
import OpenAI from "openai";
import { manusService } from "../manus/service";
const execAsync = promisify(exec);
const router = Router();
function requireAuth(req: Request, res: Response, next: NextFunction) {
if (!req.isAuthenticated || !req.isAuthenticated()) {
return res.status(401).json({ error: "Not authenticated" });
}
next();
}
const openai = new OpenAI({
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
});
const ALLOWED_DIRS = ["python-service", "server", "client/src", "shared"];
const BLOCKED_COMMANDS = ["rm -rf", "rm -r /", "mkfs", "dd if=", ":(){ :|:& };:"];
interface FileNode {
name: string;
path: string;
type: "file" | "directory";
children?: FileNode[];
}
async function buildFileTree(dirPath: string, relativePath: string = ""): Promise<FileNode[]> {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
const nodes: FileNode[] = [];
for (const entry of entries) {
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "__pycache__") {
continue;
}
const fullPath = path.join(dirPath, entry.name);
const relPath = path.join(relativePath, entry.name);
if (entry.isDirectory()) {
const children = await buildFileTree(fullPath, relPath);
nodes.push({
name: entry.name,
path: relPath,
type: "directory",
children,
});
} else {
nodes.push({
name: entry.name,
path: relPath,
type: "file",
});
}
}
return nodes.sort((a, b) => {
if (a.type === b.type) return a.name.localeCompare(b.name);
return a.type === "directory" ? -1 : 1;
});
}
router.get("/files", requireAuth, async (req, res) => {
try {
const allFiles: FileNode[] = [];
for (const dir of ALLOWED_DIRS) {
try {
const dirPath = path.resolve(process.cwd(), dir);
const children = await buildFileTree(dirPath, dir);
allFiles.push({
name: dir,
path: dir,
type: "directory",
children,
});
} catch (e) {
}
}
res.json(allFiles);
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
router.get("/file", requireAuth, async (req, res) => {
try {
const filePath = req.query.path as string;
if (!filePath) {
return res.status(400).json({ error: "Path required" });
}
const isAllowed = ALLOWED_DIRS.some(dir => filePath.startsWith(dir));
if (!isAllowed || filePath.includes("..")) {
return res.status(403).json({ error: "Access denied" });
}
const fullPath = path.resolve(process.cwd(), filePath);
const content = await fs.readFile(fullPath, "utf-8");
res.type("text/plain").send(content);
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
router.post("/file", requireAuth, async (req, res) => {
try {
const { path: filePath, content } = req.body;
if (!filePath || content === undefined) {
return res.status(400).json({ error: "Path and content required" });
}
const isAllowed = ALLOWED_DIRS.some(dir => filePath.startsWith(dir));
if (!isAllowed || filePath.includes("..")) {
return res.status(403).json({ error: "Access denied" });
}
const fullPath = path.resolve(process.cwd(), filePath);
await fs.mkdir(path.dirname(fullPath), { recursive: true });
await fs.writeFile(fullPath, content, "utf-8");
res.json({ success: true, path: filePath });
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
router.post("/execute", requireAuth, async (req, res) => {
try {
const { command } = req.body;
if (!command) {
return res.status(400).json({ error: "Command required" });
}
if (BLOCKED_COMMANDS.some(bc => command.includes(bc))) {
return res.status(403).json({ error: "Comando bloqueado por segurança" });
}
const { stdout, stderr } = await execAsync(command, { timeout: 30000 });
res.json({
success: true,
command,
output: stdout,
error: stderr
});
} catch (error: any) {
res.json({
success: false,
command: req.body.command,
error: error.message
});
}
});
router.post("/ai-generate", requireAuth, async (req, res) => {
try {
const { prompt, currentFile, currentContent } = req.body;
if (!prompt) {
return res.status(400).json({ error: "Prompt required" });
}
const systemPrompt = `Você é um assistente de programação especializado. Gere código limpo, bem documentado e funcional.
Se um arquivo atual foi fornecido, considere seu contexto ao gerar o código.
Responda APENAS com o código, sem explicações adicionais.`;
const userPrompt = currentFile
? `Arquivo atual: ${currentFile}\n\nConteúdo atual:\n${currentContent?.substring(0, 2000)}\n\nSolicitação: ${prompt}`
: prompt;
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
],
max_tokens: 2000,
temperature: 0.3,
});
const generatedCode = response.choices[0]?.message?.content || "";
const codeMatch = generatedCode.match(/```[\w]*\n?([\s\S]*?)```/);
const code = codeMatch ? codeMatch[1].trim() : generatedCode.trim();
res.json({ success: true, code });
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
router.post("/ai-chat", requireAuth, async (req, res) => {
try {
const { message, currentFile, currentContent } = req.body;
if (!message) {
return res.status(400).json({ error: "Message required" });
}
const systemPrompt = `Você é o Manus, assistente de desenvolvimento do IDE Arcádia Suite.
Você ajuda desenvolvedores com:
- Explicação e análise de código
- Geração de código sob demanda
- Debug e correção de erros
- Refatoração e melhorias
- Criação de testes
- Documentação
CAPACIDADES ESPECIAIS:
- Você pode gerar código Python, JavaScript, TypeScript
- Você entende o contexto do projeto Arcádia Suite (Frappe-like, multi-tenant)
- Você pode sugerir criação de novos arquivos
FORMATO DE RESPOSTA:
- Seja direto e prático
- Use blocos de código quando apropriado
- Se gerar código, use markdown: \`\`\`python ou \`\`\`typescript
- Se sugerir criar arquivo, indique: [CRIAR ARQUIVO: caminho/nome.ext]
Use português brasileiro.`;
const userPrompt = currentFile
? `[Contexto: Editando ${currentFile}]\n\nCódigo atual:\n\`\`\`\n${currentContent?.substring(0, 2000)}\n\`\`\`\n\nSolicitação: ${message}`
: message;
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
],
max_tokens: 2000,
temperature: 0.3,
});
const aiResponse = response.choices[0]?.message?.content || "Desculpe, não consegui processar sua solicitação.";
res.json({ success: true, response: aiResponse });
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
router.post("/create-file", requireAuth, async (req, res) => {
try {
const { filePath, content = "" } = req.body;
if (!filePath) {
return res.status(400).json({ error: "File path required" });
}
const isAllowed = ALLOWED_DIRS.some(dir => filePath.startsWith(dir));
if (!isAllowed || filePath.includes("..")) {
return res.status(403).json({ error: "Access denied" });
}
const fullPath = path.resolve(process.cwd(), filePath);
await fs.mkdir(path.dirname(fullPath), { recursive: true });
await fs.writeFile(fullPath, content, "utf-8");
res.json({ success: true, path: filePath });
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
router.post("/create-folder", requireAuth, async (req, res) => {
try {
const { folderPath } = req.body;
if (!folderPath) {
return res.status(400).json({ error: "Folder path required" });
}
const isAllowed = ALLOWED_DIRS.some(dir => folderPath.startsWith(dir));
if (!isAllowed || folderPath.includes("..")) {
return res.status(403).json({ error: "Access denied" });
}
const fullPath = path.resolve(process.cwd(), folderPath);
await fs.mkdir(fullPath, { recursive: true });
res.json({ success: true, path: folderPath });
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
router.delete("/file", requireAuth, async (req, res) => {
try {
const filePath = req.query.path as string;
if (!filePath) {
return res.status(400).json({ error: "Path required" });
}
const isAllowed = ALLOWED_DIRS.some(dir => filePath.startsWith(dir));
if (!isAllowed || filePath.includes("..")) {
return res.status(403).json({ error: "Access denied" });
}
const fullPath = path.resolve(process.cwd(), filePath);
await fs.unlink(fullPath);
res.json({ success: true });
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
router.post("/run-python", requireAuth, async (req, res) => {
try {
const { filePath, args = "" } = req.body;
if (!filePath) {
return res.status(400).json({ error: "File path required" });
}
const isAllowed = ALLOWED_DIRS.some(dir => filePath.startsWith(dir));
if (!isAllowed || !filePath.endsWith(".py")) {
return res.status(403).json({ error: "Invalid Python file" });
}
const fullPath = path.resolve(process.cwd(), filePath);
const { stdout, stderr } = await execAsync(`python3 ${fullPath} ${args}`, { timeout: 60000 });
res.json({
success: true,
output: stdout,
error: stderr,
language: "python"
});
} catch (error: any) {
res.json({
success: false,
error: error.message,
language: "python"
});
}
});
router.post("/run-node", requireAuth, async (req, res) => {
try {
const { filePath, args = "" } = req.body;
if (!filePath) {
return res.status(400).json({ error: "File path required" });
}
const isAllowed = ALLOWED_DIRS.some(dir => filePath.startsWith(dir));
const validExtensions = [".js", ".ts", ".mjs"];
if (!isAllowed || !validExtensions.some(ext => filePath.endsWith(ext))) {
return res.status(403).json({ error: "Invalid JavaScript/TypeScript file" });
}
const command = filePath.endsWith(".ts")
? `npx tsx ${filePath} ${args}`
: `node ${filePath} ${args}`;
const { stdout, stderr } = await execAsync(command, { timeout: 60000, cwd: process.cwd() });
res.json({
success: true,
output: stdout,
error: stderr,
language: "javascript"
});
} catch (error: any) {
res.json({
success: false,
error: error.message,
language: "javascript"
});
}
});
router.post("/ai-agent", requireAuth, async (req: any, res) => {
try {
const { task, currentFile, currentContent } = req.body;
if (!task) {
return res.status(400).json({ error: "Task required" });
}
const userId = req.user?.id?.toString() || "ide-user";
const contextualTask = currentFile
? `[IDE] Trabalhando no arquivo ${currentFile}. Tarefa: ${task}\n\nConteúdo atual do arquivo:\n${currentContent?.substring(0, 2000)}`
: `[IDE] ${task}`;
const { runId } = await manusService.run(userId, contextualTask);
res.json({
success: true,
runId,
message: "Manus está executando a tarefa. Acompanhe o progresso."
});
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
router.post("/manus-task", requireAuth, async (req: any, res) => {
try {
const { task } = req.body;
if (!task) {
return res.status(400).json({ error: "Task required" });
}
const userId = req.user?.id?.toString() || "ide-user";
const { runId } = await manusService.run(userId, task);
res.json({
success: true,
runId,
status: "running",
message: "Tarefa iniciada pelo Manus."
});
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
router.get("/project-structure", requireAuth, async (req, res) => {
try {
const structure: string[] = [];
const walkDir = async (dir: string, prefix: string = "") => {
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "__pycache__") {
continue;
}
const fullPath = path.join(dir, entry.name);
structure.push(`${prefix}${entry.isDirectory() ? "📁" : "📄"} ${entry.name}`);
if (entry.isDirectory() && prefix.length < 8) {
await walkDir(fullPath, prefix + " ");
}
}
} catch {}
};
for (const dir of ALLOWED_DIRS) {
structure.push(`📂 ${dir}/`);
await walkDir(path.resolve(process.cwd(), dir), " ");
}
res.json({ structure: structure.join("\n") });
} catch (error: any) {
res.status(500).json({ error: error.message });
}
});
export default router;