Sk0lovek commited on
Commit
cb6eabe
·
verified ·
1 Parent(s): 97d3c5c

Update server.ts

Browse files
Files changed (1) hide show
  1. server.ts +62 -143
server.ts CHANGED
@@ -6,7 +6,7 @@ import path from "path";
6
  import mineflayer from "mineflayer";
7
  import pkg from "mineflayer-pathfinder";
8
  const { pathfinder, Movements, goals } = pkg;
9
- import { GoogleGenAI, Type } from "@google/genai";
10
 
11
  const app = express();
12
  const httpServer = createServer(app);
@@ -17,7 +17,6 @@ const io = new Server(httpServer, {
17
  });
18
 
19
  const PORT = Number(process.env.PORT) || 7860;
20
-
21
  let bot: mineflayer.Bot | null = null;
22
  let ai: GoogleGenAI | null = null;
23
  let selectedModel: string = "gemini-3-flash-preview";
@@ -28,25 +27,24 @@ let maxBlocks: number = 10000;
28
  let globalApiKey: string = "";
29
  let chatHistory: { role: string, content: string }[] = [];
30
 
31
- // Helper to log to frontend
32
  const log = (message: string, type: "info" | "success" | "error" | "chat" = "info") => {
33
  io.emit("bot-log", { message, type, timestamp: new Date().toISOString() });
34
  console.log(`[${type.toUpperCase()}] ${message}`);
35
  };
36
 
