zerochat 7.10.0__tar.gz → 7.12.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {zerochat-7.10.0/zerochat.egg-info → zerochat-7.12.0}/PKG-INFO +1 -1
- {zerochat-7.10.0 → zerochat-7.12.0}/pyproject.toml +1 -1
- {zerochat-7.10.0 → zerochat-7.12.0/zerochat.egg-info}/PKG-INFO +1 -1
- {zerochat-7.10.0 → zerochat-7.12.0}/zerochat.py +220 -32
- {zerochat-7.10.0 → zerochat-7.12.0}/LICENSE +0 -0
- {zerochat-7.10.0 → zerochat-7.12.0}/README.md +0 -0
- {zerochat-7.10.0 → zerochat-7.12.0}/setup.cfg +0 -0
- {zerochat-7.10.0 → zerochat-7.12.0}/zerochat.egg-info/SOURCES.txt +0 -0
- {zerochat-7.10.0 → zerochat-7.12.0}/zerochat.egg-info/dependency_links.txt +0 -0
- {zerochat-7.10.0 → zerochat-7.12.0}/zerochat.egg-info/entry_points.txt +0 -0
- {zerochat-7.10.0 → zerochat-7.12.0}/zerochat.egg-info/top_level.txt +0 -0
|
@@ -14,8 +14,10 @@ from __future__ import annotations
|
|
|
14
14
|
import argparse
|
|
15
15
|
import ast
|
|
16
16
|
import atexit
|
|
17
|
+
import base64
|
|
17
18
|
import datetime
|
|
18
19
|
import fnmatch
|
|
20
|
+
import hashlib
|
|
19
21
|
import hmac
|
|
20
22
|
import importlib.metadata
|
|
21
23
|
import json
|
|
@@ -40,7 +42,7 @@ import webbrowser
|
|
|
40
42
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
41
43
|
from pathlib import Path
|
|
42
44
|
|
|
43
|
-
SOURCE_BACKEND_VERSION = "7.
|
|
45
|
+
SOURCE_BACKEND_VERSION = "7.12.0"
|
|
44
46
|
|
|
45
47
|
def _read_source_version(filename: str) -> str | None:
|
|
46
48
|
"""Lee la versión de un archivo del repositorio cuando se ejecuta desde fuentes."""
|
|
@@ -96,7 +98,8 @@ PYPI_VERSION_URL = "https://pypi.org/pypi/zerochat/json"
|
|
|
96
98
|
REMOTE_SCRIPT_URL = "https://raw.githubusercontent.com/albalday/zerochat/master/zerochat.py"
|
|
97
99
|
CONSOLE_STATUS_IDLE_SECONDS = 8.0
|
|
98
100
|
CONSOLE_CONTROL = None
|
|
99
|
-
|
|
101
|
+
NOTICES: list[str] = []
|
|
102
|
+
NOTICES_LOCK = threading.Lock()
|
|
100
103
|
|
|
101
104
|
def format_uptime(seconds: float) -> str:
|
|
102
105
|
"""Devuelve una duración breve y estable para la línea de estado de consola."""
|
|
@@ -108,9 +111,10 @@ def format_uptime(seconds: float) -> str:
|
|
|
108
111
|
|
|
109
112
|
class ConsoleControl:
|
|
110
113
|
"""Atajos de consola y línea de estado, solo para terminales interactivos."""
|
|
111
|
-
def __init__(self, server: ThreadingHTTPServer, parser: argparse.ArgumentParser):
|
|
114
|
+
def __init__(self, server: ThreadingHTTPServer, parser: argparse.ArgumentParser, target_url: str | None = None):
|
|
112
115
|
self.server = server
|
|
113
116
|
self.parser = parser
|
|
117
|
+
self.target_url = target_url
|
|
114
118
|
self.started_at = time.monotonic()
|
|
115
119
|
self.last_activity = self.started_at
|
|
116
120
|
self.stop_event = threading.Event()
|
|
@@ -190,10 +194,27 @@ class ConsoleControl:
|
|
|
190
194
|
if self.status_visible:
|
|
191
195
|
sys.stdout.write("\r\033[2K")
|
|
192
196
|
self.status_visible = False
|
|
193
|
-
|
|
197
|
+
commands = "[h] ayuda · [n] Navegador"
|
|
198
|
+
if get_notices():
|
|
199
|
+
commands += " · [i] información"
|
|
200
|
+
print(f"\nComandos de consola: {commands} · [x] salir ordenadamente\n", flush=True)
|
|
194
201
|
print(self.parser.format_help().rstrip(), flush=True)
|
|
195
202
|
self.last_activity = time.monotonic()
|
|
196
203
|
|
|
204
|
+
def show_notices(self):
|
|
205
|
+
notices = get_notices()
|
|
206
|
+
if not notices:
|
|
207
|
+
return
|
|
208
|
+
with self.lock:
|
|
209
|
+
if self.status_visible:
|
|
210
|
+
sys.stdout.write("\r\033[2K")
|
|
211
|
+
self.status_visible = False
|
|
212
|
+
print("\nInformación:", flush=True)
|
|
213
|
+
for notice in notices:
|
|
214
|
+
print(f"- {notice}", flush=True)
|
|
215
|
+
print(flush=True)
|
|
216
|
+
self.last_activity = time.monotonic()
|
|
217
|
+
|
|
197
218
|
def _render_status(self):
|
|
198
219
|
if not self.enabled:
|
|
199
220
|
return
|
|
@@ -201,7 +222,10 @@ class ConsoleControl:
|
|
|
201
222
|
with self.lock:
|
|
202
223
|
if time.monotonic() - self.last_activity < CONSOLE_STATUS_IDLE_SECONDS:
|
|
203
224
|
return
|
|
204
|
-
|
|
225
|
+
commands = "[h] ayuda · [n] Navegador"
|
|
226
|
+
if get_notices():
|
|
227
|
+
commands += " · [i] información"
|
|
228
|
+
sys.stdout.write(f"\r\033[2KZeroChat activo {uptime} · {commands} · [x] salir")
|
|
205
229
|
sys.stdout.flush()
|
|
206
230
|
self.status_visible = True
|
|
207
231
|
|
|
@@ -212,6 +236,11 @@ class ConsoleControl:
|
|
|
212
236
|
def _handle_key(self, key: str):
|
|
213
237
|
if key.lower() == "h":
|
|
214
238
|
self.show_help()
|
|
239
|
+
elif key.lower() == "n":
|
|
240
|
+
if self.target_url:
|
|
241
|
+
launch_browser(self.target_url)
|
|
242
|
+
elif key.lower() == "i":
|
|
243
|
+
self.show_notices()
|
|
215
244
|
elif key.lower() == "x":
|
|
216
245
|
self.log(f"[{time.strftime('%H:%M:%S')}] Deteniendo servidor ZeroChat...")
|
|
217
246
|
stop_zerochat_server(self.server)
|
|
@@ -278,6 +307,26 @@ def get_venv_dir() -> Path:
|
|
|
278
307
|
return get_data_dir() / ".venv"
|
|
279
308
|
|
|
280
309
|
|
|
310
|
+
def reset_notices():
|
|
311
|
+
"""Reinicia los avisos transitorios de la ejecución actual."""
|
|
312
|
+
with NOTICES_LOCK:
|
|
313
|
+
NOTICES.clear()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def add_notice(message: str):
|
|
317
|
+
"""Añade un aviso para la consola interactiva de la ejecución actual."""
|
|
318
|
+
if not isinstance(message, str) or not message.strip():
|
|
319
|
+
raise ValueError("El aviso debe ser texto no vacío")
|
|
320
|
+
with NOTICES_LOCK:
|
|
321
|
+
NOTICES.append(message.strip())
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def get_notices() -> tuple[str, ...]:
|
|
325
|
+
"""Devuelve una instantánea inmutable de los avisos actuales."""
|
|
326
|
+
with NOTICES_LOCK:
|
|
327
|
+
return tuple(NOTICES)
|
|
328
|
+
|
|
329
|
+
|
|
281
330
|
def get_daily_token() -> str:
|
|
282
331
|
"""Devuelve un token de sesión diario persistido en ~/zerochat/config/token.json."""
|
|
283
332
|
config_dir = get_data_dir() / "config"
|
|
@@ -306,6 +355,16 @@ SESSION_TOKEN = get_daily_token()
|
|
|
306
355
|
ACTIVE_PORT = DEFAULT_PORT
|
|
307
356
|
ACTIVE_HOST = DEFAULT_HOST
|
|
308
357
|
|
|
358
|
+
# Kept only for this Python process: binds an approved browser tool call to
|
|
359
|
+
# the exact JSON-RPC bytes sent to the local host.
|
|
360
|
+
TOOL_AUTH_VERSION = "zerochat-tool-auth-v1"
|
|
361
|
+
TOOL_AUTH_TTL_MS = 30_000
|
|
362
|
+
TOOL_AUTH_MAX_NONCES = 10_000
|
|
363
|
+
TOOL_AUTH_KEY = secrets.token_bytes(32)
|
|
364
|
+
TOOL_AUTH_SESSION_ID = secrets.token_urlsafe(18)
|
|
365
|
+
TOOL_AUTH_NONCES: dict[str, int] = {}
|
|
366
|
+
TOOL_AUTH_NONCES_LOCK = threading.Lock()
|
|
367
|
+
|
|
309
368
|
DETECTED_OS = "windows" if sys.platform.startswith("win") else ("android" if "ANDROID_ROOT" in os.environ else "linux")
|
|
310
369
|
|
|
311
370
|
|
|
@@ -377,16 +436,15 @@ def check_version():
|
|
|
377
436
|
installed = is_installed_runtime()
|
|
378
437
|
remote_ver = _read_remote_version(PYPI_VERSION_URL if installed else REMOTE_VERSION_URL)
|
|
379
438
|
if remote_ver and re.match(r"^\d+(\.\d+)+", remote_ver) and has_new_backend_version(remote_ver):
|
|
380
|
-
|
|
439
|
+
notice = f"Nueva versión del servidor disponible (local: {VERSION}, remota: {compatibility_version(remote_ver)})."
|
|
381
440
|
if installed:
|
|
382
|
-
|
|
441
|
+
notice += f" Actualiza cuando quieras con: {sys.executable} -m pip install --upgrade --no-cache-dir zerochat"
|
|
383
442
|
else:
|
|
384
|
-
|
|
443
|
+
notice += f" Actualiza con: curl -sSL {REMOTE_SCRIPT_URL} -o zerochat.py"
|
|
444
|
+
add_notice(notice)
|
|
385
445
|
except Exception:
|
|
386
446
|
# Modo offline o timeout ignorado de forma segura
|
|
387
447
|
pass
|
|
388
|
-
|
|
389
|
-
|
|
390
448
|
# ==============================================================================
|
|
391
449
|
# Herramientas Locales Core
|
|
392
450
|
# ==============================================================================
|
|
@@ -1159,6 +1217,38 @@ try {
|
|
|
1159
1217
|
_BROWSER_SESSION = PersistentBrowserSession()
|
|
1160
1218
|
|
|
1161
1219
|
|
|
1220
|
+
def browser_action_availability() -> dict:
|
|
1221
|
+
"""Comprueba requisitos locales sin lanzar Node.js ni Chromium."""
|
|
1222
|
+
if not shutil.which("node"):
|
|
1223
|
+
return {"available": False, "error": "Node.js is not installed or not found in system PATH."}
|
|
1224
|
+
playwright_locations = (
|
|
1225
|
+
Path.cwd() / "node_modules" / "playwright",
|
|
1226
|
+
get_data_dir() / "services" / "playwright" / "node_modules" / "playwright"
|
|
1227
|
+
)
|
|
1228
|
+
if not any(location.is_dir() for location in playwright_locations):
|
|
1229
|
+
return {"available": False, "error": "Playwright is not available."}
|
|
1230
|
+
browser_commands = (
|
|
1231
|
+
"google-chrome", "google-chrome-stable", "chromium", "chromium-browser",
|
|
1232
|
+
"msedge", "msedge.exe", "chrome", "chrome.exe"
|
|
1233
|
+
)
|
|
1234
|
+
if sys.platform == "darwin":
|
|
1235
|
+
playwright_cache = Path.home() / "Library" / "Caches" / "ms-playwright"
|
|
1236
|
+
elif sys.platform.startswith("win"):
|
|
1237
|
+
playwright_cache = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local")) / "ms-playwright"
|
|
1238
|
+
else:
|
|
1239
|
+
playwright_cache = Path.home() / ".cache" / "ms-playwright"
|
|
1240
|
+
try:
|
|
1241
|
+
has_playwright_browser = playwright_cache.is_dir() and any(
|
|
1242
|
+
child.is_dir() and child.name.startswith("chromium")
|
|
1243
|
+
for child in playwright_cache.iterdir()
|
|
1244
|
+
)
|
|
1245
|
+
except OSError:
|
|
1246
|
+
has_playwright_browser = False
|
|
1247
|
+
if has_playwright_browser or any(shutil.which(command) for command in browser_commands):
|
|
1248
|
+
return {"available": True}
|
|
1249
|
+
return {"available": False, "error": "Playwright Chromium is not installed."}
|
|
1250
|
+
|
|
1251
|
+
|
|
1162
1252
|
def browser_action(action: str, url: str | None = None, selector: str | None = None, value: str | None = None) -> str:
|
|
1163
1253
|
"""Control a headless browser for UI testing and visual inspection."""
|
|
1164
1254
|
try:
|
|
@@ -1317,7 +1407,6 @@ LOCAL_TOOL_HANDLERS = {
|
|
|
1317
1407
|
"get_diagnostics": get_diagnostics,
|
|
1318
1408
|
"browser_action": browser_action
|
|
1319
1409
|
}
|
|
1320
|
-
|
|
1321
1410
|
# ==============================================================================
|
|
1322
1411
|
# Servidor HTTP JSON-RPC 2.0 y SSE con Autenticación por Token
|
|
1323
1412
|
# ==============================================================================
|
|
@@ -2198,7 +2287,7 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
2198
2287
|
return
|
|
2199
2288
|
self.send_header("Access-Control-Allow-Origin", origin if origin else "*")
|
|
2200
2289
|
self.send_header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
|
|
2201
|
-
self.send_header("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization, X-ZeroChat-Token, X-ZeroChat-Client")
|
|
2290
|
+
self.send_header("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization, X-ZeroChat-Token, X-ZeroChat-Client, X-ZeroChat-Tool-Session, X-ZeroChat-Tool-Expires, X-ZeroChat-Tool-Nonce, X-ZeroChat-Tool-Signature")
|
|
2202
2291
|
self.send_header("Access-Control-Allow-Private-Network", "true")
|
|
2203
2292
|
|
|
2204
2293
|
def verify_token(self) -> bool:
|
|
@@ -2217,6 +2306,42 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
2217
2306
|
mark_browser_active()
|
|
2218
2307
|
return is_valid
|
|
2219
2308
|
|
|
2309
|
+
def verify_tool_authorization(self, body: bytes, request_path: str) -> str | None:
|
|
2310
|
+
"""Validate and atomically consume an authorization for one tool call."""
|
|
2311
|
+
session_id = self.headers.get("X-ZeroChat-Tool-Session", "")
|
|
2312
|
+
expires_raw = self.headers.get("X-ZeroChat-Tool-Expires", "")
|
|
2313
|
+
nonce = self.headers.get("X-ZeroChat-Tool-Nonce", "")
|
|
2314
|
+
signature = self.headers.get("X-ZeroChat-Tool-Signature", "")
|
|
2315
|
+
if (not isinstance(session_id, str) or len(session_id) > 128 or
|
|
2316
|
+
not isinstance(nonce, str) or not re.fullmatch(r"[A-Za-z0-9_-]{22,128}", nonce) or
|
|
2317
|
+
not isinstance(signature, str) or not re.fullmatch(r"[A-Za-z0-9_-]{43,128}", signature)):
|
|
2318
|
+
return "Missing or invalid tool authorization"
|
|
2319
|
+
try:
|
|
2320
|
+
expires_at = int(expires_raw)
|
|
2321
|
+
except (TypeError, ValueError):
|
|
2322
|
+
return "Missing or invalid tool authorization"
|
|
2323
|
+
now_ms = int(time.time() * 1000)
|
|
2324
|
+
if session_id != TOOL_AUTH_SESSION_ID or expires_at < now_ms or expires_at > now_ms + TOOL_AUTH_TTL_MS:
|
|
2325
|
+
return "Expired or invalid tool authorization"
|
|
2326
|
+
body_hash = hashlib.sha256(body).hexdigest()
|
|
2327
|
+
signature_base = json.dumps(
|
|
2328
|
+
[TOOL_AUTH_VERSION, session_id, "POST", request_path or "/", expires_at, nonce, body_hash],
|
|
2329
|
+
separators=(",", ":"), ensure_ascii=True
|
|
2330
|
+
).encode("utf-8")
|
|
2331
|
+
expected = base64.urlsafe_b64encode(hmac.new(TOOL_AUTH_KEY, signature_base, hashlib.sha256).digest()).rstrip(b"=").decode("ascii")
|
|
2332
|
+
if not hmac.compare_digest(signature, expected):
|
|
2333
|
+
return "Invalid tool authorization signature"
|
|
2334
|
+
with TOOL_AUTH_NONCES_LOCK:
|
|
2335
|
+
for value, expiry in list(TOOL_AUTH_NONCES.items()):
|
|
2336
|
+
if expiry < now_ms:
|
|
2337
|
+
del TOOL_AUTH_NONCES[value]
|
|
2338
|
+
if nonce in TOOL_AUTH_NONCES:
|
|
2339
|
+
return "Tool authorization was already used"
|
|
2340
|
+
if len(TOOL_AUTH_NONCES) >= TOOL_AUTH_MAX_NONCES:
|
|
2341
|
+
return "Tool authorization cache is full"
|
|
2342
|
+
TOOL_AUTH_NONCES[nonce] = expires_at
|
|
2343
|
+
return None
|
|
2344
|
+
|
|
2220
2345
|
def do_OPTIONS(self):
|
|
2221
2346
|
t0 = time.monotonic()
|
|
2222
2347
|
safe_path = sanitize_log_path(self.path)
|
|
@@ -2266,6 +2391,13 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
2266
2391
|
self._log_res(200, safe_path, (time.monotonic() - t0) * 1000)
|
|
2267
2392
|
return
|
|
2268
2393
|
|
|
2394
|
+
# Browsers request this optional public resource without API credentials.
|
|
2395
|
+
if path_clean == "/favicon.ico":
|
|
2396
|
+
self.send_response(204)
|
|
2397
|
+
self.send_header("Content-Length", "0")
|
|
2398
|
+
self.end_headers()
|
|
2399
|
+
return
|
|
2400
|
+
|
|
2269
2401
|
if not is_heartbeat:
|
|
2270
2402
|
self._log_req("GET", safe_path)
|
|
2271
2403
|
|
|
@@ -2299,12 +2431,13 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
2299
2431
|
self._log_res(200, f"{safe_path} [SSE canal activo]", (time.monotonic() - t0) * 1000)
|
|
2300
2432
|
return
|
|
2301
2433
|
|
|
2302
|
-
# Status general
|
|
2303
2434
|
res_data = json.dumps({
|
|
2304
2435
|
"status": "active",
|
|
2305
2436
|
"server": "ZeroChat Local Server",
|
|
2306
2437
|
"version": VERSION,
|
|
2438
|
+
"cwd": str(Path.cwd().resolve()),
|
|
2307
2439
|
"tools_count": len(LOCAL_TOOLS_DEFINITIONS),
|
|
2440
|
+
"browser_action": browser_action_availability(),
|
|
2308
2441
|
"os": DETECTED_OS
|
|
2309
2442
|
}, ensure_ascii=False, indent=2).encode("utf-8")
|
|
2310
2443
|
|
|
@@ -2427,6 +2560,11 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
2427
2560
|
self._send_json_response(400, {"error": tool_args_error})
|
|
2428
2561
|
self._log_res(400, safe_path, (time.monotonic() - t0) * 1000, tool_args_error)
|
|
2429
2562
|
return
|
|
2563
|
+
auth_error = self.verify_tool_authorization(post_data, self.path.split("?", 1)[0].rstrip("/") or "/")
|
|
2564
|
+
if auth_error:
|
|
2565
|
+
self._send_json_response(403, {"error": auth_error})
|
|
2566
|
+
self._log_res(403, safe_path, (time.monotonic() - t0) * 1000, auth_error)
|
|
2567
|
+
return
|
|
2430
2568
|
|
|
2431
2569
|
req_path = self.path.split("?", 1)[0].rstrip("/")
|
|
2432
2570
|
|
|
@@ -2465,6 +2603,12 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
2465
2603
|
},
|
|
2466
2604
|
"capabilities": {
|
|
2467
2605
|
"tools": {"listChanged": True}
|
|
2606
|
+
},
|
|
2607
|
+
"toolAuthorization": {
|
|
2608
|
+
"version": TOOL_AUTH_VERSION,
|
|
2609
|
+
"sessionId": TOOL_AUTH_SESSION_ID,
|
|
2610
|
+
"key": base64.urlsafe_b64encode(TOOL_AUTH_KEY).rstrip(b"=").decode("ascii"),
|
|
2611
|
+
"ttlMs": TOOL_AUTH_TTL_MS
|
|
2468
2612
|
}
|
|
2469
2613
|
}
|
|
2470
2614
|
elif method == "tools/list":
|
|
@@ -2494,14 +2638,34 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
2494
2638
|
"protocolVersion": "2024-11-05",
|
|
2495
2639
|
"serverInfo": {
|
|
2496
2640
|
"name": "ZeroChat Local Server",
|
|
2497
|
-
"version": VERSION
|
|
2641
|
+
"version": VERSION,
|
|
2642
|
+
"cwd": str(Path.cwd().resolve())
|
|
2498
2643
|
},
|
|
2499
2644
|
"capabilities": {
|
|
2500
2645
|
"tools": {"listChanged": True}
|
|
2646
|
+
},
|
|
2647
|
+
"toolAuthorization": {
|
|
2648
|
+
"version": TOOL_AUTH_VERSION,
|
|
2649
|
+
"sessionId": TOOL_AUTH_SESSION_ID,
|
|
2650
|
+
"key": base64.urlsafe_b64encode(TOOL_AUTH_KEY).rstrip(b"=").decode("ascii"),
|
|
2651
|
+
"ttlMs": TOOL_AUTH_TTL_MS
|
|
2501
2652
|
}
|
|
2502
2653
|
}
|
|
2503
2654
|
elif method == "tools/list":
|
|
2504
|
-
|
|
2655
|
+
browser_availability = browser_action_availability()
|
|
2656
|
+
tools = []
|
|
2657
|
+
for definition in LOCAL_TOOLS_DEFINITIONS:
|
|
2658
|
+
item = dict(definition)
|
|
2659
|
+
if item.get("name") == "browser_action":
|
|
2660
|
+
item["availability"] = browser_availability
|
|
2661
|
+
tools.append(item)
|
|
2662
|
+
result = {"tools": tools}
|
|
2663
|
+
elif method == "tools/availability":
|
|
2664
|
+
requested_name = params.get("name", "") if isinstance(params, dict) else ""
|
|
2665
|
+
if requested_name != "browser_action":
|
|
2666
|
+
error = {"code": -32602, "message": "Herramienta no compatible con comprobación de disponibilidad."}
|
|
2667
|
+
else:
|
|
2668
|
+
result = browser_action_availability()
|
|
2505
2669
|
elif method == "tools/call":
|
|
2506
2670
|
tool_name = params.get("name", "") if isinstance(params, dict) else ""
|
|
2507
2671
|
tool_args = params.get("arguments", {}) if isinstance(params, dict) else {}
|
|
@@ -2591,7 +2755,6 @@ class ZeroChatServerHandler(BaseHTTPRequestHandler):
|
|
|
2591
2755
|
def log_message(self, format, *args):
|
|
2592
2756
|
# Silenciar logs ruidosos por defecto
|
|
2593
2757
|
pass
|
|
2594
|
-
|
|
2595
2758
|
def is_termux_environment() -> bool:
|
|
2596
2759
|
"""Devuelve si el proceso se ejecuta dentro de la instalación de Termux."""
|
|
2597
2760
|
termux_prefix = "/data/data/com.termux/files/usr"
|
|
@@ -2720,12 +2883,35 @@ def open_browser(url: str) -> bool:
|
|
|
2720
2883
|
)
|
|
2721
2884
|
|
|
2722
2885
|
|
|
2886
|
+
def launch_browser(url: str) -> bool:
|
|
2887
|
+
"""Abre el navegador en la URL de la sesión y muestra información o diagnóstico en consola."""
|
|
2888
|
+
termux_detected = is_termux_environment()
|
|
2889
|
+
if termux_detected:
|
|
2890
|
+
console_log(f"[{time.strftime('%H:%M:%S')}] Termux detectado; se abrirá mediante termux-open-url.", flush=True)
|
|
2891
|
+
console_log(f"[{time.strftime('%H:%M:%S')}] Abriendo navegador en la interfaz configurada...", flush=True)
|
|
2892
|
+
try:
|
|
2893
|
+
if not open_browser(url):
|
|
2894
|
+
raise RuntimeError("El lanzador de navegador devolvió un resultado sin éxito.")
|
|
2895
|
+
return True
|
|
2896
|
+
except Exception as e:
|
|
2897
|
+
console_log(f"[{time.strftime('%H:%M:%S')}] No se pudo abrir el navegador automáticamente: {e}", flush=True)
|
|
2898
|
+
console_log(" Traza de diagnóstico:", flush=True)
|
|
2899
|
+
traceback.print_exc()
|
|
2900
|
+
manual_command = get_manual_browser_command(url)
|
|
2901
|
+
if manual_command:
|
|
2902
|
+
console_log(" Termux detectado. Prueba este comando exacto:", flush=True)
|
|
2903
|
+
console_log(f" {manual_command}", flush=True)
|
|
2904
|
+
return False
|
|
2905
|
+
|
|
2906
|
+
|
|
2907
|
+
|
|
2723
2908
|
# ==============================================================================
|
|
2724
2909
|
# Punto de Entrada Principal (CLI)
|
|
2725
2910
|
# ==============================================================================
|
|
2726
2911
|
|
|
2727
2912
|
def main():
|
|
2728
2913
|
global ACTIVE_PORT, ACTIVE_HOST, SESSION_TOKEN, CONSOLE_CONTROL
|
|
2914
|
+
global TOOL_AUTH_KEY, TOOL_AUTH_SESSION_ID, TOOL_AUTH_NONCES
|
|
2729
2915
|
|
|
2730
2916
|
parser = argparse.ArgumentParser(description=f"ZeroChat Local Server v{VERSION}")
|
|
2731
2917
|
parser.add_argument("--port", type=int, default=int(os.environ.get("ZEROCHAT_PORT", DEFAULT_PORT)), help=f"Puerto de escucha (default: {DEFAULT_PORT})")
|
|
@@ -2739,6 +2925,9 @@ def main():
|
|
|
2739
2925
|
parser.add_argument("--version", action="version", version=f"ZeroChat {VERSION}")
|
|
2740
2926
|
args = parser.parse_args()
|
|
2741
2927
|
|
|
2928
|
+
# Los avisos no persisten entre ejecuciones del servidor.
|
|
2929
|
+
reset_notices()
|
|
2930
|
+
|
|
2742
2931
|
if args.test:
|
|
2743
2932
|
print(f"[{time.strftime('%H:%M:%S')}] TEST list_directory {'ok' if json.loads(list_directory('.'))['success'] else 'error'}")
|
|
2744
2933
|
print(f"[{time.strftime('%H:%M:%S')}] TEST read_file {'ok' if json.loads(read_file('package.json', max_lines=5))['success'] else 'error'}")
|
|
@@ -2750,6 +2939,14 @@ def main():
|
|
|
2750
2939
|
if not args.no_venv:
|
|
2751
2940
|
ensure_virtual_environment()
|
|
2752
2941
|
|
|
2942
|
+
browser_availability = browser_action_availability()
|
|
2943
|
+
if not browser_availability.get("available"):
|
|
2944
|
+
add_notice(
|
|
2945
|
+
"browser_action no está disponible y permanecerá desactivada. "
|
|
2946
|
+
"Instala Node.js, Playwright y Chromium: "
|
|
2947
|
+
"https://albalday.github.io/zerochat/help/browser-action.html"
|
|
2948
|
+
)
|
|
2949
|
+
|
|
2753
2950
|
# 2. Detectar entorno de desarrollo y resolver URL de destino
|
|
2754
2951
|
dev_root = get_dev_root()
|
|
2755
2952
|
static_root = get_static_root()
|
|
@@ -2766,6 +2963,11 @@ def main():
|
|
|
2766
2963
|
SESSION_TOKEN = args.token
|
|
2767
2964
|
else:
|
|
2768
2965
|
SESSION_TOKEN = get_daily_token()
|
|
2966
|
+
# A restart deliberately invalidates every browser-side signing credential.
|
|
2967
|
+
TOOL_AUTH_KEY = secrets.token_bytes(32)
|
|
2968
|
+
TOOL_AUTH_SESSION_ID = secrets.token_urlsafe(18)
|
|
2969
|
+
with TOOL_AUTH_NONCES_LOCK:
|
|
2970
|
+
TOOL_AUTH_NONCES = {}
|
|
2769
2971
|
|
|
2770
2972
|
server = ThreadingHTTPServer((ACTIVE_HOST, ACTIVE_PORT), ZeroChatServerHandler)
|
|
2771
2973
|
|
|
@@ -2800,25 +3002,11 @@ def main():
|
|
|
2800
3002
|
print(f" Auto-cierre : Desactivado")
|
|
2801
3003
|
print("=" * 64, flush=True)
|
|
2802
3004
|
|
|
2803
|
-
CONSOLE_CONTROL = ConsoleControl(server, parser)
|
|
3005
|
+
CONSOLE_CONTROL = ConsoleControl(server, parser, target_url=target_url)
|
|
2804
3006
|
CONSOLE_CONTROL.start()
|
|
2805
3007
|
|
|
2806
3008
|
if not args.no_browser:
|
|
2807
|
-
|
|
2808
|
-
if termux_detected:
|
|
2809
|
-
console_log(f"[{time.strftime('%H:%M:%S')}] Termux detectado; se abrirá mediante termux-open-url.", flush=True)
|
|
2810
|
-
console_log(f"[{time.strftime('%H:%M:%S')}] Abriendo navegador en la interfaz configurada...", flush=True)
|
|
2811
|
-
try:
|
|
2812
|
-
if not open_browser(target_url):
|
|
2813
|
-
raise RuntimeError("El lanzador de navegador devolvió un resultado sin éxito.")
|
|
2814
|
-
except Exception as e:
|
|
2815
|
-
console_log(f"[{time.strftime('%H:%M:%S')}] No se pudo abrir el navegador automáticamente: {e}", flush=True)
|
|
2816
|
-
console_log(" Traza de diagnóstico:", flush=True)
|
|
2817
|
-
traceback.print_exc()
|
|
2818
|
-
manual_command = get_manual_browser_command(target_url)
|
|
2819
|
-
if manual_command:
|
|
2820
|
-
console_log(" Termux detectado. Prueba este comando exacto:", flush=True)
|
|
2821
|
-
console_log(f" {manual_command}", flush=True)
|
|
3009
|
+
launch_browser(target_url)
|
|
2822
3010
|
|
|
2823
3011
|
if exit_on_close:
|
|
2824
3012
|
require_initial = not args.no_browser
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|