everydaycats commited on
Commit
ab9b3de
·
verified ·
1 Parent(s): 45ae1d9

Update app.js

Browse files
Files changed (1) hide show
  1. app.js +111 -296
app.js CHANGED
@@ -1,327 +1,142 @@
1
  // server.js
2
- // ESM Node (package.json should contain "type": "module")
3
- // Install: npm install express body-parser cors @google/genai
4
- // Run: GEMINI_API_KEY=your_key node server.js
5
 
6
  import express from "express";
7
  import bodyParser from "body-parser";
8
  import cors from "cors";
9
- import { GoogleGenAI } from "@google/genai";
 
 
 
10
 
11
- const PORT = process.env.PORT ? Number(process.env.PORT) : 7860;
12
- if (!process.env.GEMINI_API_KEY) {
13
- console.error("GEMINI_API_KEY environment variable is required.");
14
- process.exit(1);
15
- }
16
 
17
- const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
 
18
 
19
- const app = express();
20
- app.use(cors());
21
- app.use(bodyParser.json({ limit: "2mb" }));
22
-
23
- const projects = new Map();
24
- const simpleId = (prefix = "") => prefix + Math.random().toString(36).slice(2, 9);
25
-
26
- /**
27
- * STRICT VOCABULARY - Matched to Plugin
28
- */
29
- const VALID_ACTIONS = [
30
- "create_part",
31
- "create_model",
32
- "create_meshpart",
33
- "create_script",
34
- "attach_script",
35
- "edit_script",
36
- "set_property",
37
- "move_to",
38
- "rotate_to",
39
- "set_parent",
40
- "delete",
41
- "weld",
42
- "list_workspace",
43
- "finish"
44
- ];
45
-
46
- const ACTION_SYNONYMS = {
47
- "create_complex_mesh": "create_meshpart",
48
- "load_mesh": "create_meshpart",
49
- "move": "move_to",
50
- "position": "move_to",
51
- "rotate": "rotate_to",
52
- "set_rotation": "rotate_to",
53
- "add_script": "create_script",
54
- "insert_script": "create_script",
55
- "modify_script": "edit_script",
56
- "append_script": "attach_script",
57
- "group": "create_model",
58
- "parent": "set_parent",
59
- "set_parent_to": "set_parent",
60
- "join": "weld",
61
- "union": "create_model",
62
- "create_union": "create_model",
63
- "create_rig": "create_model",
64
- "add_bone": "create_part"
65
- };
66
-
67
- function validateAndPrepareTasks(arr) {
68
- if (!Array.isArray(arr)) throw new Error("Model output must be a JSON array of tasks.");
69
- return arr.map((t) => {
70
- if (!t || typeof t !== "object") throw new Error("Each task must be an object.");
71
- if (!t.action) throw new Error("Each task must have an action field.");
72
-
73
- let actionRaw = String(t.action).trim();
74
- const actionLower = actionRaw.toLowerCase();
75
-
76
- let canonicalAction = null;
77
- if (VALID_ACTIONS.includes(actionRaw)) canonicalAction = actionRaw;
78
- if (!canonicalAction) {
79
- canonicalAction = VALID_ACTIONS.find(a => a.toLowerCase() === actionLower) || null;
80
- }
81
- if (!canonicalAction && ACTION_SYNONYMS[actionLower]) {
82
- canonicalAction = ACTION_SYNONYMS[actionLower];
83
- }
84
-
85
- if (!canonicalAction) {
86
- throw new Error(`Invalid action '${actionRaw}'. Supported: ${VALID_ACTIONS.join(", ")}`);
87
- }
88
-
89
- const id = t.id || simpleId("cmd_");
90
- const params = t.params && typeof t.params === "object" ? t.params : {};
91
- return { id, action: canonicalAction, originalAction: actionRaw, params, target_ref: t.target_ref || null };
92
- });
93
- }
94
-
95
- async function generateTasks(project) {
96
- const historySample = (project.history || []).slice(-20).map(h => ({
97
- commandId: h.commandId,
98
- status: h.status,
99
- message: h.message ? (typeof h.message === "string" ? (h.message.length > 120 ? h.message.slice(0, 117) + "..." : h.message) : null) : null,
100
- originalAction: h.originalAction || null,
101
- target_ref: h.target_ref || null,
102
- }));
103
- const refsKeys = Object.keys(project._refs || {}).slice(0, 200);
104
 
105
- const stateSummary = {
106
- projectId: project.id,
107
- refsSummary: refsKeys,
108
- recentHistory: historySample
109
- };
110
 
111
- const systemPrompt = `
112
- You are a precise Roblox Studio Assistant.
113
- Allowed actions: ${VALID_ACTIONS.join(", ")}.
114
-
115
- RULES:
116
- 1. **References:** When creating an object, ALWAYS assign a unique "target_ref" (e.g., "ref_wall_1").
117
- 2. **Chaining:** When moving/modifying later, use "target_ref": "ref_wall_1".
118
- 3. **Hierarchy:** To put a Part inside a Model: create_model (ref_grp) -> create_part (ref_part) -> set_parent (target_ref: ref_part, params: { parent_ref: ref_grp }).
119
- 4. **Math:** Use numbers. Position {x,y,z}. Rotation {x,y,z} (degrees).
120
- 5. **Output:** Return ONLY a JSON Array.
121
-
122
- EXISTING STATE:
123
- ${JSON.stringify(stateSummary, null, 2)}
124
-
125
- User Request:
126
- ${JSON.stringify({ description: project.description })}
127
- `;
128
 
129
- // Using 1.5-pro as the closest valid "Pro" model.
130
- // If you strictly need "gemini-2.5-pro", allow the string, but note it might 404 if not enabled on your account.
131
- const resp = await ai.models.generateContent({
132
- model: "gemini-2.5-pro",
133
- contents: systemPrompt,
134
- generationConfig: { responseMimeType: "application/json" }
135
- });
136
 
137
- // === FIX: Robust Response Text Extraction ===
138
- let text = "";
139
- if (resp && typeof resp.text === 'string') {
140
- // SDK returned text property directly
141
- text = resp.text;
142
- } else if (resp && typeof resp.text === 'function') {
143
- // SDK returned text method
144
- text = resp.text();
145
- } else if (resp?.response?.text) {
146
- // Nested response object
147
- text = typeof resp.response.text === 'function' ? resp.response.text() : resp.response.text;
148
- } else {
149
- // Fallback for raw data structure
150
- text = resp?.data?.candidates?.[0]?.content?.parts?.[0]?.text || "";
151
- }
152
- // ============================================
153
 
154
- let arr;
 
155
  try {
156
- // Sanitize markdown if still present
157
- const cleanText = text.replace(/```json/g, "").replace(/```/g, "").trim();
158
- arr = JSON.parse(cleanText);
159
- } catch (err) {
160
- // Fallback extraction
161
- const start = text.indexOf("[");
162
- const end = text.lastIndexOf("]");
163
- if (start >= 0 && end > start) {
164
- arr = JSON.parse(text.slice(start, end + 1));
165
- } else {
166
- console.error("AI Output was:", text);
167
- throw new Error("Could not parse JSON from model output.");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  }
169
- }
170
-
171
- return validateAndPrepareTasks(arr);
172
- }
173
-
174
- // --- ENDPOINTS ---
175
-
176
- app.post("/projects", async (req, res) => {
177
- try {
178
- const projectId = req.body.projectId || simpleId("proj_");
179
- const project = {
180
- id: projectId,
181
- description: req.body.description || "New Project",
182
- status: "queued",
183
- tasks: [],
184
- commandQueue: [],
185
- pendingResults: new Map(),
186
- running: false,
187
- history: [],
188
- _refs: {}
189
- };
190
 
191
- projects.set(projectId, project);
192
-
193
- runProject(project).catch((err) => {
194
- project.status = "error";
195
- console.error("runProject error:", err);
196
- });
197
-
198
- return res.status(202).json({ projectId, status: "accepted" });
199
  } catch (err) {
200
- return res.status(500).json({ error: err.message });
 
201
  }
202
  });
