bentosmau commited on
Commit
bc2ac28
·
1 Parent(s): fc132b8

Implement API key authentication for enhanced NeoAPI security

Browse files

Add API key generation, validation, and revocation endpoints to the API server, along with a JavaScript client for frontend integration.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: e3ff2484-bbd8-4aba-bea0-1940769b874a
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: b75b4233-01b4-45d5-b7c0-6e836674316e
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1739408b-93a5-479b-a658-30f2493b0467/e3ff2484-bbd8-4aba-bea0-1940769b874a/eHvZpi8
Replit-Helium-Checkpoint-Created: true

artifacts/api-server/data/api-keys.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "key": "neo1-0ebbf10959c3ac6d2023d9e9f3111f0051c91a0d",
4
+ "name": "mi-pagina-web",
5
+ "createdAt": "2026-04-14T21:31:10.658Z",
6
+ "active": true,
7
+ "usageCount": 1,
8
+ "lastUsed": "2026-04-14T21:31:15.634Z"
9
+ }
10
+ ]
artifacts/api-server/src/lib/apiKeys.ts ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import crypto from "crypto";
4
+
5
+ const DATA_DIR = path.join(process.cwd(), "data");
6
+ const KEYS_FILE = path.join(DATA_DIR, "api-keys.json");
7
+
8
+ export interface ApiKey {
9
+ key: string;
10
+ name: string;
11
+ createdAt: string;
12
+ active: boolean;
13
+ usageCount: number;
14
+ lastUsed: string | null;
15
+ }
16
+
17
+ function ensureDataDir() {
18
+ if (!fs.existsSync(DATA_DIR)) {
19
+ fs.mkdirSync(DATA_DIR, { recursive: true });
20
+ }
21
+ }
22
+
23
+ function readKeys(): ApiKey[] {
24
+ ensureDataDir();
25
+ if (!fs.existsSync(KEYS_FILE)) return [];
26
+ try {
27
+ return JSON.parse(fs.readFileSync(KEYS_FILE, "utf-8"));
28
+ } catch {
29
+ return [];
30
+ }
31
+ }
32
+
33
+ function writeKeys(keys: ApiKey[]) {
34
+ ensureDataDir();
35
+ fs.writeFileSync(KEYS_FILE, JSON.stringify(keys, null, 2), "utf-8");
36
+ }
37
+
38
+ export function generateKey(name: string): ApiKey {
39
+ const keys = readKeys();
40
+ const newKey: ApiKey = {
41
+ key: `neo1-${crypto.randomBytes(20).toString("hex")}`,
42
+ name,
43
+ createdAt: new Date().toISOString(),
44
+ active: true,
45
+ usageCount: 0,
46
+ lastUsed: null,
47
+ };
48
+ keys.push(newKey);
49
+ writeKeys(keys);
50
+ return newKey;
51
+ }
52
+
53
+ export function listKeys(): Omit<ApiKey, "key">[] {
54
+ return readKeys().map(({ key, ...rest }) => ({
55
+ ...rest,
56
+ keyPreview: `${key.slice(0, 10)}...${key.slice(-4)}`,
57
+ })) as Omit<ApiKey, "key">[];
58
+ }
59
+
60
+ export function validateKey(key: string): ApiKey | null {
61
+ const keys = readKeys();
62
+ const found = keys.find((k) => k.key === key && k.active);
63
+ if (!found) return null;
64
+
65
+ found.usageCount += 1;
66
+ found.lastUsed = new Date().toISOString();
67
+ writeKeys(keys);
68
+ return found;
69
+ }
70
+
71
+ export function revokeKey(keyOrName: string): boolean {
72
+ const keys = readKeys();
73
+ const idx = keys.findIndex(
74
+ (k) => k.key === keyOrName || k.name === keyOrName
75
+ );
76
+ if (idx === -1) return false;
77
+ keys[idx].active = false;
78
+ writeKeys(keys);
79
+ return true;
80
+ }
81
+
82
+ export function getAdminSecret(): string {
83
+ return process.env["NEO_ADMIN_SECRET"] ?? "neo-admin-change-me";
84
+ }
artifacts/api-server/src/middlewares/auth.ts ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Request, Response, NextFunction } from "express";
2
+ import { validateKey, getAdminSecret } from "../lib/apiKeys.js";
3
+
4
+ export function requireApiKey(req: Request, res: Response, next: NextFunction) {
5
+ const authHeader = req.headers["authorization"] ?? "";
6
+ const key = authHeader.startsWith("Bearer ")
7
+ ? authHeader.slice(7).trim()
8
+ : (req.headers["x-api-key"] as string | undefined)?.trim() ?? "";
9
+
10
+ if (!key) {
11
+ res.status(401).json({ error: "API key requerida. Usa el header: Authorization: Bearer <tu-key>" });
12
+ return;
13
+ }
14
+
15
+ const found = validateKey(key);
16
+ if (!found) {
17
+ res.status(403).json({ error: "API key inválida o revocada." });
18
+ return;
19
+ }
20
+
21
+ (req as any).apiKey = found;
22
+ next();
23
+ }
24
+
25
+ export function requireAdmin(req: Request, res: Response, next: NextFunction) {
26
+ const secret = req.headers["x-admin-secret"] as string | undefined;
27
+ if (!secret || secret !== getAdminSecret()) {
28
+ res.status(403).json({ error: "Admin secret incorrecto." });
29
+ return;
30
+ }
31
+ next();
32
+ }
artifacts/api-server/src/routes/index.ts CHANGED
@@ -1,8 +1,12 @@
1
  import { Router, type IRouter } from "express";