37
  async function startServer() {
38
- // API Routes
39
  app.get("/api/status", (req, res) => {
40
  res.json({ connected: !!bot, username: bot?.username });
41
  });
42
 
43
- // Socket.io for Bot Control
44
  io.on("connection", (socket) => {
45
  log("Frontend connected to control panel");
46
 
47
  socket.on("connect-bot", (config) => {
48
  const { host, port, version, username, apiKey, model } = config;
49
-
50
  if (bot) {
51
  bot.quit();
52
  log("Existing bot disconnected");
@@ -65,12 +63,13 @@ async function startServer() {
65
  } else {
66
  ai = null;
67
  }
68
-
69
  bot = mineflayer.createBot({
70
  host,
71
  port: parseInt(port) || 25565,
72
  username,
73
  version: version || false,
 
74
  });
75
 
76
  bot.loadPlugin(pathfinder);
@@ -84,11 +83,13 @@ async function startServer() {
84
  if (username === bot?.username) return;
85
  log(`${username}: ${message}`, "chat");
86
 
87
- // Проверяем на !build ИЛИ
88
  if (message.startsWith("!build ") || message.startsWith("!б ")) {
89
  const prompt = message.substring(message.indexOf(" ") + 1);
90
  await handleBuildRequest(prompt, username, false);
91
- } else if (message.startsWith("!next ") || message.startsWith("!н ")) {
 
 
92
  const prompt = message.substring(message.indexOf(" ") + 1);
93
  await handleBuildRequest(prompt, username, true);
94
  }
@@ -100,7 +101,6 @@ async function startServer() {
100
  bot = null;
101
  io.emit("bot-status", { connected: false });
102
  });
103
-
104
  } catch (error: any) {
105
  log(`Failed to initialize: ${error.message}`, "error");
106
  }
@@ -118,199 +118,118 @@ async function startServer() {
118
 
119
  async function handleBuildRequest(prompt: string, requestingUser: string, isNext: boolean) {
120
  if (!bot) return;
121
-
122
  log(`AI is thinking about: "${prompt}"...`);
123
- bot.chat(`Request received from ${requestingUser}, beginning generation...`);
124
 
125
  try {
126
- const systemPrompt = `You are a Minecraft architect and admin. The user "${requestingUser}" wants: "${prompt}".
127
- You can build structures, execute commands, or move around.
128
- Coordinates (x, y, z) for blocks and movement are relative to the bot's starting position (0,0,0).
129
- For commands (like /summon), use relative coordinates (~ ~ ~) which are relative to the bot!
130
- Use standard Minecraft 1.21.1 block names and command syntax.
131
 
132
- CRITICAL 1.21.1 COMMAND SYNTAX:
133
- - Items with custom names/enchantments: give ${requestingUser} cobblestone[custom_name=[{"text":"Name","italic":false}],enchantments={channeling:1}]
134
- - Summoning entities with visible names: summon armor_stand ~2 ~ ~ {CustomName:'{"text":"Palace"}',CustomNameVisible:1b,ArmorItems:[{},{},{},{id:"minecraft:shield",count:1}]}
 
 
135
 
136
- IMPORTANT: To avoid getting stuck inside the blocks you place, ALWAYS start your actions by teleporting or moving away from the build area (e.g., teleporting 5 blocks up and 5 blocks back).
 
 
 
 
137
 
138
- You can build large structures up to ${maxBlocks} blocks.
139
- To save tokens, output a JSON object containing an "actions" array.
140
- Action type 1 (Place Block): [x, y, z, "block_name"]
141
- Action type 2 (Execute Command): ["cmd", "command_string"]
142
- Action type 3 (Walk/Parkour): ["move", x, y, z]
143
- Action type 4 (Teleport): ["tp", x, y, z]
144
-
145
- Example:
146
- {
147
- "actions": [
148
- ["cmd", "gamemode creative @s"],
149
- ["tp", 0, 5, -5],
150
- [0, 0, 0, "stone"],
151
- [0, 1, 0, "oak_planks"],
152
- ["move", 2, 0, 0],
153
- ["cmd", "give ${requestingUser} diamond_sword 1"]
154
- ]
155
- }
156
- Output ONLY the JSON object.`;
157
 
158
- if (!isNext) {
159
- chatHistory = [];
160
- }
161
-
162
- let userMessage = prompt;
163
- if (isNext) {
164
- userMessage = `CONTINUE OR MODIFY THE PREVIOUS BUILD. User request: "${prompt}". Output the FULL JSON array of actions to execute next (e.g. replacing blocks, adding new ones, etc). Keep in mind the coordinates of the previous blocks.`;
165
- }
166
 
167
  chatHistory.push({ role: "user", content: userMessage });
168
 
169
  let responseText = "";
170
-
171
  if (apiType === "openai") {
172
  const url = apiBaseUrl.replace(/\/+$/, '') + '/chat/completions';
173
- const messages = [
174
- { role: "system", content: systemPrompt },
175
- ...chatHistory
176
- ];
177
-
178
  const res = await fetch(url, {
179
  method: "POST",
180
- headers: {
181
- "Content-Type": "application/json",
182
- "Authorization": `Bearer ${globalApiKey}`
183
- },
184
  body: JSON.stringify({
185
  model: customModel || "gpt-3.5-turbo",
186
- messages: messages
187
  })
188
  });
189
-
190
- if (!res.ok) {
191
- const errText = await res.text();
192
- throw new Error(`Custom API Error: ${res.status} ${errText}`);
193
- }
194
-
195
  const data = await res.json();
196
  responseText = data.choices[0].message.content;
197
  } else {
198
- if (!ai) throw new Error("Google GenAI not initialized");
199
-
200
  const contents = chatHistory.map(msg => ({
201
  role: msg.role === "assistant" ? "model" : "user",
202
  parts: [{ text: msg.content }]
203
  }));
204
-
205
  const response = await ai.models.generateContent({
206
  model: selectedModel,
207
  contents: contents,
208
- config: {
209
- systemInstruction: systemPrompt,
210
- responseMimeType: "application/json",
211
- }
212
  });
213
  responseText = response.text;
214
  }
215
 
216
  chatHistory.push({ role: "assistant", content: responseText });
217
 
218
- // Clean up markdown formatting if present
219
  responseText = responseText.replace(/```json/g, '').replace(/```/g, '').trim();
220
-
221
- // Fix bad control characters (like literal newlines or tabs inside strings) that break JSON.parse
222
  responseText = responseText.replace(/[\n\r\t]/g, ' ').replace(/[\x00-\x1F\x7F-\x9F]/g, '');
223
 
224
- const parsed = JSON.parse(responseText);
225
- const actions = parsed.actions;
226
-
227
- if (!Array.isArray(actions)) throw new Error("Invalid AI response format: missing 'actions' array");
228
 
229
- log(`AI generated ${actions.length} actions. Muting command feedback and starting execution...`, "success");
230
-
231
- // Disable command feedback in Minecraft to prevent chat spam
232
  bot.chat('/gamerule sendCommandFeedback false');
233
- bot.chat('/gamerule logAdminCommands false');
234
- bot.chat('/gamemode creative @s'); // Ensure bot is in creative so it doesn't suffocate
235
-
236
  const startPos = bot.entity.position.clone();
237
 
238
  for (const action of actions) {
239
  if (!bot) break;
240
- if (!Array.isArray(action)) continue;
241
-
242
- try {
243
- // 100ms delay to speed up building while avoiding anti-spam kicks (10 blocks/sec)
244
- await new Promise(r => setTimeout(r, 100));
245
-
246
- if (action[0] === "cmd" && typeof action[1] === "string") {
247
- let cmd = action[1];
248
- if (cmd.startsWith("/")) cmd = cmd.substring(1);
249
- log(`Executing command: /${cmd}`, "info");
250
- bot.chat(`/${cmd}`);
251
- } else if (action[0] === "move") {
252
- const dx = Number(action[1]) || 0;
253
- const dy = Number(action[2]) || 0;
254
- const dz = Number(action[3]) || 0;
255
- const targetPos = startPos.offset(dx, dy, dz);
256
- log(`Walking to ~${dx} ~${dy} ~${dz}...`, "info");
257
- const defaultMove = new Movements(bot);
258
- bot.pathfinder.setMovements(defaultMove);
259
- try {
260
- await bot.pathfinder.goto(new goals.GoalBlock(targetPos.x, targetPos.y, targetPos.z));
261
- } catch (e) {
262
- log(`Pathfinding failed or interrupted`, "error");
263
- }
264
- } else if (action[0] === "tp") {
265
- const dx = Number(action[1]) || 0;
266
- const dy = Number(action[2]) || 0;
267
- const dz = Number(action[3]) || 0;
268
- log(`Teleporting to ~${dx} ~${dy} ~${dz}`, "info");
269
- bot.chat(`/tp @s ~${dx} ~${dy} ~${dz}`);
270
- await new Promise(r => setTimeout(r, 500)); // Wait for teleport to complete
271
- } else if (action.length >= 4) {
272
- const [x, y, z, rawBlock] = action;
273
- const targetPos = startPos.offset(x, y, z);
274
-
275
- // Ensure block name has minecraft: prefix if it doesn't already
276
- let blockName = String(rawBlock).toLowerCase();
277
- if (!blockName.startsWith('minecraft:')) {
278
- blockName = `minecraft:${blockName}`;
279
- }
280
 
281
- // @ts-ignore
282
- bot.chat(`/setblock ${Math.floor(targetPos.x)} ${Math.floor(targetPos.y)} ${Math.floor(targetPos.z)} ${blockName}`);
283
- }
 
 
 
 
 
 
 
 
 
 
 
 
284
  } catch (e) {
285
- log(`Failed to execute action: ${JSON.stringify(action)}`, "error");
286
  }
287
  }
288
-
289
- bot.chat("Generation complete.");
290
- log("Generation complete!", "success");
291
  } catch (error: any) {
292
- log(`AI Error: ${error.message}`, "error");
293
- bot.chat(`Error during generation: ${error.message}`);
294
  }
295
  }
296
 
297
- // Vite middleware for development
298
  if (process.env.NODE_ENV !== "production") {
299
- const vite = await createViteServer({
300
- server: { middlewareMode: true },
301
- appType: "spa",
302
- });
303
  app.use(vite.middlewares);
304
  } else {
305
  const distPath = path.join(process.cwd(), "dist");
306
  app.use(express.static(distPath));
307
- app.get("*", (req, res) => {
308
- res.sendFile(path.join(distPath, "index.html"));
309
- });
310
  }
311
 
312
  httpServer.listen(PORT, "0.0.0.0", () => {
313
- log(`Server running on http://localhost:${PORT}`, "success");
314
  });
315
  }
316
 
 
6
  import mineflayer from "mineflayer";
7
  import pkg from "mineflayer-pathfinder";
8
  const { pathfinder, Movements, goals } = pkg;
9
+ import { GoogleGenAI } from "@google/genai";
10
 
11
  const app = express();
12
  const httpServer = createServer(app);
 
17
  });
18
 
19
  const PORT = Number(process.env.PORT) || 7860;
 
20
  let bot: mineflayer.Bot | null = null;
21
  let ai: GoogleGenAI | null = null;
22
  let selectedModel: string = "gemini-3-flash-preview";
 
27
  let globalApiKey: string = "";
28
  let chatHistory: { role: string, content: string }[] = [];
29
 
30
+ // Помощник для логов
31
  const log = (message: string, type: "info" | "success" | "error" | "chat" = "info") => {
32
  io.emit("bot-log", { message, type, timestamp: new Date().toISOString() });
33
  console.log(`[${type.toUpperCase()}] ${message}`);
34
  };
35
 
36
  async function startServer() {
37
+ // API Статус
38
  app.get("/api/status", (req, res) => {
39
  res.json({ connected: !!bot, username: bot?.username });
40
  });
41
 
42
+ // Socket.io управление
43
  io.on("connection", (socket) => {
44
  log("Frontend connected to control panel");
45
 
46
  socket.on("connect-bot", (config) => {
47
  const { host, port, version, username, apiKey, model } = config;
 
48
  if (bot) {
49
  bot.quit();
50
  log("Existing bot disconnected");
 
63
  } else {
64
  ai = null;
65
  }
66
+
67
  bot = mineflayer.createBot({
68
  host,
69
  port: parseInt(port) || 25565,
70
  username,
71
  version: version || false,
72
+ checkTimeoutInterval: 60000, // Увеличиваем ожидание ответа от сервера
73
  });
74
 
75
  bot.loadPlugin(pathfinder);
 
83
  if (username === bot?.username) return;
84
  log(`${username}: ${message}`, "chat");
85
 
86
+ // Реагируем на !build /
87
  if (message.startsWith("!build ") || message.startsWith("!б ")) {
88
  const prompt = message.substring(message.indexOf(" ") + 1);
89
  await handleBuildRequest(prompt, username, false);
90
+ }
91
+ // Реагируем на !next / !н
92
+ else if (message.startsWith("!next ") || message.startsWith("!н ")) {
93
  const prompt = message.substring(message.indexOf(" ") + 1);
94
  await handleBuildRequest(prompt, username, true);
95
  }
 
101
  bot = null;
102
  io.emit("bot-status", { connected: false });
103
  });
 
104
  } catch (error: any) {
105
  log(`Failed to initialize: ${error.message}`, "error");
106
  }
 
118
 
119
  async function handleBuildRequest(prompt: string, requestingUser: string, isNext: boolean) {
120
  if (!bot) return;
 
121
  log(`AI is thinking about: "${prompt}"...`);
122
+ bot.chat(`Request received, thinking...`);
123
 
124
  try {
125
+ const systemPrompt = `You are a Minecraft architect and admin. The user "${requestingUser}" wants: "${prompt}".
126
+ Coordinates (x, y, z) are relative to the bot's position at the START of the generation (0,0,0).
127
+ For commands, use relative coordinates (~ ~ ~) where appropriate.
 
 
128
 
129
+ CRITICAL RULES:
130
+ 1. Output ONLY a valid JSON object.
131
+ 2. STRICTLY NO comments (like // or /*) inside the JSON.
132
+ 3. Use Minecraft 1.21.1 syntax.
133
+ 4. If the user says "come to me", use action ["cmd", "tp @s ${requestingUser}"].
134
 
135
+ Action types:
136
+ - [x, y, z, "block_name"] : Place block
137
+ - ["cmd", "command"] : Execute command
138
+ - ["move", x, y, z] : Walk to relative pos
139
+ - ["tp", x, y, z] : Teleport to relative pos
140
 
141
+ Example: {"actions": [["cmd", "gamemode creative @s"], [0, 5, 0, "stone"]]}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
+ if (!isNext) chatHistory = [];
144
+
145
+ let userMessage = isNext
146
+ ? `CONTINUE/MODIFY build. User says: "${prompt}". Output FULL JSON with new actions.`
147
+ : prompt;
 
 
 
148
 
149
  chatHistory.push({ role: "user", content: userMessage });
150
 
151
  let responseText = "";
 
152
  if (apiType === "openai") {
153
  const url = apiBaseUrl.replace(/\/+$/, '') + '/chat/completions';
 
 
 
 
 
154
  const res = await fetch(url, {
155
  method: "POST",
156
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${globalApiKey}` },
 
 
 
157
  body: JSON.stringify({
158
  model: customModel || "gpt-3.5-turbo",
159
+ messages: [{ role: "system", content: systemPrompt }, ...chatHistory]
160
  })
161
  });
 
 
 
 
 
 
162
  const data = await res.json();
163
  responseText = data.choices[0].message.content;
164
  } else {
165
+ if (!ai) throw new Error("AI not initialized");
 
166
  const contents = chatHistory.map(msg => ({
167
  role: msg.role === "assistant" ? "model" : "user",
168
  parts: [{ text: msg.content }]
169
  }));
 
170
  const response = await ai.models.generateContent({
171
  model: selectedModel,
172
  contents: contents,
173
+ config: { systemInstruction: systemPrompt, responseMimeType: "application/json" }
 
 
 
174
  });
175
  responseText = response.text;
176
  }
177
 
178
  chatHistory.push({ role: "assistant", content: responseText });
179
 
180
+ // Чистка JSON
181
  responseText = responseText.replace(/```json/g, '').replace(/```/g, '').trim();
 
 
182
  responseText = responseText.replace(/[\n\r\t]/g, ' ').replace(/[\x00-\x1F\x7F-\x9F]/g, '');
183
 
184
+ const { actions } = JSON.parse(responseText);
185
+ if (!Array.isArray(actions)) throw new Error("Missing actions array");
 
 
186
 
187
+ log(`Executing ${actions.length} actions...`, "success");
 
 
188
  bot.chat('/gamerule sendCommandFeedback false');
 
 
 
189
  const startPos = bot.entity.position.clone();
190
 
191
  for (const action of actions) {
192
  if (!bot) break;
193
+ await new Promise(r => setTimeout(r, 100));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
+ try {
196
+ if (action[0] === "cmd") {
197
+ bot.chat(`/${action[1].startsWith('/') ? action[1].slice(1) : action[1]}`);
198
+ } else if (action[0] === "move") {
199
+ const target = startPos.offset(action[1], action[2], action[3]);
200
+ bot.pathfinder.setMovements(new Movements(bot));
201
+ await bot.pathfinder.goto(new goals.GoalBlock(target.x, target.y, target.z));
202
+ } else if (action[0] === "tp") {
203
+ bot.chat(`/tp @s ~${action[1]} ~${action[2]} ~${action[3]}`);
204
+ } else if (action.length >= 4) {
205
+ const target = startPos.offset(action[0], action[1], action[2]);
206
+ let block = String(action[3]).toLowerCase();
207
+ if (!block.startsWith('minecraft:')) block = `minecraft:${block}`;
208
+ bot.chat(`/setblock ${Math.floor(target.x)} ${Math.floor(target.y)} ${Math.floor(target.z)} ${block}`);
209
+ }
210
  } catch (e) {
211
+ log("Action failed", "error");
212
  }
213
  }
214
+ bot.chat("Done!");
 
 
215
  } catch (error: any) {
216
+ log(`Error: ${error.message}`, "error");
217
+ bot.chat(`Error: ${error.message}`);
218
  }
219
  }
220
 
221
+ // Vite / Static
222
  if (process.env.NODE_ENV !== "production") {
223
+ const vite = await createViteServer({ server: { middlewareMode: true }, appType: "spa" });
 
 
 
224
  app.use(vite.middlewares);
225
  } else {
226
  const distPath = path.join(process.cwd(), "dist");
227
  app.use(express.static(distPath));
228
+ app.get("*", (req, res) => res.sendFile(path.join(distPath, "index.html")));
 
 
229
  }
230
 
231
  httpServer.listen(PORT, "0.0.0.0", () => {
232
+ log(`Server running on port ${PORT}`, "success");
233
  });
234
  }
235