448 lines
13 KiB
TypeScript
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;
|