2
- import healthRouter from "./health";
 
 
3
 
4
  const router: IRouter = Router();
5
 
6
  router.use(healthRouter);
 
 
7
 
8
  export default router;
 
1
  import { Router, type IRouter } from "express";
2
+ import healthRouter from "./health.js";
3
+ import keysRouter from "./keys.js";
4
+ import neoRouter from "./neo.js";
5
 
6
  const router: IRouter = Router();
7
 
8
  router.use(healthRouter);
9
+ router.use(keysRouter);
10
+ router.use(neoRouter);
11
 
12
  export default router;
artifacts/api-server/src/routes/keys.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from "express";
2
+ import { requireAdmin } from "../middlewares/auth.js";
3
+ import { generateKey, listKeys, revokeKey } from "../lib/apiKeys.js";
4
+
5
+ const router = Router();
6
+
7
+ router.post("/keys", requireAdmin, (req, res) => {
8
+ const { name } = req.body as { name?: string };
9
+ if (!name || typeof name !== "string" || !name.trim()) {
10
+ res.status(400).json({ error: "El campo 'name' es obligatorio." });
11
+ return;
12
+ }
13
+ const key = generateKey(name.trim());
14
+ res.status(201).json({
15
+ message: "API key generada. Guárdala, no se vuelve a mostrar.",
16
+ key: key.key,
17
+ name: key.name,
18
+ createdAt: key.createdAt,
19
+ });
20
+ });
21
+
22
+ router.get("/keys", requireAdmin, (_req, res) => {
23
+ res.json({ keys: listKeys() });
24
+ });
25
+
26
+ router.delete("/keys/:name", requireAdmin, (req, res) => {
27
+ const { name } = req.params;
28
+ const ok = revokeKey(name!);
29
+ if (!ok) {
30
+ res.status(404).json({ error: "Key no encontrada o ya revocada." });
31
+ return;
32
+ }
33
+ res.json({ message: `Key '${name}' revocada.` });
34
+ });
35
+
36
+ export default router;
artifacts/api-server/src/routes/neo.ts ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from "express";
2
+ import { requireApiKey } from "../middlewares/auth.js";
3
+
4
+ const router = Router();
5
+ const NEO_REST_URL = "http://localhost:5001";
6
+
7
+ router.post("/neo/chat", requireApiKey, async (req, res) => {
8
+ const { message, history = [] } = req.body as {
9
+ message?: string;
10
+ history?: Array<{ role: string; content: string }>;
11
+ };
12
+
13
+ if (!message || typeof message !== "string" || !message.trim()) {
14
+ res.status(400).json({ error: "El campo 'message' es obligatorio." });
15
+ return;
16
+ }
17
+
18
+ try {
19
+ const response = await fetch(`${NEO_REST_URL}/chat`, {
20
+ method: "POST",
21
+ headers: { "Content-Type": "application/json" },
22
+ body: JSON.stringify({ message: message.trim(), history }),
23
+ });
24
+
25
+ if (!response.ok) {
26
+ const err = await response.text();
27
+ res.status(502).json({ error: "Error del modelo NEO-1", detail: err });
28
+ return;
29
+ }
30
+
31
+ const data = await response.json() as { response: string; model: string; status: string };
32
+ res.json({
33
+ response: data.response,
34
+ model: data.model ?? "mdfjbots-neo-1",
35
+ status: "ok",
36
+ });
37
+ } catch (err: any) {
38
+ res.status(503).json({
39
+ error: "No se pudo conectar con el modelo NEO-1. ¿Está corriendo?",
40
+ detail: err?.message ?? String(err),
41
+ });
42
+ }
43
+ });
44
+
45
+ router.get("/neo/models", requireApiKey, (_req, res) => {
46
+ res.json({
47
+ models: [
48
+ {
49
+ id: "mdfjbots-neo-1",
50
+ name: "NEO-1",
51
+ description: "Asistente conversacional con soporte de Roblox, calculadora y temas educativos.",
52
+ version: "1.13.1",
53
+ },
54
+ ],
55
+ });
56
+ });
57
+
58
+ export default router;
chat-app/app.py CHANGED
@@ -1,142 +1,17 @@
1
  import os