203
 
204
- app.post("/projects/:projectId/prompt", async (req, res) => {
205
- const project = projects.get(req.params.projectId);
206
- if (!project) return res.status(404).json({ error: "project not found" });
207
-
208
- const prompt = req.body.prompt;
209
- project.description += `\nUser Update: ${prompt}`;
210
-
211
  try {
212
- const newTasks = await generateTasks({ ...project, description: prompt });
213
- project.tasks.push(...newTasks);
214
- project.commandQueue.push(...newTasks);
215
- return res.json({ appended: newTasks.length, tasks: newTasks });
216
- } catch (err) {
217
- return res.status(500).json({ error: err.message });
218
- }
219
- });
220
-
221
- app.get("/projects/:projectId/next", (req, res) => {
222
- const project = projects.get(req.params.projectId);
223
- if (!project) return res.status(404).json({ error: "project not found" });
224
-
225
- if (project.commandQueue.length > 0) {
226
- return res.json(project.commandQueue.shift());
227
- }
228
- return res.status(204).end();
229
- });
230
-
231
- app.post("/projects/:projectId/result", (req, res) => {
232
- const project = projects.get(req.params.projectId);
233
- if (!project) return res.status(404).json({ error: "project not found" });
234
-
235
- const { commandId, status, message, extra, target_ref } = req.body;
236
-
237
- // History recording
238
- const entry = {
239
- commandId,
240
- status,
241
- message: message || null,
242
- extra: extra || null,
243
- target_ref: target_ref || null,
244
- ts: Date.now()
245
- };
246
- project.history = project.history || [];
247
- project.history.push(entry);
248
-
249
- if (target_ref && status === "ok") {
250
- project._refs = project._refs || {};
251
- project._refs[target_ref] = { commandId, ts: Date.now() };
252
- }
253
 
254
- const waiter = project.pendingResults.get(commandId);
255
- if (waiter) {
256
- project.pendingResults.delete(commandId);
257
- waiter.resolve({ status, message, target_ref });
258
- return res.json({ ok: true });
259
  }
260
-
261
- return res.json({ ok: true });
262
  });
