chore: auto-commit from dashboard
This commit is contained in:
@@ -114,5 +114,10 @@
|
|||||||
"memory": "memory/",
|
"memory": "memory/",
|
||||||
"logs": "logs/",
|
"logs": "logs/",
|
||||||
"sessions": "sessions/"
|
"sessions": "sessions/"
|
||||||
|
},
|
||||||
|
"roa2web": {
|
||||||
|
"base_url": "https://roa2web.romfast.ro/roa2web/api",
|
||||||
|
"default_company": "114",
|
||||||
|
"default_company_name": "ROMFAST SRL"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,9 @@
|
|||||||
"report_on": "changes",
|
"report_on": "changes",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"last_run": "2026-07-09T10:00:00.001988+00:00",
|
"last_run": "2026-07-10T10:00:00.001575+00:00",
|
||||||
"last_status": "ok",
|
"last_status": "ok",
|
||||||
"next_run": "2026-07-09T16:00:00+00:00"
|
"next_run": "2026-07-10T16:00:00+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "security-audit-daily",
|
"name": "security-audit-daily",
|
||||||
@@ -43,9 +43,9 @@
|
|||||||
"report_on": "never",
|
"report_on": "never",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"last_run": "2026-07-09T03:30:00.002154+00:00",
|
"last_run": "2026-07-10T03:30:00.002026+00:00",
|
||||||
"last_status": "ok",
|
"last_status": "ok",
|
||||||
"next_run": "2026-07-10T03:30:00+00:00"
|
"next_run": "2026-07-11T03:30:00+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "archive-tasks-daily",
|
"name": "archive-tasks-daily",
|
||||||
@@ -59,9 +59,9 @@
|
|||||||
"report_on": "changes",
|
"report_on": "changes",
|
||||||
"timeout": 60,
|
"timeout": 60,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"last_run": "2026-07-09T03:00:00.001431+00:00",
|
"last_run": "2026-07-10T03:00:00.002135+00:00",
|
||||||
"last_status": "ok",
|
"last_status": "ok",
|
||||||
"next_run": "2026-07-10T03:00:00+00:00"
|
"next_run": "2026-07-11T03:00:00+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "backup-config",
|
"name": "backup-config",
|
||||||
@@ -75,9 +75,9 @@
|
|||||||
"report_on": "never",
|
"report_on": "never",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"last_run": "2026-07-09T02:00:00.001391+00:00",
|
"last_run": "2026-07-10T02:00:00.002082+00:00",
|
||||||
"last_status": "ok",
|
"last_status": "ok",
|
||||||
"next_run": "2026-07-10T02:00:00+00:00"
|
"next_run": "2026-07-11T02:00:00+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "insights-extract",
|
"name": "insights-extract",
|
||||||
@@ -243,9 +243,9 @@
|
|||||||
"prompt": "Heartbeat check. Rulează src/heartbeat.py printr-un scurt raport de status.\nDacă nu e nimic de raportat (email=0, calendar nu are evenimente <2h, kb ok), răspunde doar cu HEARTBEAT_OK și oprește-te — nu trimite mesaj.\nDacă e ceva: raport scurt pe Discord #echo-work.",
|
"prompt": "Heartbeat check. Rulează src/heartbeat.py printr-un scurt raport de status.\nDacă nu e nimic de raportat (email=0, calendar nu are evenimente <2h, kb ok), răspunde doar cu HEARTBEAT_OK și oprește-te — nu trimite mesaj.\nDacă e ceva: raport scurt pe Discord #echo-work.",
|
||||||
"allowed_tools": [],
|
"allowed_tools": [],
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"last_run": "2026-07-09T14:00:00.001126+00:00",
|
"last_run": "2026-07-10T14:00:00.002633+00:00",
|
||||||
"last_status": "ok",
|
"last_status": "ok",
|
||||||
"next_run": "2026-07-09T18:00:00+00:00"
|
"next_run": "2026-07-10T18:00:00+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "night-execute",
|
"name": "night-execute",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"last_sent": 25,
|
"last_sent": 26,
|
||||||
"year": 2026,
|
"year": 2026,
|
||||||
"last_sent_at": "2026-07-02T19:00:58.783724+00:00"
|
"last_sent_at": "2026-07-09T15:01:09.525808+00:00"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
{
|
{
|
||||||
"notes": [
|
"notes": [
|
||||||
|
{
|
||||||
|
"file": "notes-data/youtube/2026-07-10_opencode-token-usage-96-percent.md",
|
||||||
|
"title": "I Cut My OpenCode Token Usage by 96% - Here's How",
|
||||||
|
"date": "2026-07-10",
|
||||||
|
"tags": [],
|
||||||
|
"domains": [
|
||||||
|
"work",
|
||||||
|
"growth"
|
||||||
|
],
|
||||||
|
"types": [],
|
||||||
|
"category": "youtube",
|
||||||
|
"project": null,
|
||||||
|
"subdir": null,
|
||||||
|
"video": "",
|
||||||
|
"tldr": "Autorul folosește un proxy man-in-the-middle pentru a vedea exact ce trimite OpenCode (agent de coding AI) către LLM la un simplu \"hello\" — și găsește 8.000 de tokeni: system prompt de 9.500 caractere..."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"file": "notes-data/youtube/2026-07-08_fine-tune-modele-mari-kimi-k27.md",
|
"file": "notes-data/youtube/2026-07-08_fine-tune-modele-mari-kimi-k27.md",
|
||||||
"title": "Fine-Tune the biggest open-source models (even with a bad PC)",
|
"title": "Fine-Tune the biggest open-source models (even with a bad PC)",
|
||||||
@@ -10192,11 +10208,11 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"stats": {
|
"stats": {
|
||||||
"total": 589,
|
"total": 590,
|
||||||
"by_domain": {
|
"by_domain": {
|
||||||
"work": 201,
|
"work": 202,
|
||||||
"health": 104,
|
"health": 104,
|
||||||
"growth": 274,
|
"growth": 275,
|
||||||
"sprijin": 39,
|
"sprijin": 39,
|
||||||
"scout": 9
|
"scout": 9
|
||||||
},
|
},
|
||||||
@@ -10213,7 +10229,7 @@
|
|||||||
"reflectii": 3,
|
"reflectii": 3,
|
||||||
"retete": 1,
|
"retete": 1,
|
||||||
"tools": 7,
|
"tools": 7,
|
||||||
"youtube": 158,
|
"youtube": 159,
|
||||||
"memory": 44
|
"memory": 44
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
- **[reflectii/](reflectii/index.md)** — 3 note
|
- **[reflectii/](reflectii/index.md)** — 3 note
|
||||||
- **[retete/](retete/index.md)** — 1 note
|
- **[retete/](retete/index.md)** — 1 note
|
||||||
- **[tools/](tools/index.md)** — 7 note
|
- **[tools/](tools/index.md)** — 7 note
|
||||||
- **[youtube/](youtube/index.md)** — 158 note
|
- **[youtube/](youtube/index.md)** — 159 note
|
||||||
|
|
||||||
## Note la rădăcină
|
## Note la rădăcină
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# I Cut My OpenCode Token Usage by 96% - Here's How
|
||||||
|
|
||||||
|
**URL:** https://youtu.be/FX7jcd3GYtI
|
||||||
|
**Durata:** 9:42
|
||||||
|
**Data salvat:** 2026-07-10
|
||||||
|
**Tags:** @work @growth @ai @agents @llm
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
Autorul folosește un proxy man-in-the-middle pentru a vedea exact ce trimite OpenCode (agent de coding AI) către LLM la un simplu "hello" — și găsește 8.000 de tokeni: system prompt de 9.500 caractere + 11 definiții de tool-uri (bash, task, edit, read etc.) trimise la FIECARE request, plus un al doilea apel LLM ascuns doar pentru titlul conversației. Soluția: creează un agent custom minimal (fără tools, system prompt scurt) → 300-500 tokeni, reducere ~90-96%. Recomandare: pornește minimal, adaugă tools doar când chiar ai nevoie de ele.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Puncte cheie
|
||||||
|
|
||||||
|
- **8.000 tokeni pentru "hello"** — 0,025% e mesajul propriu-zis, restul e scaffolding (system prompt + tool definitions)
|
||||||
|
- OpenCode face **2 apeluri LLM** per mesaj: unul ascuns doar ca să genereze titlul conversației (~2.000 tokeni, pt un titlu de 1 cuvânt)
|
||||||
|
- System prompt default: ~114 linii, spune LLM-ului tot ce poate/nu poate face, format, emoji, etc.
|
||||||
|
- 11 tool definitions trimise mereu — bash tool singur = 4.700 caractere, task tool = 3.000 caractere
|
||||||
|
- **Cache-ul providerului NU te ajută pe tine** — reduce costul $ pentru provider, dar tot trimiți full payload pe rețea, tot consumi context window și latență
|
||||||
|
- Soluție: agenți custom minimali (folder `.opencode/agents/*.md`) cu system prompt scurt și zero/puține tools, setați ca `primary` — switch cu Tab între agent complet (build) și agent lean
|
||||||
|
- Trade-off: agentul lean nu are task/sub-agents, web fetch, etc. — dar poți adăuga selectiv doar ce ai nevoie
|
||||||
|
- Filosofie: **start minimal, expand when necessary** — nu monta toate tool-urile/MCP-urile by default
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quote-uri relevante
|
||||||
|
|
||||||
|
> "8,000 tokens. That's what it cost me to say hello to an AI coding agent."
|
||||||
|
|
||||||
|
> "Cached tokens are a cost optimization for the provider, not you. For you, it's still 8,000 tokens of latency and context window usage."
|
||||||
|
|
||||||
|
> "Start minimal and expand when necessary."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Idei acționabile pentru Marius
|
||||||
|
|
||||||
|
- [ ] Relevant pentru Claude Code / echo-core: verifică dacă subagenții custom (`Agent` tool) pot fi definiți cu scope minim (fără tool-uri inutile) pentru task-uri simple, ca să reduci tokeni pe apelurile dese (heartbeat, routine checks) @work
|
||||||
|
- [ ] Ideea de "agent lean vs agent complet, switch la nevoie" se mapează pe strategia ta Haiku/Sonnet din AGENTS.md — poate merită un al treilea nivel: agent minimal fără tools pentru întrebări simple @growth
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Surse menționate
|
||||||
|
|
||||||
|
- Repo GitHub cu scriptul proxy + config agent (menționat, link nu apare explicit în transcript)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Index — youtube/
|
# Index — youtube/
|
||||||
|
|
||||||
> 158 note. Citește acest index întâi; deschide doar fișierele relevante.
|
> 159 note. Citește acest index întâi; deschide doar fișierele relevante.
|
||||||
|
|
||||||
- **[Talk to Claude on 3CX Phone System Tutorial (Full Setup)](2025-02-13_talk-to-claude-3cx-phone.md)** `@work`
|
- **[Talk to Claude on 3CX Phone System Tutorial (Full Setup)](2025-02-13_talk-to-claude-3cx-phone.md)** `@work`
|
||||||
Tutorial complet pentru a vorbi cu Claude Code prin telefon, folosind 3CX (sistem telefonic cloud gratuit) + un proiect GitHub custom. Setup
|
Tutorial complet pentru a vorbi cu Claude Code prin telefon, folosind 3CX (sistem telefonic cloud gratuit) + un proiect GitHub custom. Setup
|
||||||
@@ -316,5 +316,7 @@
|
|||||||
Chris Koerner explică cum pornești o afacere cu $1k-$5k și ce principii l-au dus la succes cu 35+ proprietăți RV și multiple business-uri. E
|
Chris Koerner explică cum pornești o afacere cu $1k-$5k și ce principii l-au dus la succes cu 35+ proprietăți RV și multiple business-uri. E
|
||||||
- **[Fine-Tune the biggest open-source models (even with a bad PC)](2026-07-08_fine-tune-modele-mari-kimi-k27.md)** `@work @growth`
|
- **[Fine-Tune the biggest open-source models (even with a bad PC)](2026-07-08_fine-tune-modele-mari-kimi-k27.md)** `@work @growth`
|
||||||
Tutorial complet despre cum se fine-tunează modele AI masive (ex: Kimi K2.7 — 1 trilion parametri) pe date proprii, fără hardware de sute de
|
Tutorial complet despre cum se fine-tunează modele AI masive (ex: Kimi K2.7 — 1 trilion parametri) pe date proprii, fără hardware de sute de
|
||||||
|
- **[I Cut My OpenCode Token Usage by 96% - Here's How](2026-07-10_opencode-token-usage-96-percent.md)** `@work @growth`
|
||||||
|
Autorul folosește un proxy man-in-the-middle pentru a vedea exact ce trimite OpenCode (agent de coding AI) către LLM la un simplu "hello" —
|
||||||
- **[thinking-on-paper](thinking-on-paper.md)** `@growth`
|
- **[thinking-on-paper](thinking-on-paper.md)** `@growth`
|
||||||
Metoda "Thinking on Paper" — 3 principii: **Make it Wrong** (scrie repede, fără perfecționism), **Make it Shorter** (doar keywords), **Make
|
Metoda "Thinking on Paper" — 3 principii: **Make it Wrong** (scrie repede, fără perfecționism), **Make it Shorter** (doar keywords), **Make
|
||||||
|
|||||||
@@ -787,6 +787,11 @@ def cmd_audio(args: list[str]) -> str:
|
|||||||
if len(text) > _MAX_TTS_CHARS:
|
if len(text) > _MAX_TTS_CHARS:
|
||||||
text = text[:_MAX_TTS_CHARS] + "..."
|
text = text[:_MAX_TTS_CHARS] + "..."
|
||||||
|
|
||||||
|
# Normalizare RO pentru TTS (numere, monedă, unități, abrevieri) —
|
||||||
|
# aceeași expandare ca voice mode live, fără trunchierea la 200 cuvinte
|
||||||
|
from src.voice.normalize import expand_for_tts
|
||||||
|
text = expand_for_tts(text)
|
||||||
|
|
||||||
# Apel TTS (import direct din tools/tts.py)
|
# Apel TTS (import direct din tools/tts.py)
|
||||||
result = _tts_synthesize(text, voice)
|
result = _tts_synthesize(text, voice)
|
||||||
if result.get("ok"):
|
if result.get("ok"):
|
||||||
|
|||||||
@@ -201,8 +201,12 @@ _CURRENCY_SUB = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_CURRENCY_PATTERNS = [
|
_CURRENCY_PATTERNS = [
|
||||||
# RON suffix (case-insensitive: RON, ron, lei)
|
# RON suffix (case-insensitive: RON, ron, lei). Accepts either a
|
||||||
(re.compile(r'(?<!\w)(\d+(?:\.\d+)?)\s+(?:RON|lei)\b', re.IGNORECASE), 'RON'),
|
# dot-decimal fraction ("12.50 lei") or, since Romanian accounting
|
||||||
|
# format uses comma for decimals ("53.600,00 lei" — the dot-thousands
|
||||||
|
# group is already collapsed by normalize_thousands upstream), a
|
||||||
|
# comma-decimal fraction in its own capture group.
|
||||||
|
(re.compile(r'(?<!\w)(\d+(?:\.\d+)?)(?:,(\d{1,2}))?\s+(?:RON|lei)\b', re.IGNORECASE), 'RON'),
|
||||||
# Prefix currencies
|
# Prefix currencies
|
||||||
(re.compile(r'\$(\d+(?:\.\d+)?)'), 'USD'),
|
(re.compile(r'\$(\d+(?:\.\d+)?)'), 'USD'),
|
||||||
(re.compile(r'€(\d+(?:\.\d+)?)'), 'EUR'),
|
(re.compile(r'€(\d+(?:\.\d+)?)'), 'EUR'),
|
||||||
@@ -222,25 +226,28 @@ def _format_currency_unit(n: int, singular: str, plural: str) -> str:
|
|||||||
return f"{word} {plural}"
|
return f"{word} {plural}"
|
||||||
|
|
||||||
|
|
||||||
def _format_currency(amount: str, code: str) -> str:
|
def _format_currency(amount: str, code: str, comma_frac: str | None = None) -> str:
|
||||||
main_sg, main_pl = _CURRENCY_MAIN[code]
|
main_sg, main_pl = _CURRENCY_MAIN[code]
|
||||||
if '.' in amount:
|
if comma_frac is not None:
|
||||||
|
whole_s, frac_s = amount, comma_frac
|
||||||
|
elif '.' in amount:
|
||||||
whole_s, frac_s = amount.split('.', 1)
|
whole_s, frac_s = amount.split('.', 1)
|
||||||
# Normalize fractional part to 2 digits so "12.5 RON" reads as
|
else:
|
||||||
# 50 bani, not 5 bani.
|
return _format_currency_unit(int(amount), main_sg, main_pl)
|
||||||
if len(frac_s) == 1:
|
# Normalize fractional part to 2 digits so "12.5 RON" reads as
|
||||||
frac_s = frac_s + '0'
|
# 50 bani, not 5 bani.
|
||||||
elif len(frac_s) > 2:
|
if len(frac_s) == 1:
|
||||||
frac_s = frac_s[:2]
|
frac_s = frac_s + '0'
|
||||||
whole = int(whole_s)
|
elif len(frac_s) > 2:
|
||||||
frac = int(frac_s)
|
frac_s = frac_s[:2]
|
||||||
whole_part = _format_currency_unit(whole, main_sg, main_pl)
|
whole = int(whole_s)
|
||||||
if frac == 0:
|
frac = int(frac_s)
|
||||||
return whole_part
|
whole_part = _format_currency_unit(whole, main_sg, main_pl)
|
||||||
sub_sg, sub_pl = _CURRENCY_SUB[code]
|
if frac == 0:
|
||||||
frac_part = _format_currency_unit(frac, sub_sg, sub_pl)
|
return whole_part
|
||||||
return f"{whole_part} și {frac_part}"
|
sub_sg, sub_pl = _CURRENCY_SUB[code]
|
||||||
return _format_currency_unit(int(amount), main_sg, main_pl)
|
frac_part = _format_currency_unit(frac, sub_sg, sub_pl)
|
||||||
|
return f"{whole_part} și {frac_part}"
|
||||||
|
|
||||||
|
|
||||||
def expand_currency(text: str) -> str:
|
def expand_currency(text: str) -> str:
|
||||||
@@ -251,7 +258,10 @@ def expand_currency(text: str) -> str:
|
|||||||
bani / cenți / pence).
|
bani / cenți / pence).
|
||||||
"""
|
"""
|
||||||
for pattern, code in _CURRENCY_PATTERNS:
|
for pattern, code in _CURRENCY_PATTERNS:
|
||||||
text = pattern.sub(lambda m, c=code: _format_currency(m.group(1), c), text)
|
def _sub(m, c=code):
|
||||||
|
comma_frac = m.group(2) if m.re.groups >= 2 else None
|
||||||
|
return _format_currency(m.group(1), c, comma_frac)
|
||||||
|
text = pattern.sub(_sub, text)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
@@ -296,12 +306,13 @@ _MAX_WORDS = 200
|
|||||||
_TRUNCATE_SUFFIX = "Restul l-am scris în chat."
|
_TRUNCATE_SUFFIX = "Restul l-am scris în chat."
|
||||||
|
|
||||||
|
|
||||||
def normalize_for_tts(text: str) -> str:
|
def expand_for_tts(text: str) -> str:
|
||||||
"""Apply the full normalization pipeline and truncate to 200 words.
|
"""Apply the full normalization pipeline (markdown strip, abbreviations,
|
||||||
|
numbers, currency, units, symbols) WITHOUT the 200-word truncation.
|
||||||
|
|
||||||
If the text exceeds 200 words, the first 200 are kept and the suffix
|
Use this for one-shot TTS generation (e.g. /audio command) where the
|
||||||
"Restul l-am scris în chat." is appended so the listener knows the
|
"Restul l-am scris în chat." suffix from normalize_for_tts() would be
|
||||||
response continues in the text channel mirror.
|
misleading (no live chat mirror exists for that flow).
|
||||||
"""
|
"""
|
||||||
text = strip_markdown(text)
|
text = strip_markdown(text)
|
||||||
text = sanitize_punctuation(text)
|
text = sanitize_punctuation(text)
|
||||||
@@ -312,6 +323,17 @@ def normalize_for_tts(text: str) -> str:
|
|||||||
text = expand_units(text)
|
text = expand_units(text)
|
||||||
text = expand_numbers_ro(text)
|
text = expand_numbers_ro(text)
|
||||||
text = expand_symbols(text)
|
text = expand_symbols(text)
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_for_tts(text: str) -> str:
|
||||||
|
"""Apply the full normalization pipeline and truncate to 200 words.
|
||||||
|
|
||||||
|
If the text exceeds 200 words, the first 200 are kept and the suffix
|
||||||
|
"Restul l-am scris în chat." is appended so the listener knows the
|
||||||
|
response continues in the text channel mirror.
|
||||||
|
"""
|
||||||
|
text = expand_for_tts(text)
|
||||||
words = text.split()
|
words = text.split()
|
||||||
if len(words) > _MAX_WORDS:
|
if len(words) > _MAX_WORDS:
|
||||||
text = ' '.join(words[:_MAX_WORDS]) + f" {_TRUNCATE_SUFFIX}"
|
text = ' '.join(words[:_MAX_WORDS]) + f" {_TRUNCATE_SUFFIX}"
|
||||||
|
|||||||
@@ -78,6 +78,28 @@ def test_expand_currency(text, expected):
|
|||||||
assert expand_currency(text) == expected
|
assert expand_currency(text) == expected
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Currency — Romanian accounting format (comma decimal, dot
|
||||||
|
# thousands already collapsed upstream by normalize_thousands)
|
||||||
|
# ============================================================
|
||||||
|
@pytest.mark.parametrize("text,expected", [
|
||||||
|
("53600,00 lei", "cincizeci și trei de mii șase sute de lei"),
|
||||||
|
("17600,50 lei", "șaptesprezece mii șase sute de lei și cincizeci de bani"),
|
||||||
|
("100,00 lei", "o sută de lei"),
|
||||||
|
("17600,05 lei", "șaptesprezece mii șase sute de lei și cinci bani"),
|
||||||
|
])
|
||||||
|
def test_expand_currency_comma_decimal(text, expected):
|
||||||
|
assert expand_currency(text) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("text,expected", [
|
||||||
|
("53.600,00 lei", "cincizeci și trei de mii șase sute de lei"),
|
||||||
|
("17.600,50 lei", "șaptesprezece mii șase sute de lei și cincizeci de bani"),
|
||||||
|
])
|
||||||
|
def test_normalize_for_tts_currency_ro_accounting_format(text, expected):
|
||||||
|
assert normalize_for_tts(text) == expected
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Symbols (4)
|
# Symbols (4)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"D406": "ca6103448d663ab16fcaef0f29f8933ef526cbf5aad12c7ff5dbd61b22ca9fc6",
|
"D406": "ca6103448d663ab16fcaef0f29f8933ef526cbf5aad12c7ff5dbd61b22ca9fc6",
|
||||||
"SIT_FIN_SEM_2025": "8164843431e6b703a38fbdedc7898ec6ae83559fe10f88663ba0b55f3091d5fe",
|
"SIT_FIN_SEM_2025": "8164843431e6b703a38fbdedc7898ec6ae83559fe10f88663ba0b55f3091d5fe",
|
||||||
"SIT_FIN_AN_2025": "accceef5b6585a3e901d83d23fc2e60f6562eac4a2ce00f943856232bed929d6",
|
"SIT_FIN_AN_2025": "accceef5b6585a3e901d83d23fc2e60f6562eac4a2ce00f943856232bed929d6",
|
||||||
"DESCARCARE_DECLARATII": "0fb1535fe97aeadc7f415431238fae2cf7bc52d9e0c17ce3c4b4a12db92cd733",
|
"DESCARCARE_DECLARATII": "1abf61068fc38e76963520f455a87635efdcf6c126fbf913d8d860478d3fc5ba",
|
||||||
"D205": "d3c20a7ae70f4c18bbb7add42af035e3746d323b2e6df37a4e31ed625ddb86d9",
|
"D205": "d3c20a7ae70f4c18bbb7add42af035e3746d323b2e6df37a4e31ed625ddb86d9",
|
||||||
"D390": "31d0bf3f98eda3556039cc7e186bfcaf71bdb962459a2cf54f4d03d1d57053fc",
|
"D390": "31d0bf3f98eda3556039cc7e186bfcaf71bdb962459a2cf54f4d03d1d57053fc",
|
||||||
"BILANT_2024": "fbb8d66c2e530d8798362992c6983e07e1250188228c758cb6da4cde4f955950",
|
"BILANT_2024": "fbb8d66c2e530d8798362992c6983e07e1250188228c758cb6da4cde4f955950",
|
||||||
|
|||||||
@@ -507,6 +507,11 @@ L153
|
|||||||
Se transmit prin
|
Se transmit prin
|
||||||
portalul
|
portalul
|
||||||
e-guvernare.ro
|
e-guvernare.ro
|
||||||
|
Formularul
|
||||||
|
S007
|
||||||
|
Se transmit prin
|
||||||
|
portalul
|
||||||
|
e-guvernare.ro
|
||||||
C801
|
C801
|
||||||
- Cerere de atribuire a numărului unic de identificare din aplicaţia
|
- Cerere de atribuire a numărului unic de identificare din aplicaţia
|
||||||
informatică a ANAF - publicat în data de
|
informatică a ANAF - publicat în data de
|
||||||
|
|||||||
214
tools/roa2web_client.py
Normal file
214
tools/roa2web_client.py
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Client generic pentru API-ul roa2web — autentificare + query-uri rapoarte.
|
||||||
|
|
||||||
|
Autentificare: user/parolă Oracle (ca la login-ul normal ROA) + 2FA email la
|
||||||
|
prima logare. După primul login cu trust_device=True, sesiunile următoare
|
||||||
|
folosesc refresh_token din keyring — fără OTP.
|
||||||
|
|
||||||
|
Credențiale (keyring, service "echo-core", vezi src/credential_store.py):
|
||||||
|
roa2web_user, roa2web_pass, roa2web_server,
|
||||||
|
roa2web_access_token, roa2web_refresh_token, roa2web_trusted_device_token
|
||||||
|
|
||||||
|
Firma implicită și base_url sunt în config.json -> "roa2web".
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 tools/roa2web_client.py summary [--company COD_SAU_NUME] [--luna N] [--an N]
|
||||||
|
python3 tools/roa2web_client.py companies
|
||||||
|
python3 tools/roa2web_client.py get <path> [--company COD_SAU_NUME] [--param k=v ...]
|
||||||
|
python3 tools/roa2web_client.py verify-2fa <cod> <email>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlsplit, urlunsplit
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
|
from src.credential_store import get_secret, set_secret
|
||||||
|
from src.config import Config
|
||||||
|
|
||||||
|
|
||||||
|
class ROA2WebError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ROA2WebClient:
|
||||||
|
def __init__(self):
|
||||||
|
self.config = Config()
|
||||||
|
self.base_url = self.config.get("roa2web.base_url", "https://roa2web.romfast.ro/roa2web/api")
|
||||||
|
self.session = requests.Session()
|
||||||
|
self._token = get_secret("roa2web_access_token")
|
||||||
|
|
||||||
|
parsed = urlsplit(self.base_url)
|
||||||
|
self._origin = f"{parsed.scheme}://{parsed.netloc}"
|
||||||
|
# IIS/ARR reverse-proxy-ul din fata backend-ului serveste totul sub /roa2web,
|
||||||
|
# dar FastAPI nu stie de acest prefix -> redirect-urile lui (307, ex. trailing
|
||||||
|
# slash pe rutele definite la "/") vin fara el si pica in 404. Il reinjectam.
|
||||||
|
self._prefix = parsed.path[: -len("/api")] if parsed.path.endswith("/api") else parsed.path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_company(self) -> str:
|
||||||
|
return self.config.get("roa2web.default_company", "114")
|
||||||
|
|
||||||
|
def _store_tokens(self, data: dict) -> None:
|
||||||
|
set_secret("roa2web_access_token", data["access_token"])
|
||||||
|
if data.get("refresh_token"):
|
||||||
|
set_secret("roa2web_refresh_token", data["refresh_token"])
|
||||||
|
if data.get("trusted_device_token"):
|
||||||
|
set_secret("roa2web_trusted_device_token", data["trusted_device_token"])
|
||||||
|
self._token = data["access_token"]
|
||||||
|
|
||||||
|
def _login_password(self) -> None:
|
||||||
|
user = get_secret("roa2web_user")
|
||||||
|
pw = get_secret("roa2web_pass")
|
||||||
|
server = get_secret("roa2web_server") or "romfast"
|
||||||
|
if not user or not pw:
|
||||||
|
raise ROA2WebError("Lipsesc roa2web_user/roa2web_pass din keyring (eco secrets set ...)")
|
||||||
|
|
||||||
|
payload = {"username": user, "password": pw, "server_id": server}
|
||||||
|
trusted = get_secret("roa2web_trusted_device_token")
|
||||||
|
if trusted:
|
||||||
|
payload["trusted_device_token"] = trusted
|
||||||
|
|
||||||
|
r = self.session.post(f"{self.base_url}/auth/login", json=payload, timeout=15)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
if data.get("requires_2fa"):
|
||||||
|
raise ROA2WebError(
|
||||||
|
f"Necesar cod OTP nou, trimis pe {data.get('masked_email')}. "
|
||||||
|
"Cere-i lui Marius codul, apoi ruleaza verify_2fa(code, email)."
|
||||||
|
)
|
||||||
|
self._store_tokens(data)
|
||||||
|
|
||||||
|
def _refresh(self) -> bool:
|
||||||
|
refresh_token = get_secret("roa2web_refresh_token")
|
||||||
|
if not refresh_token:
|
||||||
|
return False
|
||||||
|
r = self.session.post(
|
||||||
|
f"{self.base_url}/auth/refresh",
|
||||||
|
json={"refresh_token": refresh_token},
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
if r.status_code != 200:
|
||||||
|
return False
|
||||||
|
self._store_tokens(r.json())
|
||||||
|
return True
|
||||||
|
|
||||||
|
def verify_2fa(self, code: str, email: str, server_id: str | None = None) -> dict:
|
||||||
|
server = server_id or get_secret("roa2web_server") or "romfast"
|
||||||
|
r = self.session.post(
|
||||||
|
f"{self.base_url}/auth/verify-2fa-code",
|
||||||
|
json={"code": code, "email": email, "server_id": server, "trust_device": True},
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
self._store_tokens(data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def request(self, method: str, path: str, params: dict | None = None, _retried: bool = False) -> dict:
|
||||||
|
if not self._token and not self._refresh():
|
||||||
|
self._login_password()
|
||||||
|
|
||||||
|
url = f"{self.base_url}{path}" if path.startswith("/") else f"{self.base_url}/{path}"
|
||||||
|
headers = {"Authorization": f"Bearer {self._token}"}
|
||||||
|
r = self.session.request(method, url, params=params, headers=headers, timeout=30, allow_redirects=False)
|
||||||
|
|
||||||
|
if r.status_code in (307, 308) and "Location" in r.headers:
|
||||||
|
loc = urlsplit(r.headers["Location"])
|
||||||
|
loc_path = loc.path
|
||||||
|
if self._prefix and not loc_path.startswith(self._prefix):
|
||||||
|
loc_path = self._prefix + loc_path
|
||||||
|
fixed_url = urlunsplit((urlsplit(self.base_url).scheme, urlsplit(self.base_url).netloc, loc_path, loc.query, ""))
|
||||||
|
r = self.session.request(method, fixed_url, headers=headers, timeout=30)
|
||||||
|
|
||||||
|
if r.status_code == 401 and not _retried:
|
||||||
|
if self._refresh() or (self._login_password() or True):
|
||||||
|
return self.request(method, path, params=params, _retried=True)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
def get(self, path: str, params: dict | None = None) -> dict:
|
||||||
|
return self.request("GET", path, params=params)
|
||||||
|
|
||||||
|
def companies(self) -> list:
|
||||||
|
return self.get("/companies").get("companies", [])
|
||||||
|
|
||||||
|
def dashboard_summary(self, company: str | None = None, luna: int | None = None, an: int | None = None) -> dict:
|
||||||
|
params = {"company": company or self.default_company}
|
||||||
|
if luna:
|
||||||
|
params["luna"] = luna
|
||||||
|
if an:
|
||||||
|
params["an"] = an
|
||||||
|
return self.get("/reports/dashboard/summary", params)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_company(client: ROA2WebClient, value: str | None) -> str:
|
||||||
|
"""Acceptă cod numeric sau (parte din) numele firmei; implicit firma din config."""
|
||||||
|
if not value:
|
||||||
|
return client.default_company
|
||||||
|
if value.isdigit():
|
||||||
|
return value
|
||||||
|
needle = value.lower()
|
||||||
|
matches = [c for c in client.companies() if needle in c["name"].lower()]
|
||||||
|
if not matches:
|
||||||
|
raise ROA2WebError(f"Firma '{value}' nu a fost gasita in lista de firme.")
|
||||||
|
if len(matches) > 1:
|
||||||
|
names = ", ".join(f"{c['id_firma']}={c['name']}" for c in matches)
|
||||||
|
raise ROA2WebError(f"'{value}' e ambiguu, se potriveste cu mai multe firme: {names}")
|
||||||
|
return str(matches[0]["id_firma"])
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Client CLI pentru roa2web API")
|
||||||
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
|
p = sub.add_parser("summary", help="Dashboard financiar (trezorerie, clienti, furnizori)")
|
||||||
|
p.add_argument("--company", help="Cod sau nume firma (implicit: cea din config.json)")
|
||||||
|
p.add_argument("--luna", type=int)
|
||||||
|
p.add_argument("--an", type=int)
|
||||||
|
|
||||||
|
sub.add_parser("companies", help="Lista firmelor disponibile")
|
||||||
|
|
||||||
|
p = sub.add_parser("get", help="Query generic pe orice endpoint /api/...")
|
||||||
|
p.add_argument("path", help='ex: "/reports/invoices" sau "/reports/treasury/bank-cash-register"')
|
||||||
|
p.add_argument("--company", help="Daca e dat, se adauga ca param 'company'")
|
||||||
|
p.add_argument("--param", action="append", default=[], metavar="k=v", help="Parametru query, repetabil")
|
||||||
|
|
||||||
|
v = sub.add_parser("verify-2fa", help="Finalizeaza login-ul cu un cod OTP primit pe email")
|
||||||
|
v.add_argument("code")
|
||||||
|
v.add_argument("email")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
client = ROA2WebClient()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if args.cmd == "summary":
|
||||||
|
company = resolve_company(client, args.company)
|
||||||
|
data = client.dashboard_summary(company=company, luna=args.luna, an=args.an)
|
||||||
|
elif args.cmd == "companies":
|
||||||
|
for c in client.companies():
|
||||||
|
print(f"{c['id_firma']}\t{c['name']}")
|
||||||
|
return
|
||||||
|
elif args.cmd == "get":
|
||||||
|
params = dict(kv.split("=", 1) for kv in args.param)
|
||||||
|
if args.company:
|
||||||
|
params["company"] = resolve_company(client, args.company)
|
||||||
|
data = client.get(args.path, params or None)
|
||||||
|
elif args.cmd == "verify-2fa":
|
||||||
|
raw = client.verify_2fa(args.code, args.email)
|
||||||
|
data = {k: v for k, v in raw.items() if k not in ("access_token", "refresh_token", "trusted_device_token")}
|
||||||
|
except ROA2WebError as e:
|
||||||
|
print(f"Eroare: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(json.dumps(data, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user