2
- import re
3
- import json
4
  import time
5
  import gradio as gr
 
 
 
 
 
6
  from roblox_api import buscar_jugador, buscar_juego, formatear_jugador, formatear_juego
7
  from matematicas import (
8
  es_solicitud_calculadora, es_operacion_matematica,
9
- resolver_operacion, formatear_resultado, extraer_nombre_usuario
10
  )
11
 
12
- GENEROS_ROBLOX = {
13
- "Town and City": "un juego de vida urbana y roleplay social",
14
- "Adventure": "un juego de aventura y exploración",
15
- "Role Playing": "un juego de rol donde puedes ser quien quieras",
16
- "Comedy": "un juego de comedia y entretenimiento",
17
- "Action": "un juego de acción y combate",
18
- "Horror": "un juego de terror y suspenso",
19
- "Military": "un juego de temática militar y estrategia",
20
- "Medieval": "un juego de temática medieval con castillos y caballeros",
21
- "Naval": "un juego de aventuras navales y piratas",
22
- "Sci-Fi": "un juego de ciencia ficción",
23
- "Sports": "un juego deportivo",
24
- "Fighting": "un juego de peleas y combate",
25
- "Western": "un juego de temática del lejano oeste",
26
- "FPS": "un juego de disparos en primera persona",
27
- "Building": "un juego de construcción y creatividad",
28
- "Simulator": "un simulador",
29
- "Tycoon": "un juego de gestión y negocios (tycoon)",
30
- "Obby": "un juego de obstáculos (obby)",
31
- "All Genres": "un juego variado",
32
- }
33
-
34
- def cargar_respuestas():
35
- ruta = os.path.join(os.path.dirname(__file__), "respuestas.json")
36
- try:
37
- with open(ruta, "r", encoding="utf-8") as f:
38
- datos = json.load(f)
39
- return datos.get("respuestas", [])
40
- except Exception:
41
- return []
42
-
43
- RESPUESTAS_PERSONALIZADAS = cargar_respuestas()
44
-
45
- def buscar_respuesta_personalizada(mensaje):
46
- texto = mensaje.lower().strip()
47
- for entrada in RESPUESTAS_PERSONALIZADAS:
48
- for pregunta in entrada.get("preguntas", []):
49
- if pregunta.lower() in texto or texto in pregunta.lower():
50
- return entrada.get("respuesta")
51
- return None
52
-
53
- def generar_explicacion_juego(datos):
54
- nombre = datos.get("nombre", "Este juego")
55
- tema = datos.get("tema", "")
56
- descripcion = datos.get("descripcion", "").strip()
57
- visitas = datos.get("visitas", 0)
58
- creador = datos.get("creador", "")
59
-
60
- tipo = GENEROS_ROBLOX.get(tema, f"un juego de {tema.lower()}" if tema else "un juego")
61
-
62
- partes = [f"**{nombre}** es {tipo} de Roblox creado por **{creador}**."]
63
-
64
- if descripcion and descripcion != "Sin descripción.":
65
- desc_corta = descripcion[:200] + ("..." if len(descripcion) > 200 else "")
66
- partes.append(f"Según su descripción oficial: *\"{desc_corta}\"*")
67
-
68
- try:
69
- v = int(visitas)
70
- if v >= 1_000_000_000:
71
- partes.append(f"Es uno de los juegos más visitados de Roblox con más de {v // 1_000_000_000} mil millones de visitas.")
72
- elif v >= 1_000_000:
73
- partes.append(f"Cuenta con más de {v // 1_000_000} millones de visitas en total.")
74
- elif v >= 1_000:
75
- partes.append(f"Cuenta con más de {v // 1_000}K visitas en total.")
76
- except Exception:
77
- pass
78
-
79
- return " ".join(partes)
80
-
81
- PATRONES_JUGADOR = [
82
- r"buscar\s+jugador\s+(.+)",
83
- r"busca\s+jugador\s+(.+)",
84
- r"jugador\s+de\s+roblox\s+(.+)",
85
- r"usuario\s+de\s+roblox\s+(.+)",
86
- r"perfil\s+de\s+roblox\s+(.+)",
87
- r"buscar\s+usuario\s+(.+)",
88
- r"busca\s+usuario\s+(.+)",
89
- r"quien\s+es\s+(.+)\s+en\s+roblox",
90
- r"quién\s+es\s+(.+)\s+en\s+roblox",
91
- r"info\s+de\s+(.+)\s+roblox",
92
- ]
93
-
94
- PATRONES_JUEGO = [
95
- r"buscar\s+juego\s+(.+)",
96
- r"busca\s+juego\s+(.+)",
97
- r"juego\s+de\s+roblox\s+(.+)",
98
- r"buscar\s+(.+)\s+en\s+roblox",
99
- r"busca\s+(.+)\s+en\s+roblox",
100
- r"información\s+del\s+juego\s+(.+)",
101
- r"informacion\s+del\s+juego\s+(.+)",
102
- ]
103
-
104
- def detectar_roblox(mensaje):
105
- texto = mensaje.lower().strip()
106
- for patron in PATRONES_JUGADOR:
107
- m = re.search(patron, texto)
108
- if m:
109
- return "jugador", m.group(1).strip()
110
- for patron in PATRONES_JUEGO:
111
- m = re.search(patron, texto)
112
- if m:
113
- return "juego", m.group(1).strip()
114
- return None, None
115
-
116
- def extraer_texto_content(content):
117
- if not content:
118
- return ""
119
- if isinstance(content, str):
120
- return content
121
- if isinstance(content, list):
122
- partes = []
123
- for bloque in content:
124
- if isinstance(bloque, str):
125
- partes.append(bloque)
126
- elif isinstance(bloque, dict):
127
- partes.append(str(bloque.get("text") or bloque.get("value") or bloque.get("content") or ""))
128
- return " ".join(partes)
129
- return str(content)
130
-
131
- def modo_calculadora_activo(historial):
132
- if not historial:
133
- return False
134
- for msg in reversed(historial):
135
- if msg.get("role") == "assistant":
136
- texto = extraer_texto_content(msg.get("content")).lower()
137
- return "calculadora neo-1" in texto or "aquí tienes nuestra calculadora" in texto
138
- return False
139
-
140
  def añadir_turno(historial, user_msg, bot_msg=""):