263
 
264
- app.get("/status/:projectId", (req, res) => {
265
- const project = projects.get(req.params.projectId);
266
- if (!project) return res.status(404).json({ error: "project not found" });
267
- return res.json({
268
- id: project.id,
269
- status: project.status,
270
- queued: project.commandQueue.length,
271
- refs: project._refs || {}
272
- });
273
- });
274
-
275
- async function runProject(project) {
276
- if (project.running) return;
277
- project.running = true;
278
- project.status = "running";
279
-
280
  try {
281
- if (!project.tasks || project.tasks.length === 0) {
282
- project.tasks = await generateTasks(project);
283
- }
284
- project.commandQueue.push(...project.tasks);
285
-
286
- // Blocking wait logic
287
- for (const task of project.tasks) {
288
- const result = await new Promise((resolve, reject) => {
289
- const timeoutMs = 3 * 60 * 1000;
290
- let timedOut = false;
291
- const timeout = setTimeout(() => {
292
- timedOut = true;
293
- project.pendingResults.delete(task.id);
294
- reject(new Error("Command timed out: " + task.id));
295
- }, timeoutMs);
296
-
297
- project.pendingResults.set(task.id, {
298
- resolve: (payload) => {
299
- if (timedOut) return;
300
- clearTimeout(timeout);
301
- resolve(payload);
302
- },
303
- reject: (err) => {
304
- if (timedOut) return;
305
- clearTimeout(timeout);
306
- reject(err);
307
- }
308
- });
309
- });
310
-
311
- if (result.status !== "ok") {
312
- project.status = "failed";
313
- throw new Error(`Command ${task.id} failed: ${result.message}`);
314
- }
315
- }
316
- project.status = "complete";
317
  } catch (err) {
318
- console.error("Project run error:", err);
319
- if (project.status !== "failed") project.status = "error";
320
- } finally {
321
- project.running = false;
322
  }
323
- }
324
 
 
325
  app.listen(PORT, () => {
326
- console.log(`BloxBuddy backend listening on port ${PORT}`);
 
327
  });
 
1
  // server.js
2
+ // Node + Express server to accept Snapshotter payloads from the Roblox plugin.
3
+ // Save as server.js and run: node server.js
4
+ // Installs: npm install express body-parser cors
5
 
6
  import express from "express";
7
  import bodyParser from "body-parser";
8
  import cors from "cors";
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import { fileURLToPath } from "url";
12
+ import fetch from "node-fetch"; // Node 18+ has fetch globally; if not, install node-fetch
13
 
14
+ // If you're running Node 18+, you can use global fetch and remove node-fetch import.
15
+ // If you get "fetch is not defined", run: npm install node-fetch
16
+ // and uncomment the import above (already present).
 
 
17
 
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = path.dirname(__filename);
20
 
21
+ const PORT = process.env.PORT ? Number(process.env.PORT) : 3000;
22
+ const STORAGE_DIR = path.join(__dirname, "data");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
+ if (!fs.existsSync(STORAGE_DIR)) fs.mkdirSync(STORAGE_DIR, { recursive: true });
 
 
 
 
25
 
26
+ const app = express();
27
+ app.use(cors());
28
+ app.use(bodyParser.json({ limit: "10mb" })); // Allow larger payloads (tweak as needed)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
+ // Simple logging middleware
31
+ app.use((req, res, next) => {
32
+ console.log(new Date().toISOString(), req.method, req.url);
33
+ next();
34
+ });
 
 
35
 
