feat(maria): asociere prin cod de telefon si citirea capturilor cu erori
Doua lucruri cerute de utilizator, ambele pe acelasi drum: sa poti lega
puntea fara sa ai un ecran de scanat, si sa poti trimite Mariei o poza cu
eroarea in loc sa transcrii mesajul.
Asociere prin cod de 8 caractere (alternativa la QR):
- endpoint-ul /pair exista, dar nu era folosibil: fara `browser` explicit
WhatsApp refuza codul, iar dupa introducerea lui corecta serverul cere un
restart (515) care consuma din bugetul de reincercari si putea opri puntea.
Acum descriptorul e Browsers.ubuntu('Chrome') si restartRequired reconecteaza
imediat, fara sa numere.
- codul are TTL de 3 minute, iar /status il da doar cat timp e valabil —
un cod expirat afisat in dashboard trimite omul sa tasteze degeaba.
- dashboard: camp pentru numar + buton, in acelasi card cu QR-ul.
Imagini cu erori (capturi de ecran):
- puntea descarca imaginile in ~/.maria-bridge/media/ (imageMessage sau
document cu mimetype image/*, si prin ambalajele efemer/"vezi o data" —
fara despachetare pareau mesaje fara continut si se aruncau tacut).
- rag/ocr.py: tesseract ron+eng. Modelul de raspuns e strict text, deci OCR
nu e o optiune de calitate, e singura cale.
- cautarea in index merge DOAR pe liniile care arata a eroare; o fereastra
intreaga de meniuri si totaluri dilueaza embedding-ul si scoate chunk-uri
fara legatura. Modelul primeste captura intreaga, marcata ca text OCR.
- capturile se sterg imediat dupa citire (pot contine date de client).
- cand nu se citeste nimic si nu exista legenda, Maria cere textul erorii
in loc sa raspunda in gol.
16 teste noi (42 in total). install.sh verifica tesseract; README documenteaza
ambele metode de asociere si drumul unei capturi.
Separat, in docs/chatboti-si-punti.md: chatul "Eu" e vazut de AMBELE punti de pe
numar. Puntea lui Echo (LXC 110) e asociata ca dispozitiv :11 al aceluiasi cont,
momentan nelegata dar pornita — daca se reasociaza, raspunde in "Eu" langa Maria.
Notat si ca serviciile lui Echo sunt unitati de UTILIZATOR: `systemctl is-active`
ca root raspunde "inactive" desi botul ruleaza.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4uzvgm7AyJch5WH8QHRhY
This commit is contained in:
@@ -98,6 +98,29 @@ def maria_whatsapp_status() -> dict:
|
||||
return {"connected": False, "phone": None, "qr": None, "reachable": False}
|
||||
|
||||
|
||||
def maria_request_pairing_code(phone: str) -> tuple[dict, int]:
|
||||
"""Cere puntii un cod de asociere de 8 caractere pentru `phone`.
|
||||
|
||||
Dashboard-ul nu decide nimic despre numar — puntea valideaza si raspunde; noi
|
||||
doar transmitem, ca sa nu existe doua reguli de validare care se pot desincroniza.
|
||||
"""
|
||||
body = json.dumps({"phone": phone}).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
f"{maria_bridge_url()}/pair", data=body,
|
||||
headers={"Content-Type": "application/json"}, method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return json.loads(resp.read().decode("utf-8")), resp.status
|
||||
except urllib.error.HTTPError as exc:
|
||||
try:
|
||||
return json.loads(exc.read().decode("utf-8")), exc.code
|
||||
except ValueError:
|
||||
return {"ok": False, "error": f"puntea a raspuns {exc.code}"}, exc.code
|
||||
except (urllib.error.URLError, TimeoutError, OSError, ValueError) as exc:
|
||||
return {"ok": False, "error": f"puntea WhatsApp nu raspunde: {exc}"}, 503
|
||||
|
||||
|
||||
def maria_index_info() -> dict:
|
||||
try:
|
||||
entries = json.loads(MARIA_INDEX_FILE.read_text(encoding="utf-8"))
|
||||
@@ -710,6 +733,8 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
return self.handle_decide()
|
||||
if path == "/api/maria/service":
|
||||
return self.handle_maria_service()
|
||||
if path == "/api/maria/pair":
|
||||
return self.handle_maria_pair()
|
||||
if path == "/api/maria/documents":
|
||||
return self.handle_maria_document_write()
|
||||
if path == "/api/maria/documents/delete":
|
||||
@@ -803,6 +828,16 @@ class Handler(SimpleHTTPRequestHandler):
|
||||
time.sleep(1.0)
|
||||
return self.send_json({"ok": True, "action": action, "service": unit_info(unit)})
|
||||
|
||||
def handle_maria_pair(self):
|
||||
"""Cod de asociere WhatsApp — alternativa la QR, cand nu poti scana ecranul."""
|
||||
data = self.read_json()
|
||||
phone = str(data.get("phone") or "")
|
||||
who = tailnet_user(self.headers) or "local"
|
||||
print(f"[actiune] cod de asociere WhatsApp pentru {phone}, cerut de {who}",
|
||||
file=sys.stderr, flush=True)
|
||||
out, status = maria_request_pairing_code(phone)
|
||||
return self.send_json(out, status)
|
||||
|
||||
def handle_maria_document_write(self):
|
||||
data = self.read_json()
|
||||
name = str(data.get("name") or "")
|
||||
|
||||
@@ -99,6 +99,19 @@
|
||||
<h2>Conectare WhatsApp</h2>
|
||||
<div id="mWaStatus" class="empty">se încarcă…</div>
|
||||
<div id="mWaQr"></div>
|
||||
<div id="mWaPair" hidden>
|
||||
<div class="detail" style="margin-top:var(--space-5)">
|
||||
Nu poți scana codul QR? Cere un <strong>cod de asociere</strong> și
|
||||
introdu-l pe telefon în WhatsApp → Dispozitive conectate → Conectează un
|
||||
dispozitiv → <em>Conectează cu număr de telefon</em>.
|
||||
</div>
|
||||
<div class="actions" style="margin-top:var(--space-3)">
|
||||
<input type="text" id="mPairPhone" placeholder="40723197939 (internațional, fără +)"
|
||||
style="flex:1;min-width:220px">
|
||||
<button onclick="mPair()">Cere cod de asociere</button>
|
||||
</div>
|
||||
<div id="mPairOut" class="detail" style="margin-top:var(--space-3)"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
@@ -351,6 +364,31 @@ function mMetricsHtml(info) {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function mShowPairCode(code, expiresIn) {
|
||||
var pretty = code.length === 8 ? code.slice(0, 4) + '-' + code.slice(4) : code;
|
||||
var out = document.getElementById('mPairOut');
|
||||
out.dataset.code = code;
|
||||
out.innerHTML =
|
||||
'<div style="font-family:monospace;font-size:1.9rem;letter-spacing:.18em;margin:var(--space-3) 0">'
|
||||
+ esc(pretty) + '</div><span class="muted">tastează-l pe telefon'
|
||||
+ (expiresIn ? ' — expiră în ~' + Math.round(expiresIn / 60) + ' min' : '')
|
||||
+ '. Dacă expiră, cere altul.</span>';
|
||||
}
|
||||
|
||||
async function mPair() {
|
||||
var phone = document.getElementById('mPairPhone').value.trim();
|
||||
var out = document.getElementById('mPairOut');
|
||||
out.textContent = 'cer codul…';
|
||||
var res = await post('api/maria/pair', { phone: phone });
|
||||
if (res.ok && res.data && res.data.code) {
|
||||
mShowPairCode(res.data.code, res.data.expires_in);
|
||||
} else {
|
||||
out.innerHTML = '<span class="pill bad">eroare</span> '
|
||||
+ esc((res.data && res.data.error) || 'nu am putut cere codul');
|
||||
}
|
||||
mRefresh();
|
||||
}
|
||||
|
||||
async function mRefresh() {
|
||||
var res = await api('api/maria/status');
|
||||
if (!res.ok) return;
|
||||
@@ -364,9 +402,21 @@ async function mRefresh() {
|
||||
var wa = s.whatsapp || {};
|
||||
var waStatus = document.getElementById('mWaStatus');
|
||||
var waQr = document.getElementById('mWaQr');
|
||||
// Cardul de asociere are rost doar cât timp puntea rulează și nu e conectată.
|
||||
document.getElementById('mWaPair').hidden = !!wa.connected || wa.reachable === false;
|
||||
var pairOut = document.getElementById('mPairOut');
|
||||
if (wa.pairing && wa.pairing.code) {
|
||||
mShowPairCode(wa.pairing.code, wa.pairing.expires_in);
|
||||
} else if (!wa.connected && pairOut.dataset.code) {
|
||||
// Codul afisat a expirat la punte. Lasat pe ecran, ar trimite omul sa
|
||||
// tasteze un cod pe care WhatsApp il refuza fara sa spuna de ce.
|
||||
pairOut.innerHTML = '<span class="muted">codul a expirat — cere altul</span>';
|
||||
delete pairOut.dataset.code;
|
||||
}
|
||||
if (wa.connected) {
|
||||
waStatus.innerHTML = '<span class="pill ok">conectat</span> ca ' + esc(wa.phone || '?');
|
||||
waQr.innerHTML = '';
|
||||
document.getElementById('mPairOut').innerHTML = '';
|
||||
} else if (wa.qr) {
|
||||
waStatus.innerHTML = '<span class="pill bad">neconectat</span> — scanează codul QR din WhatsApp → Dispozitive conectate';
|
||||
waQr.innerHTML = '<img src="' + wa.qr + '" style="width:220px;height:220px;border-radius:8px;background:#fff;padding:8px;margin-top:var(--space-4)">';
|
||||
|
||||
Reference in New Issue
Block a user