141
  return historial + [
142
  {"role": "user", "content": user_msg},
@@ -199,11 +74,9 @@ def responder(mensaje, historial):
199
  yield historial, ""
200
  datos = buscar_juego(nombre_roblox)
201
  resultado = formatear_juego(datos)
202
-
203
  if datos and "error" not in datos:
204
  explicacion = generar_explicacion_juego(datos)
205
  resultado = resultado + "\n\n💡 **¿De qué trata el juego?**\n" + explicacion
206
-
207
  historial[-1]["content"] = resultado
208
  yield historial, ""
209
  return
@@ -259,22 +132,13 @@ with gr.Blocks(title="mdfjbots-neo-1") as demo:
259
  label="Ejemplos de preguntas",
260
  )
261
 
262
- entrada.submit(
263
- fn=responder,
264
- inputs=[entrada, chatbot],
265
- outputs=[chatbot, entrada],
266
- )
267
- btn_enviar.click(
268
- fn=responder,
269
- inputs=[entrada, chatbot],
270
- outputs=[chatbot, entrada],
271
- )
272
- btn_limpiar.click(
273
- fn=lambda: ([], ""),
274
- outputs=[chatbot, entrada],
275
- )
276
 
277
  if __name__ == "__main__":
 
 
278
  port = int(os.environ.get("PORT", 5000))
279
  dev_domain = os.environ.get("REPLIT_DEV_DOMAIN", "")
280
  root_path = f"https://{dev_domain}/__neo1" if dev_domain else ""
 
1
  import os
 
 
2
  import time
3
  import gradio as gr
4
+ from neo_rest import iniciar_servidor
5
+ from logica import (
6
+ buscar_respuesta_personalizada, generar_explicacion_juego,
7
+ detectar_roblox, modo_calculadora_activo, extraer_texto_content,
8
+ )
9
  from roblox_api import buscar_jugador, buscar_juego, formatear_jugador, formatear_juego