36
+ // Simple health/ping
37
+ app.get("/ping", (req, res) => res.status(200).send("pong"));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
+ // Compatibility endpoint from your older plugin code
40
+ app.post("/capture-and-upload", async (req, res) => {
41
  try {
42
+ const payload = req.body;
43
+ if (!payload) return res.status(400).json({ success: false, error: "empty payload" });
44
+
45
+ // generate an id and save payload to disk
46
+ const id = "asset_" + Math.random().toString(36).slice(2, 10);
47
+ const filename = path.join(STORAGE_DIR, `${id}.json`);
48
+ fs.writeFileSync(filename, JSON.stringify({ receivedAt: Date.now(), payload }, null, 2), "utf8");
49
+
50
+ // Optionally perform immediate processing here (call AI service, upload images...).
51
+ // For this demo we simply accept and return an assetId.
52
+
53
+ // If client asked for a callbackUrl, try to POST back a small completion message
54
+ if (payload.callbackUrl && typeof payload.callbackUrl === "string") {
55
+ const cbBody = { assetId: id, status: "processed", ts: Date.now() };
56
+ // fire-and-forget; do not block the main response on callback success
57
+ (async () => {
58
+ try {
59
+ // Use fetch if available
60
+ if (typeof fetch === "function") {
61
+ const r = await fetch(payload.callbackUrl, {
62
+ method: "POST",
63
+ headers: { "Content-Type": "application/json" },
64
+ body: JSON.stringify(cbBody),
65
+ timeout: 10000
66
+ });
67
+ console.log("Callback posted, status:", r.status);
68
+ } else {
69
+ console.log("No fetch available to call callback URL.");
70
+ }
71
+ } catch (err) {
72
+ console.warn("Callback POST failed:", err && err.message ? err.message : err);
73
+ }
74
+ })();
75
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
+ return res.json({ success: true, assetId: id });
 
 
 
 
 
 
 
78
  } catch (err) {
79
+ console.error("capture-and-upload error:", err);
80
+ return res.status(500).json({ success: false, error: String(err) });
81
  }
82
  });
83
 
84
+ // New route name used in the plugin snippet: /api/ai-build
85
+ app.post("/api/ai-build", async (req, res) => {
 
 
 
 
 
86
  try {
87
+ const payload = req.body;
88
+ if (!payload) return res.status(400).json({ success: false, error: "empty payload" });
89
+
90
+ // Save payload to disk
91
+ const id = "asset_" + Math.random().toString(36).slice(2, 10);
92
+ const filename = path.join(STORAGE_DIR, `${id}.json`);
93
+ fs.writeFileSync(filename, JSON.stringify({ receivedAt: Date.now(), payload }, null, 2), "utf8");
94
+
95
+ // (Here you could call the Gemini API, save images, run processing, etc.)
96
+ // For now we immediately return success with the generated asset id.
97
+
98
+ // If payload provides callbackUrl, POST a callback (fire-and-forget)
99
+ if (payload.callbackUrl && typeof payload.callbackUrl === "string") {
100
+ const cbBody = { assetId: id, status: "processed", ts: Date.now() };
101
+ (async () => {
102
+ try {
103
+ if (typeof fetch === "function") {
104
+ await fetch(payload.callbackUrl, {
105
+ method: "POST",
106
+ headers: { "Content-Type": "application/json" },
107
+ body: JSON.stringify(cbBody)
108
+ });
109
+ console.log("Callback sent to", payload.callbackUrl);
110
+ }
111
+ } catch (err) {
112
+ console.warn("Callback failed:", err && err.message ? err.message : err);
113
+ }
114
+ })();
115
+ }
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
+ return res.json({ success: true, assetId: id, message: "received" });
118
+ } catch (err) {
119
+ console.error("/api/ai-build error:", err);
120
+ return res.status(500).json({ success: false, error: String(err) });
 
121
  }
 
 
122
  });
123
 
124
+ // Optional: status endpoint for an asset (reads saved JSON)
125
+ app.get("/asset/:id", (req, res) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  try {
127
+ const id = req.params.id;
128
+ const filename = path.join(STORAGE_DIR, `${id}.json`);
129
+ if (!fs.existsSync(filename)) return res.status(404).json({ found: false });
130
+ const content = fs.readFileSync(filename, "utf8");
131
+ return res.json({ found: true, data: JSON.parse(content) });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  } catch (err) {
133
+ console.error("/asset error", err);
134
+ return res.status(500).json({ success: false, error: String(err) });
 
 
135
  }
136
+ });
137
 
138
+ // Start server
139
  app.listen(PORT, () => {
140
+ console.log(`Plugin backend listening on http://localhost:${PORT}`);
141
+ console.log("Saved payloads will be stored in:", STORAGE_DIR);
142
  });