| import express from 'express'; | |
| import bodyParser from 'body-parser'; | |
| import cors from 'cors'; | |
| import { StateManager, initDB } from './stateManager.js'; | |
| import { AIEngine } from './aiEngine.js'; | |
| import fs from 'fs'; | |
| import crypto from "crypto"; | |
| import dotenv from 'dotenv'; | |
| dotenv.config(); | |
| // Initialize Database | |
| initDB(); | |
| const supabase = StateManager.getSupabaseClient(); | |
| const app = express(); | |
| const PORT = process.env.PORT || 7860; | |
| app.use(cors()); | |
| app.use(express.json({ limit: '50mb' })); | |
| // --- BILLING CONSTANTS --- | |
| const MIN_BASIC_REQUIRED = 100; | |
| const MIN_DIAMOND_REQUIRED = 100; | |
| // --- STATUS PHASES --- | |
| const WORKER_PHASES = [ | |
| "Worker: Sorting...", | |
| "Worker: Analyzing Request...", | |
| "Worker: Reading Context...", | |
| "Worker: Checking Hierarchy...", | |
| "Worker: Planning Logic...", | |
| "Worker: Preparing Environment...", | |
| "Worker: Thinking..." | |
| ]; | |
| const PM_PHASES = [ | |
| "Manager: Sorting...", | |
| "Manager: Reviewing Request...", | |
| "Manager: Analyzing Project Structure...", | |
| "Manager: Consulting Guidelines...", | |
| "Manager: Formulating Strategy...", | |
| "Manager: Delegating Tasks...", | |
| "Manager: Thinking..." | |
| ]; | |
| // --- HELPER FUNCTIONS --- | |
| function startStatusLoop(projectId, type = 'worker') { | |
| const phases = type === 'pm' ? PM_PHASES : WORKER_PHASES; | |
| let index = 0; | |
| // Set the initial state immediately | |
| StateManager.setStatus(projectId, phases[0]); | |
| const interval = setInterval(() => { | |
| if (index < phases.length - 1) { | |
| index++; | |
| StateManager.setStatus(projectId, phases[index]); | |
| } else { | |
| // Stop the loop once we reach the last entry | |
| clearInterval(interval); | |
| } | |
| }, 3500); | |
| return () => clearInterval(interval); | |
| }; | |
| function extractWorkerPrompt(text) { | |
| if (!text || typeof text !== 'string') return null; | |
| const match = text.match(/WORKER_PROMPT:\s*(.*)/s); | |
| return match ? match[1].trim() : null; | |
| } | |
| function extractPMQuestion(text) { | |
| if (!text || typeof text !== 'string') return null; | |
| const match = text.match(/\[ASK_PM:\s*(.*?)\]/s); | |
| return match ? match[1].trim() : null; | |
| } | |
| function extractImagePrompt(text) { | |
| if (!text || typeof text !== 'string') return null; | |
| const match = text.match(/\[GENERATE_IMAGE:\s*(.*?)\]/s); | |
| return match ? match[1].trim() : null; | |
| } | |
| function extractRouteToPM(text) { | |
| if (!text || typeof text !== 'string') return null; | |
| const match = text.match(/\[ROUTE_TO_PM:\s*(.*?)\]/s); | |
| return match ? match[1].trim() : null; | |
| } | |
| function formatContext({ hierarchyContext, scriptContext, logContext }) { | |
| let parts = []; | |
| if (hierarchyContext) { | |
| parts.push(`[HIERARCHY_VIEW]\n${hierarchyContext}`); | |
| } | |
| if (scriptContext && scriptContext.targetName) { | |
| // Enforce code block formatting for the script content | |
| const source = scriptContext.scriptSource || "(Empty Script)"; | |
| parts.push(`[ACTIVE_SCRIPT: ${scriptContext.targetName}]\n\`\`\`lua\n${source}\n\`\`\``); | |
| } | |
| if (logContext && logContext.logs) { | |
| parts.push(`[OUTPUT_LOGS]\n${logContext.logs}`); | |
| } | |
| if (parts.length === 0) return ""; | |
| return `\n\n=== STUDIO CONTEXT ===\n${parts.join("\n\n")}\n======================\n`; | |
| } | |
| const validateRequest = (req, res, next) => { | |
| if (req.path.includes('/admin/cleanup')) return next(); | |
| const { userId } = req.body; | |
| if (!userId && (req.path.includes('/onboarding') || req.path.includes('/new') || req.path.includes('/project'))) { | |
| return res.status(400).json({ error: "Missing userId" }); | |
| } | |
| next(); | |
| }; | |
| // --- BILLING LOGIC --- | |
| async function checkMinimumCredits(userId, type = 'basic') { | |
| if (!supabase) return; | |
| const { data } = await supabase.from('users').select('credits').eq('id', userId).single(); | |
| if (!data) return; | |
| const credits = data.credits?.[type] || 0; | |
| const req = type === 'diamond' ? MIN_DIAMOND_REQUIRED : MIN_BASIC_REQUIRED; | |
| if (credits < req) { | |
| throw new Error(`Insufficient ${type} credits. Required: ${req}, Available: ${credits}`); | |
| } | |
| } | |
| async function deductUserCredits(userId, amount, type = 'basic') { | |
| const deduction = Math.floor(parseInt(amount, 10)); | |
| if (!supabase || !deduction || deduction <= 0) return; | |
| try { | |
| const { data } = await supabase.from('users').select('credits').eq('id', userId).single(); | |
| if (!data) return; | |
| const currentCredits = data.credits || { basic: 0, diamond: 0 }; | |
| const currentVal = currentCredits[type] || 0; | |
| const newVal = Math.max(0, currentVal - deduction); | |
| const updatedCredits = { ...currentCredits, [type]: newVal }; | |
| await supabase.from('users').update({ credits: updatedCredits }).eq('id', userId); | |
| console.log(`[Credits] User ${userId} | -${deduction} ${type} | Remaining: ${newVal}`); | |
| } catch (err) { | |
| console.error("Deduction failed", err); | |
| } | |
| } | |
| // --- CORE LOGIC: Background Initialization --- | |
| async function runBackgroundInitialization(projectId, userId, description) { | |
| if (StateManager.isLocked(projectId)) { | |
| console.log(`[Init Guard] Project ${projectId} is already initializing. Skipping.`); | |
| return; | |
| } | |
| StateManager.lock(projectId); | |
| console.log(`[Background] Starting initialization for ${projectId}`); | |
| let diamondUsage = 0; | |
| let basicUsage = 0; | |
| try { | |
| await StateManager.getProject(projectId); | |
| await StateManager.updateProject(projectId, { | |
| status: "working", | |
| gdd: "", | |
| failureCount: 0 | |
| }); | |
| const pmHistory = []; | |
| // --- Step 1: Generate GDD (PM) --- | |
| let stopStatus = startStatusLoop(projectId, 'pm'); | |
| const gddPrompt = `Create a comprehensive Game Design Document (GDD) for: ${description}`; | |
| const gddResult = await AIEngine.callPM(pmHistory, gddPrompt); | |
| stopStatus(); | |
| if (!gddResult?.text) throw new Error("PM failed to generate GDD"); | |
| const gddText = gddResult.text; | |
| diamondUsage += (gddResult.usage?.totalTokenCount || 0); | |
| await StateManager.addHistory(projectId, 'pm', 'user', gddPrompt); | |
| await StateManager.addHistory(projectId, 'pm', 'model', gddText); | |
| pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] }); | |
| pmHistory.push({ role: 'model', parts: [{ text: gddText }] }); | |
| // --- Step 2: Generate Tasks (PM) --- | |
| stopStatus = startStatusLoop(projectId, 'pm'); | |
| const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>"; | |
| const taskResult = await AIEngine.callPM(pmHistory, taskPrompt); | |
| stopStatus(); | |
| if (!taskResult?.text) throw new Error("PM failed to generate Task"); | |
| const taskText = taskResult.text; | |
| diamondUsage += (taskResult.usage?.totalTokenCount || 0); | |
| await StateManager.addHistory(projectId, 'pm', 'user', taskPrompt); | |
| await StateManager.addHistory(projectId, 'pm', 'model', taskText); | |
| // --- Step 3: Initialize Worker --- | |
| const initialWorkerInstruction = extractWorkerPrompt(taskText) || `Initialize structure for: ${description}`; | |
| const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`; | |
| stopStatus = startStatusLoop(projectId, 'worker'); | |
| let workerTextAccumulated = ""; | |
| const workerResult = await AIEngine.callWorkerStream( | |
| [], | |
| initialWorkerPrompt, | |
| (thought) => { | |
| stopStatus(); | |
| StateManager.setStatus(projectId, "Worker: Thinking..."); | |
| StateManager.appendSnapshotOnly(projectId, thought); | |
| }, | |
| (chunk) => { | |
| stopStatus(); | |
| StateManager.setStatus(projectId, "Worker: Coding..."); | |
| workerTextAccumulated += chunk; | |
| StateManager.appendStream(projectId, chunk); | |
| } | |
| ); | |
| stopStatus(); | |
| basicUsage += (workerResult.usage?.totalTokenCount || 0); | |
| if (!workerTextAccumulated && !workerResult.text) throw new Error("Worker failed to initialize"); | |
| const finalWorkerText = workerResult.text || workerTextAccumulated; | |
| await StateManager.addHistory(projectId, 'worker', 'user', initialWorkerPrompt); | |
| await StateManager.addHistory(projectId, 'worker', 'model', finalWorkerText); | |
| await StateManager.updateProject(projectId, { | |
| gdd: gddText, | |
| status: "idle" | |
| }); | |
| await processAndQueueResponse(projectId, finalWorkerText, userId); | |
| console.log(`[Background] Init complete for ${projectId}`); | |
| } catch (err) { | |
| console.error(`[Background] Init Error for ${projectId}:`, err.message); | |
| await StateManager.updateProject(projectId, { status: "error" }); | |
| } finally { | |
| StateManager.unlock(projectId); | |
| if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond'); | |
| if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic'); | |
| } | |
| } | |
| // --- CORE LOGIC: Async Feedback Loop --- | |
| async function runAsyncFeedback(projectId, userId, fullInput, images = []) { | |
| let diamondUsage = 0; | |
| let basicUsage = 0; | |
| try { | |
| console.log(`[${projectId}] Feedback received. Persisting state immediately.`); | |
| await Promise.all([ | |
| StateManager.updateProject(projectId, { status: "working" }), | |
| StateManager.addHistory(projectId, 'worker', 'user', fullInput) | |
| ]); | |
| const project = await StateManager.getProject(projectId); | |
| StateManager.clearSnapshot(projectId); | |
| let stopStatus = startStatusLoop(projectId, 'worker'); | |
| let firstTurnResponse = ""; | |
| let thoughtText = ""; | |
| const currentWorkerHistory = [...(project.workerHistory || [])]; | |
| // 2. First Turn (Worker Execution) | |
| const workerResult = await AIEngine.callWorkerStream( | |
| currentWorkerHistory, | |
| fullInput, | |
| (thought) => { | |
| stopStatus(); | |
| thoughtText += thought; | |
| StateManager.setStatus(projectId, "Worker: Thinking..."); | |
| StateManager.appendSnapshotOnly(projectId, thought); | |
| }, | |
| (chunk) => { | |
| stopStatus(); | |
| firstTurnResponse += chunk; | |
| StateManager.setStatus(projectId, "Worker: Coding..."); | |
| StateManager.appendStream(projectId, chunk); | |
| }, | |
| images | |
| ); | |
| stopStatus(); | |
| basicUsage += (workerResult.usage?.totalTokenCount || 0); | |
| firstTurnResponse = workerResult.text || firstTurnResponse; | |
| // 3. Analyze Output (Delegation/Questions) | |
| const routeTask = extractRouteToPM(firstTurnResponse); | |
| const pmQuestion = extractPMQuestion(firstTurnResponse); | |
| let finalResponseToSave = firstTurnResponse; | |
| if (routeTask || pmQuestion) { | |
| await StateManager.addHistory(projectId, 'worker', 'model', firstTurnResponse); | |
| currentWorkerHistory.push({ role: 'model', parts: [{ text: firstTurnResponse }] }); | |
| let pmPrompt = ""; | |
| let pmContextPrefix = ""; | |
| if (routeTask) { | |
| console.log(`[${projectId}] Routing task to PM...`); | |
| pmPrompt = `[ROUTED TASK]: ${routeTask}\nWrite the backend/logic. Then use WORKER_PROMPT: <task> to delegate.`; | |
| pmContextPrefix = "[PM DELEGATION]:"; | |
| } else { | |
| console.log(`[${projectId}] Worker consulting PM...`); | |
| pmPrompt = `[WORKER QUESTION]: ${pmQuestion}\nProvide a technical answer or code snippet.`; | |
| pmContextPrefix = "[PM ANSWER]:"; | |
| } | |
| // 4. Run PM (Diamond Logic) | |
| StateManager.clearSnapshot(projectId); | |
| stopStatus = startStatusLoop(projectId, 'pm'); | |
| let pmResponseText = ""; | |
| const pmResult = await AIEngine.callPMStream( | |
| project.pmHistory, | |
| pmPrompt, | |
| (thought) => { | |
| stopStatus(); | |
| StateManager.setStatus(projectId, "Manager: Thinking..."); | |
| StateManager.appendSnapshotOnly(projectId, thought); | |
| }, | |
| (chunk) => { | |
| stopStatus(); | |
| StateManager.setStatus(projectId, "Manager: Architecture..."); | |
| pmResponseText += chunk; | |
| StateManager.appendSnapshotOnly(projectId, chunk); | |
| } | |
| ); | |
| stopStatus(); | |
| diamondUsage += (pmResult.usage?.totalTokenCount || 0); | |
| pmResponseText = pmResult.text || pmResponseText; | |
| await StateManager.addHistory(projectId, 'pm', 'user', pmPrompt); | |
| await StateManager.addHistory(projectId, 'pm', 'model', pmResponseText); | |
| // --- EXECUTE PM CODE --- | |
| await processAndQueueResponse(projectId, pmResponseText, userId); | |
| // 5. Run Worker Continuation | |
| const nextInstruction = extractWorkerPrompt(pmResponseText) || pmResponseText; | |
| const workerContinuationPrompt = `${pmContextPrefix} ${nextInstruction}\n\nBased on this, continue the task and output the code.`; | |
| await StateManager.addHistory(projectId, 'worker', 'user', workerContinuationPrompt); | |
| currentWorkerHistory.push({ role: 'user', parts: [{ text: workerContinuationPrompt }] }); | |
| StateManager.clearSnapshot(projectId); | |
| stopStatus = startStatusLoop(projectId, 'worker'); | |
| let secondTurnResponse = ""; | |
| const secondWorkerResult = await AIEngine.callWorkerStream( | |
| currentWorkerHistory, | |
| workerContinuationPrompt, | |
| (thought) => { | |
| stopStatus(); | |
| StateManager.setStatus(projectId, "Worker: Applying Fix..."); | |
| StateManager.appendSnapshotOnly(projectId, thought); | |
| }, | |
| (chunk) => { | |
| stopStatus(); | |
| StateManager.setStatus(projectId, "Worker: Finalizing..."); | |
| secondTurnResponse += chunk; | |
| StateManager.appendStream(projectId, chunk); | |
| } | |
| ); | |
| stopStatus(); | |
| basicUsage += (secondWorkerResult.usage?.totalTokenCount || 0); | |
| secondTurnResponse = secondWorkerResult.text || secondTurnResponse; | |
| await StateManager.addHistory(projectId, 'worker', 'model', secondTurnResponse); | |
| finalResponseToSave = secondTurnResponse; | |
| } else { | |
| // No delegation, just save the first response | |
| await StateManager.addHistory(projectId, 'worker', 'model', firstTurnResponse); | |
| } | |
| // --- STATUS RESET --- | |
| StateManager.setStatus(projectId, "Idle"); | |
| // 6. Final Execution & Cleanup | |
| await processAndQueueResponse(projectId, finalResponseToSave, userId); | |
| await StateManager.updateProject(projectId, { status: "idle" }); | |
| } catch (err) { | |
| console.error("Async Feedback Error:", err); | |
| StateManager.setStatus(projectId, "Error: " + err.message); | |
| await StateManager.updateProject(projectId, { status: "error" }); | |
| } finally { | |
| if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond'); | |
| if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic'); | |
| } | |
| } | |
| async function processAndQueueResponse(projectId, rawResponse, userId) { | |
| if (!rawResponse) return; | |
| // Image Generation | |
| const imgPrompt = extractImagePrompt(rawResponse); | |
| if (imgPrompt) { | |
| try { | |
| const imgResult = await AIEngine.generateImage(imgPrompt); | |
| if (imgResult?.image) { | |
| if (userId) await deductUserCredits(userId, 1000, 'basic'); // Flat fee | |
| await StateManager.queueCommand(projectId, { type: "CREATE_ASSET", payload: imgResult.image }); | |
| } | |
| } catch (e) { | |
| console.error("Image gen failed", e); | |
| } | |
| } | |
| // Code Execution | |
| const codeMatch = rawResponse.match(/```(?:lua|luau)?([\s\S]*?)```/i); | |
| if (codeMatch) { | |
| await StateManager.queueCommand(projectId, { type: "EXECUTE", payload: codeMatch[1].trim() }); | |
| } | |
| // Explicit Read Commands (Parsing what the AI wrote) | |
| await StateManager.queueCommand(projectId, rawResponse); | |
| } | |
| // --- EXPRESS ROUTES --- | |
| app.post('/onboarding/analyze', validateRequest, async (req, res) => { | |
| const { description, userId } = req.body; | |
| if (!description) return res.status(400).json({ error: "Description required" }); | |
| try { | |
| try { | |
| await checkMinimumCredits(userId, 'basic'); | |
| } catch (creditErr) { | |
| return res.json({ success: false, insufficient: true, error: creditErr.message }); | |
| } | |
| const result = await AIEngine.generateEntryQuestions(description); | |
| if (result.usage?.totalTokenCount > 0) { | |
| await deductUserCredits(userId, result.usage.totalTokenCount, 'basic'); | |
| } | |
| if (result.status === "REJECTED") { | |
| return res.json({ rejected: true, reason: result.reason || "Idea violates TOS." }); | |
| } | |
| res.json({ questions: result.questions }); | |
| } catch (err) { | |
| res.status(500).json({ error: err.message }); | |
| } | |
| }); | |
| app.post('/onboarding/create', validateRequest, async (req, res) => { | |
| const { userId, description, answers } = req.body; | |
| try { | |
| try { | |
| await checkMinimumCredits(userId, 'basic'); | |
| await checkMinimumCredits(userId, 'diamond'); | |
| } catch (creditErr) { | |
| return res.json({ success: false, insufficient: true, error: creditErr.message }); | |
| } | |
| const randomHex = (n = 6) => crypto.randomBytes(Math.ceil(n/2)).toString("hex").slice(0, n); | |
| const projectId = `proj_${Date.now()}_${randomHex(7)}`; | |
| const gradingResult = await AIEngine.gradeProject(description, answers); | |
| if (gradingResult.usage?.totalTokenCount) { | |
| await deductUserCredits(userId, gradingResult.usage.totalTokenCount, 'basic'); | |
| } | |
| const isFailure = gradingResult.feasibility < 30 || gradingResult.rating === 'F'; | |
| if (!isFailure) { | |
| const { error: insertError } = await supabase.from('projects').insert({ | |
| id: projectId, | |
| user_id: userId, | |
| info: { | |
| title: gradingResult.title || "Untitled", | |
| stats: gradingResult, | |
| description, | |
| answers, | |
| status: "initializing" | |
| } | |
| }); | |
| if (insertError) { | |
| console.error("Failed to insert project:", insertError.message); | |
| return res.status(500).json({ error: "Database save failed" }); | |
| } | |
| runBackgroundInitialization(projectId, userId, description); | |
| } | |
| res.json({ | |
| status: 200, | |
| success: !isFailure, | |
| projectId, | |
| stats: gradingResult, | |
| title: gradingResult.title || "Untitled" | |
| }); | |
| } catch (err) { | |
| console.error("Create Error:", err); | |
| res.status(500).json({ error: err.message }); | |
| } | |
| }); | |
| app.post('/project/feedback', async (req, res) => { | |
| const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, images } = req.body; | |
| try { | |
| const project = await StateManager.getProject(projectId); | |
| if (!project || project.userId !== userId) return res.status(403).json({ error: "Auth Error" }); | |
| try { | |
| await checkMinimumCredits(userId, 'basic'); | |
| } catch (creditErr) { | |
| return res.json({ success: false, insufficient: true, error: creditErr.message }); | |
| } | |
| // Pass the context objects into formatContext | |
| const formattedContext = formatContext({ hierarchyContext, scriptContext, logContext }); | |
| // Append to user prompt | |
| const fullInput = `USER: ${prompt || "Automatic Feedback"}${formattedContext}`; | |
| // Trigger Async | |
| runAsyncFeedback(projectId, userId, fullInput, images || []); | |
| res.json({ success: true, message: "Processing started" }); | |
| } catch (err) { | |
| console.error("Feedback Error:", err); | |
| await StateManager.updateProject(projectId, { status: "error" }); | |
| res.status(500).json({ error: "Feedback process failed" }); | |
| } | |
| }); | |
| app.post('/project/ping', async (req, res) => { | |
| const { projectId, userId, isFrontend } = req.body; | |
| if (!projectId || !userId) return res.status(400).json({ error: "Missing IDs" }); | |
| const project = await StateManager.getProject(projectId); | |
| if (!project || project.userId !== userId) return res.json({ action: "IDLE" }); | |
| // 1. BASE RESPONSE (Visuals) | |
| const response = { | |
| action: "IDLE", | |
| status: StateManager.getStatus(projectId), // Will now return "Idle" by default | |
| snapshot: StateManager.getSnapshot(projectId) | |
| }; | |
| // 2. EXECUTOR LOGIC (Plugin Only) | |
| if (!isFrontend) { | |
| const command = await StateManager.popCommand(projectId); | |
| if (command) { | |
| response.action = command.type; | |
| response.target = command.payload; | |
| response.code = command.type === 'EXECUTE' ? command.payload : null; | |
| } | |
| } | |
| res.json(response); | |
| }); | |
| app.post('/human/override', validateRequest, async (req, res) => { | |
| const { projectId, instruction, userId } = req.body; | |
| try { | |
| await checkMinimumCredits(userId, 'basic'); | |
| const project = await StateManager.getProject(projectId); | |
| res.json({ success: true }); | |
| const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`; | |
| let overrideText = ""; | |
| let stopStatus = startStatusLoop(projectId, 'worker'); | |
| const workerResult = await AIEngine.callWorkerStream( | |
| project.workerHistory, | |
| overrideMsg, | |
| (thought) => { | |
| stopStatus(); | |
| StateManager.setStatus(projectId, "Worker: Thinking..."); | |
| StateManager.appendSnapshotOnly(projectId, thought); | |
| }, | |
| (chunk) => { | |
| stopStatus(); | |
| StateManager.setStatus(projectId, "Worker: Coding..."); | |
| overrideText += chunk; | |
| StateManager.appendStream(projectId, chunk); | |
| } | |
| ); | |
| stopStatus(); | |
| await StateManager.addHistory(projectId, 'worker', 'user', overrideMsg); | |
| await StateManager.addHistory(projectId, 'worker', 'model', workerResult.text || overrideText); | |
| await processAndQueueResponse(projectId, workerResult.text || overrideText, userId); | |
| const usage = workerResult.usage?.totalTokenCount || 0; | |
| if (usage > 0) await deductUserCredits(userId, usage, 'basic'); | |
| } catch (err) { | |
| console.error("Override failed", err); | |
| } | |
| }); | |
| app.get('/admin/cleanup', async (req, res) => { | |
| try { | |
| const removed = StateManager.cleanupMemory ? StateManager.cleanupMemory() : 0; | |
| res.json({ success: true, removedCount: removed }); | |
| } catch (err) { | |
| res.status(500).json({ error: "Cleanup failed" }); | |
| } | |
| }); | |
| app.listen(PORT, () => { | |
| console.log(`AI Backend Running on ${PORT} (Supabase + Billing Edition)`); | |
| }); | |
| /* import express from 'express'; | |
| import bodyParser from 'body-parser'; | |
| import cors from 'cors'; | |
| import { StateManager, initDB } from './stateManager.js'; | |
| import { AIEngine } from './aiEngine.js'; | |
| import fs from 'fs'; | |
| import crypto from "crypto"; | |
| import dotenv from 'dotenv'; | |
| dotenv.config(); | |
| // Initialize Database | |
| initDB(); | |
| const supabase = StateManager.getSupabaseClient(); | |
| const app = express(); | |
| const PORT = process.env.PORT || 7860; | |
| app.use(cors()); | |
| app.use(express.json({ limit: '50mb' })); | |
| // --- BILLING CONSTANTS --- | |
| const MIN_BASIC_REQUIRED = 100; | |
| const MIN_DIAMOND_REQUIRED = 100; | |
| // --- STATUS PHASES --- | |
| const WORKER_PHASES = [ | |
| "Worker: Sorting...", | |
| "Worker: Analyzing Request...", | |
| "Worker: Reading Context...", | |
| "Worker: Checking Hierarchy...", | |
| "Worker: Planning Logic...", | |
| "Worker: Preparing Environment...", | |
| "Worker: Thinking..." | |
| ]; | |
| const PM_PHASES = [ | |
| "Manager: Sorting...", | |
| "Manager: Reviewing Request...", | |
| "Manager: Analyzing Project Structure...", | |
| "Manager: Consulting Guidelines...", | |
| "Manager: Formulating Strategy...", | |
| "Manager: Delegating Tasks...", | |
| "Manager: Thinking..." | |
| ]; | |
| // --- HELPER FUNCTIONS --- | |
| function startStatusLoop(projectId, type = 'worker') { | |
| const phases = type === 'pm' ? PM_PHASES : WORKER_PHASES; | |
| let index = 0; | |
| // Set the initial state immediately | |
| StateManager.setStatus(projectId, phases[0]); | |
| const interval = setInterval(() => { | |
| if (index < phases.length - 1) { | |
| index++; | |
| StateManager.setStatus(projectId, phases[index]); | |
| } else { | |
| // Stop the loop once we reach the last entry | |
| clearInterval(interval); | |
| } | |
| }, 3500); // 3.5 second delay as requested | |
| // Return a cleanup function in case the component unmounts early | |
| return () => clearInterval(interval); | |
| }; | |
| function extractWorkerPrompt(text) { | |
| if (!text || typeof text !== 'string') return null; | |
| const match = text.match(/WORKER_PROMPT:\s*(.*)/s); | |
| return match ? match[1].trim() : null; | |
| } | |
| function extractPMQuestion(text) { | |
| if (!text || typeof text !== 'string') return null; | |
| const match = text.match(/\[ASK_PM:\s*(.*?)\]/s); | |
| return match ? match[1].trim() : null; | |
| } | |
| function extractImagePrompt(text) { | |
| if (!text || typeof text !== 'string') return null; | |
| const match = text.match(/\[GENERATE_IMAGE:\s*(.*?)\]/s); | |
| return match ? match[1].trim() : null; | |
| } | |
| function extractRouteToPM(text) { | |
| if (!text || typeof text !== 'string') return null; | |
| const match = text.match(/\[ROUTE_TO_PM:\s*(.*?)\]/s); | |
| return match ? match[1].trim() : null; | |
| } | |
| function formatContext({ hierarchyContext, scriptContext, logContext }) { | |
| let out = ""; | |
| if (scriptContext) out += `\n[TARGET SCRIPT]: ${scriptContext.targetName}\n[SOURCE PREVIEW]: ${scriptContext.scriptSource}`; | |
| if (logContext) out += `\n[LAST LOGS]: ${logContext.logs}`; | |
| if (hierarchyContext) out += `\n[Hierarchy Context]: ${hierarchyContext}`; | |
| return out; | |
| } | |
| const validateRequest = (req, res, next) => { | |
| if (req.path.includes('/admin/cleanup')) return next(); | |
| const { userId } = req.body; | |
| if (!userId && (req.path.includes('/onboarding') || req.path.includes('/new') || req.path.includes('/project'))) { | |
| return res.status(400).json({ error: "Missing userId" }); | |
| } | |
| next(); | |
| }; | |
| // --- BILLING LOGIC --- | |
| async function checkMinimumCredits(userId, type = 'basic') { | |
| if (!supabase) return; | |
| const { data } = await supabase.from('users').select('credits').eq('id', userId).single(); | |
| if (!data) return; | |
| const credits = data.credits?.[type] || 0; | |
| const req = type === 'diamond' ? MIN_DIAMOND_REQUIRED : MIN_BASIC_REQUIRED; | |
| if (credits < req) { | |
| throw new Error(`Insufficient ${type} credits. Required: ${req}, Available: ${credits}`); | |
| } | |
| } | |
| async function deductUserCredits(userId, amount, type = 'basic') { | |
| const deduction = Math.floor(parseInt(amount, 10)); | |
| if (!supabase || !deduction || deduction <= 0) return; | |
| try { | |
| const { data } = await supabase.from('users').select('credits').eq('id', userId).single(); | |
| if (!data) return; | |
| const currentCredits = data.credits || { basic: 0, diamond: 0 }; | |
| const currentVal = currentCredits[type] || 0; | |
| const newVal = Math.max(0, currentVal - deduction); | |
| const updatedCredits = { ...currentCredits, [type]: newVal }; | |
| await supabase.from('users').update({ credits: updatedCredits }).eq('id', userId); | |
| console.log(`[Credits] User ${userId} | -${deduction} ${type} | Remaining: ${newVal}`); | |
| } catch (err) { | |
| console.error("Deduction failed", err); | |
| } | |
| } | |
| // --- CORE LOGIC: Background Initialization --- | |
| async function runBackgroundInitialization(projectId, userId, description) { | |
| if (StateManager.isLocked(projectId)) { | |
| console.log(`[Init Guard] Project ${projectId} is already initializing. Skipping.`); | |
| return; | |
| } | |
| StateManager.lock(projectId); | |
| console.log(`[Background] Starting initialization for ${projectId}`); | |
| let diamondUsage = 0; | |
| let basicUsage = 0; | |
| try { | |
| await StateManager.getProject(projectId); | |
| await StateManager.updateProject(projectId, { | |
| status: "working", | |
| gdd: "", | |
| failureCount: 0 | |
| }); | |
| const pmHistory = []; | |
| // --- Step 1: Generate GDD (PM) --- | |
| let stopStatus = startStatusLoop(projectId, 'pm'); | |
| const gddPrompt = `Create a comprehensive Game Design Document (GDD) for: ${description}`; | |
| const gddResult = await AIEngine.callPM(pmHistory, gddPrompt); | |
| stopStatus(); | |
| if (!gddResult?.text) throw new Error("PM failed to generate GDD"); | |
| const gddText = gddResult.text; | |
| diamondUsage += (gddResult.usage?.totalTokenCount || 0); | |
| await StateManager.addHistory(projectId, 'pm', 'user', gddPrompt); | |
| await StateManager.addHistory(projectId, 'pm', 'model', gddText); | |
| pmHistory.push({ role: 'user', parts: [{ text: gddPrompt }] }); | |
| pmHistory.push({ role: 'model', parts: [{ text: gddText }] }); | |
| // --- Step 2: Generate Tasks (PM) --- | |
| stopStatus = startStatusLoop(projectId, 'pm'); | |
| const taskPrompt = "Based on the GDD, generate the first technical milestone.\nOutput format:\nTASK_NAME: <Name>\nWORKER_PROMPT: <Specific, isolated instructions for the worker>"; | |
| const taskResult = await AIEngine.callPM(pmHistory, taskPrompt); | |
| stopStatus(); | |
| if (!taskResult?.text) throw new Error("PM failed to generate Task"); | |
| const taskText = taskResult.text; | |
| diamondUsage += (taskResult.usage?.totalTokenCount || 0); | |
| await StateManager.addHistory(projectId, 'pm', 'user', taskPrompt); | |
| await StateManager.addHistory(projectId, 'pm', 'model', taskText); | |
| // --- Step 3: Initialize Worker --- | |
| const initialWorkerInstruction = extractWorkerPrompt(taskText) || `Initialize structure for: ${description}`; | |
| const initialWorkerPrompt = `CONTEXT: New Project. \nINSTRUCTION: ${initialWorkerInstruction}`; | |
| stopStatus = startStatusLoop(projectId, 'worker'); | |
| let workerTextAccumulated = ""; | |
| const workerResult = await AIEngine.callWorkerStream( | |
| [], | |
| initialWorkerPrompt, | |
| (thought) => { | |
| stopStatus(); | |
| StateManager.setStatus(projectId, "Worker: Thinking..."); | |
| StateManager.appendSnapshotOnly(projectId, thought); | |
| }, | |
| (chunk) => { | |
| stopStatus(); | |
| StateManager.setStatus(projectId, "Worker: Coding..."); | |
| workerTextAccumulated += chunk; | |
| StateManager.appendStream(projectId, chunk); | |
| } | |
| ); | |
| stopStatus(); | |
| basicUsage += (workerResult.usage?.totalTokenCount || 0); | |
| if (!workerTextAccumulated && !workerResult.text) throw new Error("Worker failed to initialize"); | |
| const finalWorkerText = workerResult.text || workerTextAccumulated; | |
| await StateManager.addHistory(projectId, 'worker', 'user', initialWorkerPrompt); | |
| await StateManager.addHistory(projectId, 'worker', 'model', finalWorkerText); | |
| await StateManager.updateProject(projectId, { | |
| gdd: gddText, | |
| status: "idle" | |
| }); | |
| await processAndQueueResponse(projectId, finalWorkerText, userId); | |
| console.log(`[Background] Init complete for ${projectId}`); | |
| } catch (err) { | |
| console.error(`[Background] Init Error for ${projectId}:`, err.message); | |
| await StateManager.updateProject(projectId, { status: "error" }); | |
| } finally { | |
| StateManager.unlock(projectId); | |
| if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond'); | |
| if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic'); | |
| } | |
| } | |
| // --- CORE LOGIC: Async Feedback Loop --- | |
| async function runAsyncFeedback(projectId, userId, fullInput, images = []) { | |
| let diamondUsage = 0; | |
| let basicUsage = 0; | |
| try { | |
| console.log(`[${projectId}] Feedback received. Persisting state immediately.`); | |
| // 1. Instant Persistence: Saves status and message BEFORE processing | |
| // This ensures if user reloads, the state is preserved | |
| await Promise.all([ | |
| StateManager.updateProject(projectId, { status: "working" }), | |
| StateManager.addHistory(projectId, 'worker', 'user', fullInput) | |
| ]); | |
| const project = await StateManager.getProject(projectId); | |
| StateManager.clearSnapshot(projectId); | |
| let stopStatus = startStatusLoop(projectId, 'worker'); | |
| let firstTurnResponse = ""; | |
| let thoughtText = ""; | |
| // Clone history to prevent mutation issues | |
| // Note: project.workerHistory includes the message we just saved above | |
| const currentWorkerHistory = [...(project.workerHistory || [])]; | |
| // 2. First Turn (Worker Execution) | |
| const workerResult = await AIEngine.callWorkerStream( | |
| currentWorkerHistory, | |
| fullInput, | |
| (thought) => { | |
| stopStatus(); | |
| thoughtText += thought; | |
| StateManager.setStatus(projectId, "Worker: Thinking..."); | |
| StateManager.appendSnapshotOnly(projectId, thought); | |
| }, | |
| (chunk) => { | |
| stopStatus(); | |
| firstTurnResponse += chunk; | |
| StateManager.setStatus(projectId, "Worker: Coding..."); | |
| StateManager.appendStream(projectId, chunk); | |
| }, | |
| images | |
| ); | |
| stopStatus(); | |
| basicUsage += (workerResult.usage?.totalTokenCount || 0); | |
| firstTurnResponse = workerResult.text || firstTurnResponse; | |
| // 3. Analyze Output (Delegation/Questions) | |
| const routeTask = extractRouteToPM(firstTurnResponse); | |
| const pmQuestion = extractPMQuestion(firstTurnResponse); | |
| let finalResponseToSave = firstTurnResponse; | |
| if (routeTask || pmQuestion) { | |
| // Save preliminary response to DB so it doesn't get lost | |
| await StateManager.addHistory(projectId, 'worker', 'model', firstTurnResponse); | |
| currentWorkerHistory.push({ role: 'model', parts: [{ text: firstTurnResponse }] }); | |
| let pmPrompt = ""; | |
| let pmContextPrefix = ""; | |
| if (routeTask) { | |
| console.log(`[${projectId}] Routing task to PM...`); | |
| pmPrompt = `[ROUTED TASK]: ${routeTask}\nWrite the backend/logic. Then use WORKER_PROMPT: <task> to delegate.`; | |
| pmContextPrefix = "[PM DELEGATION]:"; | |
| } else { | |
| console.log(`[${projectId}] Worker consulting PM...`); | |
| pmPrompt = `[WORKER QUESTION]: ${pmQuestion}\nProvide a technical answer or code snippet.`; | |
| pmContextPrefix = "[PM ANSWER]:"; | |
| } | |
| // 4. Run PM (Diamond Logic) | |
| StateManager.clearSnapshot(projectId); | |
| stopStatus = startStatusLoop(projectId, 'pm'); | |
| let pmResponseText = ""; | |
| const pmResult = await AIEngine.callPMStream( | |
| project.pmHistory, | |
| pmPrompt, | |
| (thought) => { | |
| stopStatus(); | |
| StateManager.setStatus(projectId, "Manager: Thinking..."); | |
| StateManager.appendSnapshotOnly(projectId, thought); | |
| }, | |
| (chunk) => { | |
| stopStatus(); | |
| StateManager.setStatus(projectId, "Manager: Architecture..."); | |
| pmResponseText += chunk; | |
| StateManager.appendSnapshotOnly(projectId, chunk); | |
| } | |
| ); | |
| stopStatus(); | |
| diamondUsage += (pmResult.usage?.totalTokenCount || 0); | |
| pmResponseText = pmResult.text || pmResponseText; | |
| await StateManager.addHistory(projectId, 'pm', 'user', pmPrompt); | |
| await StateManager.addHistory(projectId, 'pm', 'model', pmResponseText); | |
| // --- EXECUTE PM CODE --- | |
| // If the PM wrote code (e.g. ServerScriptService logic), execute it immediately | |
| await processAndQueueResponse(projectId, pmResponseText, userId); | |
| // 5. Run Worker Continuation | |
| const nextInstruction = extractWorkerPrompt(pmResponseText) || pmResponseText; | |
| const workerContinuationPrompt = `${pmContextPrefix} ${nextInstruction}\n\nBased on this, continue the task and output the code.`; | |
| // Add system prompt to history | |
| await StateManager.addHistory(projectId, 'worker', 'user', workerContinuationPrompt); | |
| currentWorkerHistory.push({ role: 'user', parts: [{ text: workerContinuationPrompt }] }); | |
| StateManager.clearSnapshot(projectId); | |
| stopStatus = startStatusLoop(projectId, 'worker'); | |
| let secondTurnResponse = ""; | |
| const secondWorkerResult = await AIEngine.callWorkerStream( | |
| currentWorkerHistory, | |
| workerContinuationPrompt, | |
| (thought) => { | |
| stopStatus(); | |
| StateManager.setStatus(projectId, "Worker: Applying Fix..."); | |
| StateManager.appendSnapshotOnly(projectId, thought); | |
| }, | |
| (chunk) => { | |
| stopStatus(); | |
| StateManager.setStatus(projectId, "Worker: Finalizing..."); | |
| secondTurnResponse += chunk; | |
| StateManager.appendStream(projectId, chunk); | |
| } | |
| ); | |
| stopStatus(); | |
| // Deduct usage for second turn | |
| basicUsage += (secondWorkerResult.usage?.totalTokenCount || 0); | |
| secondTurnResponse = secondWorkerResult.text || secondTurnResponse; | |
| await StateManager.addHistory(projectId, 'worker', 'model', secondTurnResponse); | |
| finalResponseToSave = secondTurnResponse; | |
| } else { | |
| // No delegation, just save the first response | |
| await StateManager.addHistory(projectId, 'worker', 'model', firstTurnResponse); | |
| } | |
| StateManager.setStatus(projectId, "Idle"); | |
| // 6. Final Execution & Cleanup | |
| await processAndQueueResponse(projectId, finalResponseToSave, userId); | |
| // Ensure status is idle (this also updates timestamps) | |
| await StateManager.updateProject(projectId, { status: "idle" }); | |
| } catch (err) { | |
| console.error("Async Feedback Error:", err); | |
| StateManager.setStatus(projectId, "Error: " + err.message); | |
| await StateManager.updateProject(projectId, { status: "error" }); | |
| } finally { | |
| // Billing | |
| if (diamondUsage > 0) await deductUserCredits(userId, diamondUsage, 'diamond'); | |
| if (basicUsage > 0) await deductUserCredits(userId, basicUsage, 'basic'); | |
| } | |
| } | |
| async function processAndQueueResponse(projectId, rawResponse, userId) { | |
| if (!rawResponse) return; | |
| // Image Generation | |
| const imgPrompt = extractImagePrompt(rawResponse); | |
| if (imgPrompt) { | |
| try { | |
| const imgResult = await AIEngine.generateImage(imgPrompt); | |
| if (imgResult?.image) { | |
| if (userId) await deductUserCredits(userId, 1000, 'basic'); // Flat fee | |
| await StateManager.queueCommand(projectId, { type: "CREATE_ASSET", payload: imgResult.image }); | |
| } | |
| } catch (e) { | |
| console.error("Image gen failed", e); | |
| } | |
| } | |
| // Code Execution | |
| // Regex looks for lua/luau code blocks to execute | |
| const codeMatch = rawResponse.match(/```(?:lua|luau)?([\s\S]*?)```/i); | |
| if (codeMatch) { | |
| await StateManager.queueCommand(projectId, { type: "EXECUTE", payload: codeMatch[1].trim() }); | |
| } else { | |
| // If there's no code block but response is non-empty, maybe check if it's pure code? | |
| // For safety, we usually only execute if wrapped in blocks. | |
| // But if you want to support raw text execution: | |
| // await StateManager.queueCommand(projectId, { type: "EXECUTE", payload: rawResponse }); | |
| } | |
| } | |
| // --- EXPRESS ROUTES --- | |
| app.post('/onboarding/analyze', validateRequest, async (req, res) => { | |
| const { description, userId } = req.body; | |
| if (!description) return res.status(400).json({ error: "Description required" }); | |
| try { | |
| // await checkMinimumCredits(userId, 'basic'); | |
| try { | |
| await checkMinimumCredits(userId, 'basic'); | |
| } catch (creditErr) { | |
| return res.json({ success: false, insufficient: true, error: creditErr.message }); | |
| } | |
| const result = await AIEngine.generateEntryQuestions(description); | |
| if (result.usage?.totalTokenCount > 0) { | |
| await deductUserCredits(userId, result.usage.totalTokenCount, 'basic'); | |
| } | |
| if (result.status === "REJECTED") { | |
| return res.json({ rejected: true, reason: result.reason || "Idea violates TOS." }); | |
| } | |
| res.json({ questions: result.questions }); | |
| } catch (err) { | |
| res.status(500).json({ error: err.message }); | |
| } | |
| }); | |
| app.post('/onboarding/create', validateRequest, async (req, res) => { | |
| const { userId, description, answers } = req.body; | |
| try { | |
| try { | |
| await checkMinimumCredits(userId, 'basic'); | |
| await checkMinimumCredits(userId, 'diamond'); | |
| } catch (creditErr) { | |
| return res.json({ success: false, insufficient: true, error: creditErr.message }); | |
| } | |
| const randomHex = (n = 6) => crypto.randomBytes(Math.ceil(n/2)).toString("hex").slice(0, n); | |
| const projectId = `proj_${Date.now()}_${randomHex(7)}`; | |
| const gradingResult = await AIEngine.gradeProject(description, answers); | |
| if (gradingResult.usage?.totalTokenCount) { | |
| await deductUserCredits(userId, gradingResult.usage.totalTokenCount, 'basic'); | |
| } | |
| const isFailure = gradingResult.feasibility < 30 || gradingResult.rating === 'F'; | |
| if (!isFailure) { | |
| const { error: insertError } = await supabase.from('projects').insert({ | |
| id: projectId, | |
| user_id: userId, | |
| info: { | |
| title: gradingResult.title || "Untitled", | |
| stats: gradingResult, | |
| description, | |
| answers, | |
| status: "initializing" | |
| } | |
| }); | |
| if (insertError) { | |
| console.error("Failed to insert project:", insertError.message); | |
| return res.status(500).json({ error: "Database save failed" }); | |
| } | |
| runBackgroundInitialization(projectId, userId, description); | |
| } | |
| res.json({ | |
| status: 200, | |
| success: !isFailure, | |
| projectId, | |
| stats: gradingResult, | |
| title: gradingResult.title || "Untitled" | |
| }); | |
| } catch (err) { | |
| console.error("Create Error:", err); | |
| res.status(500).json({ error: err.message }); | |
| } | |
| }); | |
| app.post('/project/feedback', async (req, res) => { | |
| const { userId, projectId, prompt, hierarchyContext, scriptContext, logContext, images } = req.body; | |
| try { | |
| const project = await StateManager.getProject(projectId); | |
| if (!project || project.userId !== userId) return res.status(403).json({ error: "Auth Error" }); | |
| // --- FIXED CREDIT CHECK --- | |
| // Catch credit errors here to return a soft 200 OK so frontend banner shows | |
| try { | |
| await checkMinimumCredits(userId, 'basic'); | |
| } catch (creditErr) { | |
| return res.json({ success: false, insufficient: true, error: creditErr.message }); | |
| } | |
| const context = formatContext({ hierarchyContext, scriptContext, logContext }); | |
| const fullInput = `USER: ${prompt || "Automatic Feedback"}${context}`; | |
| // Trigger Async | |
| runAsyncFeedback(projectId, userId, fullInput, images || []); | |
| res.json({ success: true, message: "Processing started" }); | |
| } catch (err) { | |
| console.error("Feedback Error:", err); | |
| await StateManager.updateProject(projectId, { status: "error" }); | |
| res.status(500).json({ error: "Feedback process failed" }); | |
| } | |
| }); | |
| app.post('/project/ping', async (req, res) => { | |
| const { projectId, userId, isFrontend } = req.body; | |
| if (!projectId || !userId) return res.status(400).json({ error: "Missing IDs" }); | |
| const project = await StateManager.getProject(projectId); | |
| if (!project || project.userId !== userId) return res.json({ action: "IDLE" }); | |
| // 1. BASE RESPONSE (Visuals) | |
| // Both Frontend and Plugin need this to show "Thinking..." or text generation | |
| const response = { | |
| action: "IDLE", | |
| status: StateManager.getStatus(projectId), | |
| snapshot: StateManager.getSnapshot(projectId) | |
| }; | |
| // 2. EXECUTOR LOGIC (Plugin Only) | |
| // Only pop commands if this is NOT the passive frontend viewer | |
| if (!isFrontend) { | |
| const command = await StateManager.popCommand(projectId); | |
| // We don't need 'streamBuffers' anymore since we use 'snapshot' for visuals. | |
| // But if you have legacy stream logic, we can leave it or remove it. | |
| // For this hybrid model, snapshot is king. | |
| if (command) { | |
| response.action = command.type; | |
| response.target = command.payload; | |
| response.code = command.type === 'EXECUTE' ? command.payload : null; | |
| } | |
| } | |
| res.json(response); | |
| }); | |
| app.post('/human/override', validateRequest, async (req, res) => { | |
| const { projectId, instruction, userId } = req.body; | |
| try { | |
| await checkMinimumCredits(userId, 'basic'); | |
| const project = await StateManager.getProject(projectId); | |
| res.json({ success: true }); | |
| const overrideMsg = `[SYSTEM OVERRIDE]: ${instruction}`; | |
| let overrideText = ""; | |
| let stopStatus = startStatusLoop(projectId, 'worker'); | |
| const workerResult = await AIEngine.callWorkerStream( | |
| project.workerHistory, | |
| overrideMsg, | |
| (thought) => { | |
| stopStatus(); | |
| StateManager.setStatus(projectId, "Worker: Thinking..."); | |
| StateManager.appendSnapshotOnly(projectId, thought); | |
| }, | |
| (chunk) => { | |
| stopStatus(); | |
| StateManager.setStatus(projectId, "Worker: Coding..."); | |
| overrideText += chunk; | |
| StateManager.appendStream(projectId, chunk); | |
| } | |
| ); | |
| stopStatus(); | |
| await StateManager.addHistory(projectId, 'worker', 'user', overrideMsg); | |
| await StateManager.addHistory(projectId, 'worker', 'model', workerResult.text || overrideText); | |
| await processAndQueueResponse(projectId, workerResult.text || overrideText, userId); | |
| const usage = workerResult.usage?.totalTokenCount || 0; | |
| if (usage > 0) await deductUserCredits(userId, usage, 'basic'); | |
| } catch (err) { | |
| console.error("Override failed", err); | |
| } | |
| }); | |
| app.get('/admin/cleanup', async (req, res) => { | |
| try { | |
| const removed = StateManager.cleanupMemory ? StateManager.cleanupMemory() : 0; | |
| res.json({ success: true, removedCount: removed }); | |
| } catch (err) { | |
| res.status(500).json({ error: "Cleanup failed" }); | |
| } | |
| }); | |
| app.listen(PORT, () => { | |
| console.log(`AI Backend Running on ${PORT} (Supabase + Billing Edition)`); | |
| }); | |
| */ |