10
  from matematicas import (
11
  es_solicitud_calculadora, es_operacion_matematica,
12
+ resolver_operacion, formatear_resultado, extraer_nombre_usuario,
13
  )
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  def añadir_turno(historial, user_msg, bot_msg=""):
16
  return historial + [
17
  {"role": "user", "content": user_msg},
 
74
  yield historial, ""
75
  datos = buscar_juego(nombre_roblox)
76
  resultado = formatear_juego(datos)
 
77
  if datos and "error" not in datos:
78
  explicacion = generar_explicacion_juego(datos)
79
  resultado = resultado + "\n\n💡 **¿De qué trata el juego?**\n" + explicacion
 
80
  historial[-1]["content"] = resultado
81
  yield historial, ""
82
  return
 
132
  label="Ejemplos de preguntas",
133
  )
134
 
135
+ entrada.submit(fn=responder, inputs=[entrada, chatbot], outputs=[chatbot, entrada])
136
+ btn_enviar.click(fn=responder, inputs=[entrada, chatbot], outputs=[chatbot, entrada])
137
+ btn_limpiar.click(fn=lambda: ([], ""), outputs=[chatbot, entrada])
 
 
 
 
 
 
 
 
 
 
 
138
 
139
  if __name__ == "__main__":
140
+ iniciar_servidor()
141
+
142
  port = int(os.environ.get("PORT", 5000))
143
  dev_domain = os.environ.get("REPLIT_DEV_DOMAIN", "")
144
  root_path = f"https://{dev_domain}/__neo1" if dev_domain else ""
