feat(servidor): adiciona SSE, token, E-Stop real e trava de hardware
- comum.py: ler_json (Origin/JSON/tamanho), gravar_atomico e token (CARINHA_TOKEN ou seguranca.json), substituindo 6 copias nos modulos - eventos.py: hub SSE em /eventos com heartbeat e Last-Event-ID; modulos publicam expressao, telemetria, conversa, reacao, ia, motores - /estado agrega o snapshot inicial; /comando unifica gesto/rosto/cor/reacao - /parar-tudo desliga o PCA9685, para reacoes e trava motores ate /rearmar - braco: lease (ttl_ms) e interpolacao com limite de velocidade no servidor - braco e diagnostico nao disputam o PCA9685 (409 para o segundo dono) - fila limitada (20) com validade de 10 s; POST /face usa prefixo "rosto" - /app/config passa a checar Origin e JSON; versao vem de version.json - figurinhas automaticas do Gemini limitadas a 6 por hora - rosto entende a ordem "calma" enviada pelo E-Stop Token so e exigido quando configurado (docs/SEGURANCA.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+20
-9
@@ -6,14 +6,18 @@ import threading
|
||||
import time
|
||||
import unicodedata
|
||||
import re
|
||||
from collections import deque
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from contextlib import contextmanager
|
||||
from urllib.request import Request, urlopen
|
||||
from cerebro import GESTOS
|
||||
from pathlib import Path
|
||||
from gemini import responder_gemini
|
||||
from comum import gravar_atomico, ler_json
|
||||
import gemini
|
||||
|
||||
FIGURINHAS_POR_HORA = 6 # cada figurinha automática é mais uma chamada paga ao Gemini
|
||||
|
||||
def resposta_factual(texto,agora=None):
|
||||
normal=''.join(c for c in unicodedata.normalize('NFD',texto.lower()) if not unicodedata.combining(c))
|
||||
normal=' '.join(normal.strip(' ?!.').split())
|
||||
@@ -93,6 +97,14 @@ class Conversa:
|
||||
if saved.get('provedor') in ('local','gemini'):self.provedor=saved['provedor']
|
||||
except (OSError,ValueError,AttributeError):pass
|
||||
self.lock=threading.Lock();self.busy=False;self.ultima=None;self.erro='';self.generation=0
|
||||
self.publicar=lambda tipo,dados:None;self.figurinhas_auto=deque()
|
||||
def avisar(self):
|
||||
self.publicar('conversa',self.status())
|
||||
def pode_gerar_figurinha(self,agora=None):
|
||||
agora=agora or time.time()
|
||||
while self.figurinhas_auto and agora-self.figurinhas_auto[0]>3600:self.figurinhas_auto.popleft()
|
||||
if len(self.figurinhas_auto)>=FIGURINHAS_POR_HORA:return False
|
||||
self.figurinhas_auto.append(agora);return True
|
||||
def status(self):
|
||||
with self.lock:return dict(provedor=self.provedor,modelo=gemini.MODELO if self.provedor=='gemini' else 'Qwen3-0.6B',gemini_configurado=bool(os.environ.get('GEMINI_API_KEY')),ocupado=self.busy,ultima=self.ultima,erro=self.erro,historico=self.memoria.historico(),lembrancas=self.memoria.listar())
|
||||
def enviar(self,texto):
|
||||
@@ -108,7 +120,9 @@ class Conversa:
|
||||
self.memoria.registrar('assistant',factual['texto'],2 if self.provedor=='gemini' else 1)
|
||||
self.ultima=dict(factual,id=time.time_ns(),horario=time.time(),segundos=0,fonte='relogio')
|
||||
self.busy=False;self.cerebro.conversando=False
|
||||
return dict(ok=True)
|
||||
factual=True
|
||||
self.avisar()
|
||||
if factual is True:return dict(ok=True)
|
||||
threading.Thread(target=self.responder,args=(generation,),daemon=True).start()
|
||||
return dict(ok=True)
|
||||
def responder(self,generation):
|
||||
@@ -140,32 +154,28 @@ class Conversa:
|
||||
self.ultima=dict(result,id=time.time_ns(),horario=time.time(),segundos=round(time.monotonic()-inicio,2))
|
||||
if self.provedor=='gemini' and treino:
|
||||
if type(reaction) is int and reaction in [r['id'] for r in catalog]:treino.testar(reaction)
|
||||
elif not treino.status()['ocupado']:
|
||||
elif not treino.status()['ocupado'] and self.pode_gerar_figurinha():
|
||||
treino.gerar_figurinha(history[-1]['content'])
|
||||
except Exception as e:
|
||||
with self.lock:self.erro=str(e)[:200] if generation==self.generation else ''
|
||||
finally:
|
||||
with self.lock,self.cerebro.lock:
|
||||
self.busy=False;self.cerebro.conversando=False;self.cerebro.last=time.monotonic()
|
||||
self.avisar()
|
||||
def rotas(self,h,method):
|
||||
route=h.path.split('?',1)[0]
|
||||
if not route.startswith('/conversa/'):return False
|
||||
try:
|
||||
if method=='GET' and route=='/conversa/status':result=self.status()
|
||||
elif method=='POST':
|
||||
origin=h.headers.get('Origin')
|
||||
if origin and origin not in ('http://'+h.headers.get('Host',''),'https://'+h.headers.get('Host','')):raise ValueError('Origem inválida')
|
||||
size=int(h.headers.get('Content-Length','0'))
|
||||
if not 0<size<=4096 or h.headers.get('Content-Type','').split(';')[0]!='application/json':raise ValueError('Use JSON até 4096 bytes')
|
||||
data=json.loads(h.rfile.read(size))
|
||||
if not isinstance(data,dict):raise ValueError('Objeto obrigatório')
|
||||
data=ler_json(h,4096)
|
||||
if route=='/conversa/config':
|
||||
with self.lock:
|
||||
if self.busy:raise ValueError('Aguarde a resposta antes de trocar a IA')
|
||||
provider=data.get('provedor')
|
||||
if provider not in ('local','gemini'):raise ValueError('Provedor inválido')
|
||||
if provider=='gemini' and not os.environ.get('GEMINI_API_KEY'):raise ValueError('Chave Gemini não configurada')
|
||||
tmp=self.config_path.with_suffix('.tmp');tmp.write_text(json.dumps(dict(provedor=provider)));tmp.replace(self.config_path)
|
||||
gravar_atomico(self.config_path,dict(provedor=provider))
|
||||
self.provedor=provider
|
||||
result=self.status()
|
||||
elif route=='/conversa/enviar':result=self.enviar(data.get('texto'))
|
||||
@@ -183,6 +193,7 @@ class Conversa:
|
||||
else:self.memoria.apagar(data.get('id'))
|
||||
result=dict(ok=True)
|
||||
else:raise ValueError('Rota inválida')
|
||||
if route!='/conversa/enviar':self.avisar()
|
||||
else:raise ValueError('Método inválido')
|
||||
h.enviar(json.dumps(result,ensure_ascii=False))
|
||||
except (ValueError,TypeError) as e:h.enviar(json.dumps(dict(erro=str(e)),ensure_ascii=False),codigo=400)
|
||||
|
||||
Reference in New Issue
Block a user