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

Update server.ts

Browse files
Files changed (1) hide show
  1. server.ts +169 -62
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 } from "@google/genai";
10
 
11
  const app = express();
12
  const httpServer = createServer(app);
@@ -17,6 +17,7 @@ const io = new Server(httpServer, {
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,24 +28,25 @@ let maxBlocks: number = 10000;
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,13 +65,12 @@ async function startServer() {
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,13 +84,11 @@ async function startServer() {
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,6 +100,7 @@ async function startServer() {
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,118 +118,225 @@ async function startServer() {
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
 
 
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
  });
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
  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
  } 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
  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
  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
 
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 nearbyPlayers = Object.values(bot.players)
127
+ .filter(p => p.username !== bot?.username && p.entity)
128
+ .map(p => `${p.username} (distance: ${Math.round(bot!.entity.position.distanceTo(p.entity.position))}m)`)
129
+ .join(", ") || "No one nearby";
130
+
131
+ const systemPrompt = `You are a Minecraft architect, admin, and player. The user "${requestingUser}" wants: "${prompt}".
132
+ You can build structures, execute commands, move around, or interact with players.
133
+ Coordinates (x, y, z) for blocks and movement are relative to the bot's starting position (0,0,0).
134
+ For commands (like /summon), use relative coordinates (~ ~ ~) which are relative to the bot!
135
+ Use standard Minecraft 1.21.1 block names and command syntax.
136
+
137
+ ENVIRONMENT CONTEXT (What you currently see):
138
+ - Nearby players: ${nearbyPlayers}
139
+ - Your position: ${Math.round(bot.entity.position.x)}, ${Math.round(bot.entity.position.y)}, ${Math.round(bot.entity.position.z)}
140
+
141
+ CRITICAL 1.21.1 COMMAND SYNTAX:
142
+ - Items with custom names/enchantments: give ${requestingUser} cobblestone[custom_name=[{"text":"Name","italic":false}],enchantments={channeling:1}]
143
+ - Summoning entities with visible names: summon armor_stand ~2 ~ ~ {CustomName:'{"text":"Palace"}',CustomNameVisible:1b,ArmorItems:[{},{},{},{id:"minecraft:shield",count:1}]}
144
 
145
+ IMPORTANT: To avoid getting stuck inside the blocks you place, ALWAYS start your actions by teleporting or moving away from the build area.
146
+ If you need to attack someone, equip a weapon first using: ["cmd", "item replace entity @s weapon.mainhand with diamond_sword"]
 
 
 
147
 
148
+ You can build large structures up to ${maxBlocks} blocks.
149
+ To save tokens, output a JSON object containing an "actions" array.
150
+ Action type 1 (Place Block): [x, y, z, "block_name"]
151
+ Action type 2 (Execute Command): ["cmd", "command_string"]
152
+ Action type 3 (Walk/Parkour): ["move", x, y, z]
153
+ Action type 4 (Teleport): ["tp", x, y, z]
154
+ Action type 5 (Attack Player): ["attack", "player_name"]
155
+ Action type 6 (Follow Player): ["follow", "player_name"]
156
 
157
+ Example:
158
+ {
159
+ "actions": [
160
+ ["cmd", "item replace entity @s weapon.mainhand with diamond_sword"],
161
+ ["attack", "PlayerName"]
162
+ ]
163
+ }
164
+ Output ONLY the JSON object.`;
165
 
166
+ if (!isNext) {
167
+ chatHistory = [];
168
+ }
169
+
170
+ let userMessage = prompt;
171
+ if (isNext) {
172
+ 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.`;
173
+ }
174
 
175
  chatHistory.push({ role: "user", content: userMessage });
176
 
177
  let responseText = "";
178
+
179
  if (apiType === "openai") {
180
  const url = apiBaseUrl.replace(/\/+$/, '') + '/chat/completions';
181
+ const messages = [
182
+ { role: "system", content: systemPrompt },
183
+ ...chatHistory
184
+ ];
185
+
186
  const res = await fetch(url, {
187
  method: "POST",
188
+ headers: {
189
+ "Content-Type": "application/json",
190
+ "Authorization": `Bearer ${globalApiKey}`
191
+ },
192
  body: JSON.stringify({
193
  model: customModel || "gpt-3.5-turbo",
194
+ messages: messages
195
  })
196
  });
197
+
198
+ if (!res.ok) {
199
+ const errText = await res.text();
200
+ throw new Error(`Custom API Error: ${res.status} ${errText}`);
201
+ }
202
+
203
  const data = await res.json();
204
  responseText = data.choices[0].message.content;
205
  } else {
206
+ if (!ai) throw new Error("Google GenAI not initialized");
207
+
208
  const contents = chatHistory.map(msg => ({
209
  role: msg.role === "assistant" ? "model" : "user",
210
  parts: [{ text: msg.content }]
211
  }));
212
+
213
  const response = await ai.models.generateContent({
214
  model: selectedModel,
215
  contents: contents,
216
+ config: {
217
+ systemInstruction: systemPrompt,
218
+ responseMimeType: "application/json",
219
+ }
220
  });
221
  responseText = response.text;
222
  }
223
 
224
  chatHistory.push({ role: "assistant", content: responseText });
225
 
226
+ // Clean up markdown formatting if present
227
  responseText = responseText.replace(/```json/g, '').replace(/```/g, '').trim();
228
+
229
+ // Fix bad control characters (like literal newlines or tabs inside strings) that break JSON.parse
230
  responseText = responseText.replace(/[\n\r\t]/g, ' ').replace(/[\x00-\x1F\x7F-\x9F]/g, '');
231
 
232
+ const parsed = JSON.parse(responseText);
233
+ const actions = parsed.actions;
234
+
235
+ if (!Array.isArray(actions)) throw new Error("Invalid AI response format: missing 'actions' array");
236
+
237
+ log(`AI generated ${actions.length} actions. Muting command feedback and starting execution...`, "success");
238
 
239
+ // Disable command feedback in Minecraft to prevent chat spam
240
  bot.chat('/gamerule sendCommandFeedback false');
241
+ bot.chat('/gamerule logAdminCommands false');
242
+ bot.chat('/gamemode creative @s'); // Ensure bot is in creative so it doesn't suffocate
243
+
244
  const startPos = bot.entity.position.clone();
245
 
246
  for (const action of actions) {
247
  if (!bot) break;
248
+ if (!Array.isArray(action)) continue;
249
+
250
  try {
251
+ // 100ms delay to speed up building while avoiding anti-spam kicks (10 blocks/sec)
252
+ await new Promise(r => setTimeout(r, 100));
253
+
254
+ if (action[0] === "cmd" && typeof action[1] === "string") {
255
+ let cmd = action[1];
256
+ if (cmd.startsWith("/")) cmd = cmd.substring(1);
257
+ log(`Executing command: /${cmd}`, "info");
258
+ bot.chat(`/${cmd}`);
259
+ } else if (action[0] === "move") {
260
+ const dx = Number(action[1]) || 0;
261
+ const dy = Number(action[2]) || 0;
262
+ const dz = Number(action[3]) || 0;
263
+ const targetPos = startPos.offset(dx, dy, dz);
264
+ log(`Walking to ~${dx} ~${dy} ~${dz}...`, "info");
265
+ const defaultMove = new Movements(bot);
266
+ bot.pathfinder.setMovements(defaultMove);
267
+ try {
268
+ await bot.pathfinder.goto(new goals.GoalBlock(targetPos.x, targetPos.y, targetPos.z));
269
+ } catch (e) {
270
+ log(`Pathfinding failed or interrupted`, "error");
271
+ }
272
+ } else if (action[0] === "tp") {
273
+ const dx = Number(action[1]) || 0;
274
+ const dy = Number(action[2]) || 0;
275
+ const dz = Number(action[3]) || 0;
276
+ log(`Teleporting to ~${dx} ~${dy} ~${dz}`, "info");
277
+ bot.chat(`/tp @s ~${dx} ~${dy} ~${dz}`);
278
+ await new Promise(r => setTimeout(r, 500)); // Wait for teleport to complete
279
+ } else if (action[0] === "attack" || action[0] === "follow") {
280
+ const targetName = action[1];
281
+ const target = bot.players[targetName]?.entity;
282
+ if (target) {
283
+ log(`${action[0] === "attack" ? "Attacking" : "Following"} ${targetName}...`, "info");
284
+ const defaultMove = new Movements(bot);
285
+ bot.pathfinder.setMovements(defaultMove);
286
+ try {
287
+ await bot.pathfinder.goto(new goals.GoalNear(target.position.x, target.position.y, target.position.z, action[0] === "attack" ? 2 : 3));
288
+ if (action[0] === "attack") {
289
+ bot.attack(target);
290
+ }
291
+ } catch (e) {
292
+ log(`Could not reach ${targetName}`, "error");
293
+ }
294
+ } else {
295
+ log(`Cannot find player ${targetName}`, "error");
296
+ }
297
+ } else if (action.length >= 4) {
298
+ const [x, y, z, rawBlock] = action;
299
+ const targetPos = startPos.offset(x, y, z);
300
+
301
+ // Ensure block name has minecraft: prefix if it doesn't already
302
+ let blockName = String(rawBlock).toLowerCase();
303
+ if (!blockName.startsWith('minecraft:')) {
304
+ blockName = `minecraft:${blockName}`;
305
+ }
306
+
307
+ // @ts-ignore
308
+ bot.chat(`/setblock ${Math.floor(targetPos.x)} ${Math.floor(targetPos.y)} ${Math.floor(targetPos.z)} ${blockName}`);
309
+ }
310
  } catch (e) {
311
+ log(`Failed to execute action: ${JSON.stringify(action)}`, "error");
312
  }
313
  }
314
+
315
+ bot.chat("Generation complete.");
316
+ log("Generation complete!", "success");
317
  } catch (error: any) {
318
+ log(`AI Error: ${error.message}`, "error");
319
+ bot.chat(`Error during generation: ${error.message}`);
320
  }
321
  }
322
 
323
+ // Vite middleware for development
324
  if (process.env.NODE_ENV !== "production") {
325
+ const vite = await createViteServer({
326
+ server: { middlewareMode: true },
327
+ appType: "spa",
328
+ });
329
  app.use(vite.middlewares);
330
  } else {
331
  const distPath = path.join(process.cwd(), "dist");
332
  app.use(express.static(distPath));
333
+ app.get("*", (req, res) => {
334
+ res.sendFile(path.join(distPath, "index.html"));
335
+ });
336
  }
337
 
338
  httpServer.listen(PORT, "0.0.0.0", () => {
339
+ log(`Server running on http://localhost:${PORT}`, "success");
340
  });
341
  }
342