chat-app/logica.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import json
4
+ from roblox_api import buscar_jugador, buscar_juego, formatear_jugador, formatear_juego
5
+ from matematicas import (
6
+ es_solicitud_calculadora, es_operacion_matematica,
7
+ resolver_operacion, formatear_resultado, extraer_nombre_usuario,
8
+ )
9
+
10
+ GENEROS_ROBLOX = {
11
+ "Town and City": "un juego de vida urbana y roleplay social",
12
+ "Adventure": "un juego de aventura y exploración",
13
+ "Role Playing": "un juego de rol donde puedes ser quien quieras",
14
+ "Comedy": "un juego de comedia y entretenimiento",
15
+ "Action": "un juego de acción y combate",
16
+ "Horror": "un juego de terror y suspenso",
17
+ "Military": "un juego de temática militar y estrategia",
18
+ "Medieval": "un juego de temática medieval con castillos y caballeros",
19
+ "Naval": "un juego de aventuras navales y piratas",
20
+ "Sci-Fi": "un juego de ciencia ficción",
21
+ "Sports": "un juego deportivo",
22
+ "Fighting": "un juego de peleas y combate",
23
+ "Western": "un juego de temática del lejano oeste",
24
+ "FPS": "un juego de disparos en primera persona",
25
+ "Building": "un juego de construcción y creatividad",
26
+ "Simulator": "un simulador",
27
+ "Tycoon": "un juego de gestión y negocios (tycoon)",
28
+ "Obby": "un juego de obstáculos (obby)",
29
+ "All Genres": "un juego variado",
30
+ }
31
+
32
+ def cargar_respuestas():
33
+ ruta = os.path.join(os.path.dirname(__file__), "respuestas.json")
34
+ try:
35
+ with open(ruta, "r", encoding="utf-8") as f:
36
+ datos = json.load(f)
37
+ return datos.get("respuestas", [])
38
+ except Exception:
39
+ return []
40
+
41
+ RESPUESTAS_PERSONALIZADAS = cargar_respuestas()
42
+
43
+ def buscar_respuesta_personalizada(mensaje):
44
+ texto = mensaje.lower().strip()
45
+ for entrada in RESPUESTAS_PERSONALIZADAS:
46
+ for pregunta in entrada.get("preguntas", []):
47
+ if pregunta.lower() in texto or texto in pregunta.lower():
48
+ return entrada.get("respuesta")
49
+ return None
50
+
51
+ def generar_explicacion_juego(datos):
52
+ nombre = datos.get("nombre", "Este juego")
53
+ tema = datos.get("tema", "")
54
+ descripcion = datos.get("descripcion", "").strip()
55
+ visitas = datos.get("visitas", 0)
56
+ creador = datos.get("creador", "")
57
+
58
+ tipo = GENEROS_ROBLOX.get(tema, f"un juego de {tema.lower()}" if tema else "un juego")
59
+ partes = [f"**{nombre}** es {tipo} de Roblox creado por **{creador}**."]
60
+
61
+ if descripcion and descripcion != "Sin descripción.":
62
+ desc_corta = descripcion[:200] + ("..." if len(descripcion) > 200 else "")
63
+ partes.append(f"Según su descripción oficial: *\"{desc_corta}\"*")
64
+
65
+ try:
66
+ v = int(visitas)
67
+ if v >= 1_000_000_000:
68
+ partes.append(f"Es uno de los juegos más visitados de Roblox con más de {v // 1_000_000_000} mil millones de visitas.")
69
+ elif v >= 1_000_000:
70
+ partes.append(f"Cuenta con más de {v // 1_000_000} millones de visitas en total.")
71
+ elif v >= 1_000:
72
+ partes.append(f"Cuenta con más de {v // 1_000}K visitas en total.")
73
+ except Exception:
74
+ pass
75
+
76
+ return " ".join(partes)
77
+
78
+ PATRONES_JUGADOR = [
79
+ r"buscar\s+jugador\s+(.+)",
80
+ r"busca\s+jugador\s+(.+)",
81
+ r"jugador\s+de\s+roblox\s+(.+)",
82
+ r"usuario\s+de\s+roblox\s+(.+)",
83
+ r"perfil\s+de\s+roblox\s+(.+)",
84
+ r"buscar\s+usuario\s+(.+)",
85
+ r"busca\s+usuario\s+(.+)",
86
+ r"quien\s+es\s+(.+)\s+en\s+roblox",
87
+ r"quién\s+es\s+(.+)\s+en\s+roblox",
88
+ r"info\s+de\s+(.+)\s+roblox",
89
+ ]
90
+
91
+ PATRONES_JUEGO = [
92
+ r"buscar\s+juego\s+(.+)",
93
+ r"busca\s+juego\s+(.+)",
94
+ r"juego\s+de\s+roblox\s+(.+)",
95
+ r"buscar\s+(.+)\s+en\s+roblox",
96
+ r"busca\s+(.+)\s+en\s+roblox",
97
+ r"información\s+del\s+juego\s+(.+)",
98
+ r"informacion\s+del\s+juego\s+(.+)",
99
+ ]
100
+
101
+ def detectar_roblox(mensaje):
102
+ texto = mensaje.lower().strip()
103
+ for patron in PATRONES_JUGADOR:
104
+ m = re.search(patron, texto)
105
+ if m:
106
+ return "jugador", m.group(1).strip()
107
+ for patron in PATRONES_JUEGO:
108
+ m = re.search(patron, texto)
109
+ if m:
110
+ return "juego", m.group(1).strip()
111
+ return None, None
112
+
113
+ def extraer_texto_content(content):
114
+ if not content:
115
+ return ""
116
+ if isinstance(content, str):
117
+ return content
118
+ if isinstance(content, list):
119
+ partes = []
120
+ for bloque in content:
121
+ if isinstance(bloque, str):
122
+ partes.append(bloque)
123
+ elif isinstance(bloque, dict):
124
+ partes.append(str(bloque.get("text") or bloque.get("value") or bloque.get("content") or ""))
125
+ return " ".join(partes)
126
+ return str(content)
127
+
128
+ def modo_calculadora_activo(historial):
129
+ if not historial:
130
+ return False
131
+ for msg in reversed(historial):
132
+ if isinstance(msg, dict) and msg.get("role") == "assistant":
133
+ texto = extraer_texto_content(msg.get("content")).lower()
134
+ return "calculadora neo-1" in texto or "aquí tienes nuestra calculadora" in texto
135
+ return False
136
+
137
+ def respuesta_final(mensaje, historial):
138
+ texto = mensaje.strip().lower()
139
+
140
+ if es_solicitud_calculadora(mensaje):
141
+ nombre = extraer_nombre_usuario(historial)
142
+ return (
143
+ f"Claro 😀, {nombre} aquí tienes nuestra calculadora:\n\n"
144
+ "🧮 **Calculadora Virtual NEO-1**\n"
145
+ "Escribe cualquier operación matemática y la resolveré al instante.\n\n"
146
+ "**Ejemplos:** `5 + 3`, `12 * 7`, `100 / 4`, `2 ** 8` (potencia)\n\n"
147
+ "_Escribe tu operación o di 'salir de calculadora' para volver._"
148
+ )
149
+
150
+ if texto in ("salir de calculadora", "salir calculadora", "cerrar calculadora", "volver al chat"):
151
+ return "De acuerdo, volviendo al chat normal. ¡Pregúntame lo que quieras! 😊"
152
+
153
+ if modo_calculadora_activo(historial) or es_operacion_matematica(mensaje):
154
+ resultado = resolver_operacion(mensaje)
155
+ if resultado is not None:
156
+ return formatear_resultado(mensaje, resultado)
157
+
158
+ tipo_roblox, nombre_roblox = detectar_roblox(mensaje)
159
+
160
+ if tipo_roblox == "jugador":
161
+ datos = buscar_jugador(nombre_roblox)
162
+ return formatear_jugador(datos)
163
+
164
+ if tipo_roblox == "juego":
165
+ datos = buscar_juego(nombre_roblox)
166
+ resultado = formatear_juego(datos)
167
+ if datos and "error" not in datos:
168
+ explicacion = generar_explicacion_juego(datos)
169
+ resultado = resultado + "\n\n💡 **¿De qué trata el juego?**\n" + explicacion
170
+ return resultado
171
+
172
+ respuesta = buscar_respuesta_personalizada(mensaje)
173
+ if respuesta:
174
+ return respuesta
175
+
176
+ return "🤖 NEO-1 aún no tiene una respuesta para eso. Puedes enseñarle más temas agregando respuestas al modelo."
chat-app/neo_rest.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import threading
3
+ from http.server import HTTPServer, BaseHTTPRequestHandler
4
+
5
+ import sys
6
+ import os
7
+ sys.path.insert(0, os.path.dirname(__file__))
8
+ from logica import respuesta_final
9
+
10
+ NEO_REST_PORT = 5001
11
+
12
+ class NeoAPIHandler(BaseHTTPRequestHandler):
13
+ def _send_json(self, code, data):
14
+ body = json.dumps(data, ensure_ascii=False).encode("utf-8")
15
+ self.send_response(code)
16
+ self.send_header("Content-Type", "application/json; charset=utf-8")
17
+ self.send_header("Content-Length", str(len(body)))
18
+ self.end_headers()
19
+ self.wfile.write(body)
20
+
21
+ def do_POST(self):
22
+ if self.path == "/chat":
23
+ try:
24
+ length = int(self.headers.get("Content-Length", 0))
25
+ raw = self.rfile.read(length)
26
+ data = json.loads(raw)
27
+
28
+ mensaje = str(data.get("message", "")).strip()
29
+ historial = data.get("history", [])
30
+
31
+ if not mensaje:
32
+ self._send_json(400, {"error": "El campo 'message' es obligatorio"})
33
+ return
34
+
35
+ respuesta = respuesta_final(mensaje, historial)
36
+ self._send_json(200, {
37
+ "response": respuesta,
38
+ "model": "mdfjbots-neo-1",
39
+ "status": "ok",
40
+ })
41
+ except json.JSONDecodeError:
42
+ self._send_json(400, {"error": "JSON inválido"})
43
+ except Exception as e:
44
+ self._send_json(500, {"error": str(e)})
45
+ else:
46
+ self._send_json(404, {"error": "Ruta no encontrada"})
47
+
48
+ def do_GET(self):
49
+ if self.path == "/health":
50
+ self._send_json(200, {"status": "ok", "model": "mdfjbots-neo-1"})
51
+ else:
52
+ self._send_json(404, {"error": "Ruta no encontrada"})
53
+
54
+ def log_message(self, format, *args):
55
+ pass
56
+
57
+ def iniciar_servidor():
58
+ server = HTTPServer(("0.0.0.0", NEO_REST_PORT), NeoAPIHandler)
59
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
60
+ thread.start()
61
+ print(f"[NeoAPI REST] Corriendo en http://0.0.0.0:{NEO_REST_PORT}")
62
+ return server
neo-api-client.js ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * NeoAPI Client — mdfjbots-neo-1
3
+ * Úsalo en tu página web para conectarte con la NeoAPI.
4
+ *
5
+ * Uso básico:
6
+ * const neo = new NeoAPI("neo1-tu_api_key_aqui");
7
+ * const respuesta = await neo.chat("Hola, ¿quién eres?");
8
+ * console.log(respuesta);
9
+ */
10
+
11
+ class NeoAPI {
12
+ /**
13
+ * @param {string} apiKey - Tu API key (formato: neo1-...)
14
+ * @param {string} [baseUrl] - URL base de la NeoAPI (opcional, por defecto la URL de producción)
15
+ */
16
+ constructor(apiKey, baseUrl = "https://TU_DOMINIO_AQUI/api") {
17
+ if (!apiKey) throw new Error("NeoAPI: se requiere una API key.");
18
+ this.apiKey = apiKey;
19
+ this.baseUrl = baseUrl.replace(/\/$/, "");
20
+ this.history = [];
21
+ }
22
+
23
+ /**
24
+ * Envía un mensaje al modelo NEO-1 y obtiene la respuesta.
25
+ * El historial de conversación se mantiene automáticamente.
26
+ *
27
+ * @param {string} message - El mensaje del usuario
28
+ * @returns {Promise<string>} - La respuesta del modelo
29
+ */
30
+ async chat(message) {
31
+ const response = await fetch(`${this.baseUrl}/neo/chat`, {
32
+ method: "POST",
33
+ headers: {
34
+ "Content-Type": "application/json",
35
+ Authorization: `Bearer ${this.apiKey}`,
36
+ },
37
+ body: JSON.stringify({
38
+ message,
39
+ history: this.history,
40
+ }),
41
+ });
42
+
43
+ if (!response.ok) {
44
+ const err = await response.json().catch(() => ({ error: response.statusText }));
45
+ throw new Error(`NeoAPI error ${response.status}: ${err.error ?? response.statusText}`);
46
+ }
47
+
48
+ const data = await response.json();
49
+ const botReply = data.response;
50
+
51
+ this.history.push(
52
+ { role: "user", content: message },
53
+ { role: "assistant", content: botReply }
54
+ );
55
+
56
+ return botReply;
57
+ }
58
+
59
+ /**
60
+ * Envía un mensaje sin mantener historial (cada llamada es independiente).
61
+ *
62
+ * @param {string} message - El mensaje del usuario
63
+ * @param {Array} [history] - Historial opcional a enviar
64
+ * @returns {Promise<string>}
65
+ */
66
+ async chatOnce(message, history = []) {
67
+ const response = await fetch(`${this.baseUrl}/neo/chat`, {
68
+ method: "POST",
69
+ headers: {
70
+ "Content-Type": "application/json",
71
+ Authorization: `Bearer ${this.apiKey}`,
72
+ },
73
+ body: JSON.stringify({ message, history }),
74
+ });
75
+
76
+ if (!response.ok) {
77
+ const err = await response.json().catch(() => ({ error: response.statusText }));
78
+ throw new Error(`NeoAPI error ${response.status}: ${err.error ?? response.statusText}`);
79
+ }
80
+
81
+ const data = await response.json();
82
+ return data.response;
83
+ }
84
+
85
+ /** Limpia el historial de conversación. */
86
+ clearHistory() {
87
+ this.history = [];
88
+ }
89
+
90
+ /** Obtiene los modelos disponibles. */
91
+ async getModels() {
92
+ const response = await fetch(`${this.baseUrl}/neo/models`, {
93
+ headers: { Authorization: `Bearer ${this.apiKey}` },
94
+ });
95
+ if (!response.ok) throw new Error("NeoAPI: error al obtener modelos.");
96
+ return response.json();
97
+ }
98
+ }
99
+
100
+ /**
101
+ * ─────────────────────────────────────────────
102
+ * EJEMPLOS DE USO
103
+ * ─────────────────────────────────────────────
104
+ *
105
+ * 1) Conversación simple:
106
+ *
107
+ * const neo = new NeoAPI("neo1-tu_api_key");
108
+ * const res = await neo.chat("¿Qué es Linux?");
109
+ * document.getElementById("respuesta").textContent = res;
110
+ *
111
+ *
112
+ * 2) Chatbot con historial en la página:
113
+ *
114
+ * const neo = new NeoAPI("neo1-tu_api_key");
115
+ *
116
+ * async function enviar() {
117
+ * const input = document.getElementById("input").value;
118
+ * const res = await neo.chat(input);
119
+ * mostrarMensaje("NEO-1", res);
120
+ * }
121
+ *
122
+ *
123
+ * 3) Llamada directa sin historial (para búsquedas puntuales):
124
+ *
125
+ * const neo = new NeoAPI("neo1-tu_api_key");
126
+ * const res = await neo.chatOnce("buscar juego Adopt Me");
127
+ * console.log(res);
128
+ *
129
+ *
130
+ * ─────────────────────────────────────────────
131
+ * ENDPOINTS DE ADMIN (para gestionar API keys)
132
+ * Se hacen desde tu backend, nunca desde el frontend.
133
+ * ─────────────────────────────────────────────
134
+ *
135
+ * Crear una nueva key:
136
+ * POST /api/keys
137
+ * Header: x-admin-secret: TU_ADMIN_SECRET
138
+ * Body: { "name": "mi-app-web" }
139
+ *
140
+ * Listar keys:
141
+ * GET /api/keys
142
+ * Header: x-admin-secret: TU_ADMIN_SECRET
143
+ *
144
+ * Revocar una key:
145
+ * DELETE /api/keys/mi-app-web
146
+ * Header: x-admin-secret: TU_ADMIN_SECRET
147
+ */
148
+
149
+ export default NeoAPI;