Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a14fe87f9 | |||
| a6c46f34e4 |
@@ -39,6 +39,9 @@ Alle wesentlichen Projektänderungen werden hier in verständlicher Form dokumen
|
||||
bleiben ladbar.
|
||||
- Nicht geheime Providerkonfiguration liegt lokal außerhalb von Git; ein Key wird
|
||||
erst unmittelbar vor einem zulässigen Cloudaufruf geladen.
|
||||
- Allgemeine unpersönliche Technikfragen werden präziser als `allowed`
|
||||
klassifiziert; persönliche, interne und geheime Inhalte behalten Vorrang.
|
||||
- Privacy-Ausgabe unterscheidet lokalen Modus von technischem Fallback.
|
||||
|
||||
### Security
|
||||
|
||||
@@ -48,3 +51,5 @@ Alle wesentlichen Projektänderungen werden hier in verständlicher Form dokumen
|
||||
Nicht-Billing deaktiviert; Javis aktiviert niemals Billing.
|
||||
- Cloudkontext wird lokal klassifiziert und minimiert; 429, Netzwerk-, Timeout-,
|
||||
Key- und Modellfehler fallen auf Ollama zurück.
|
||||
- Gemini-/Datenschutzablauf wurde am 30.07.2026 zusätzlich von Pascal manuell
|
||||
erfolgreich abgenommen; kein Keywert erschien in der Ausgabe.
|
||||
|
||||
+21
-11
@@ -99,6 +99,11 @@ Rootserver:
|
||||
- `scripts/start-javis.ps1` verifiziert das Repository, setzt nur
|
||||
Prozessvariablen, prüft Ollama auf Loopback, startet es bei Bedarf versteckt
|
||||
und wartet begrenzt. Pfade sind per Parameter oder `JAVIS_*` überschreibbar.
|
||||
- Allgemeine unpersönliche Technikfragen zu RAM/SSD, SQLite, HTTP, Git und
|
||||
ähnlichen Begriffen werden gezielter `allowed`; `never` und `ask` behalten
|
||||
Vorrang bei Secrets, interner Infrastruktur und persönlichem Kontext.
|
||||
- `/privacy` trennt Inhaltsklassifikation, tatsächlichen Provider, Unterdrückung
|
||||
durch Modus `local` und technischen Provider-Fallback.
|
||||
|
||||
## Aktuelle Architektur
|
||||
|
||||
@@ -171,12 +176,11 @@ Letzter bestätigter Projektstand:
|
||||
- uv-Lock und `uv sync --dev`: bestanden
|
||||
- Python in `.venv`: 3.12.13
|
||||
- Ruff in `.venv`: 0.16.0
|
||||
- Unit-Tests: 62 bestanden; Timeout und fehlendes Modell fallen nachweislich lokal zurück
|
||||
- Unit-Tests: 66 bestanden; neue Technikklassifikation und Privacy-Ausgabe abgedeckt
|
||||
- PowerShell-Syntax des Startskripts: erfolgreich geparst
|
||||
- Ruff Lint: bestanden
|
||||
- Ruff Formatprüfung: bestanden
|
||||
- lokaler nachrichtenfreier CLI-Smoke: bestanden; Nullkostenmodus aktiv,
|
||||
Gemini nicht konfiguriert, Schlüssel nicht vorhanden, 0 Cloudanfragen
|
||||
- lokaler CLI-Smoke ohne Secretzugriff: bestanden
|
||||
- Obsidian-Vault unverändert: 1 Markdown-, 11 Gesamtdateien, letzter
|
||||
Schreibzeitpunkt weiterhin 30.07.2026 15:42:31
|
||||
- Rootserver und Ubuntu-Laptop wurden nicht kontaktiert
|
||||
@@ -203,15 +207,21 @@ Abnahmestatus:
|
||||
- manuell durch Pascal getestet: am 30.07.2026 erfolgreich; CLI-Start, echte
|
||||
deutsche Antworten, Sitzungskontext, `/sessions`, `/exit`, Neustart,
|
||||
`/load <UUID>`, vollständiger Verlauf und Fortsetzung
|
||||
- Gemini/Datenschutz/Fallback automatisiert durch Codex getestet: ja, nur mit
|
||||
Test-Doubles und lokalem nachrichtenfreiem CLI-Smoke
|
||||
- Gemini/Datenschutz/Fallback manuell durch Pascal getestet: noch nicht
|
||||
- von Pascal für produktive Verwendung freigegeben: noch nicht ausdrücklich erklärt
|
||||
- Gemini/Datenschutz/Fallback automatisiert durch Codex getestet: ja
|
||||
- Gemini/Datenschutz/Fallback manuell durch Pascal getestet: am 30.07.2026;
|
||||
verdeckte Key-Speicherung ohne Wertausgabe, vollständiger `/status`, echter
|
||||
Gemini-Erfolg, Cloudzähler, `ask`-Ablehnung, `never`, `/provider local` und
|
||||
Startskript erfolgreich
|
||||
- für Merge freigegeben: ja, nach den dokumentierten kleinen Korrekturen
|
||||
- für produktive Nutzung freigegeben: nicht separat erklärt; keine Freigabe für
|
||||
spätere Tools, Obsidian oder autonome Aktionen
|
||||
|
||||
## Git-Stand
|
||||
|
||||
- aktueller Branch: `feat/gemini-privacy-router`
|
||||
- stabiler Branch: `main`
|
||||
- Feature-Branch ist zu `origin/feat/gemini-privacy-router` gepusht und verfolgt
|
||||
diesen Remote-Branch; `main` und `origin/main` blieben auf `8a678ae`
|
||||
- Grundgerüstcommit: `6d04171`
|
||||
- letzter Commit vor der Handoff-Verdichtung: `0e82748`
|
||||
- Handoff-Verdichtung auf `main`: `e6eb995`
|
||||
@@ -243,7 +253,7 @@ Abnahmestatus:
|
||||
- aktuell: portable Ollama-Laufzeit und `qwen3:8b` außerhalb von Git verifiziert
|
||||
- aktuell: lokaler CLI-Chat und persistente SQLite-Sitzungen vollständig abgenommen
|
||||
- aktuell: Gemini-Free-Hybridrouting, Datenschutz und Nullkostenmodus
|
||||
automatisiert vollständig geprüft; manueller Live-Test steht aus
|
||||
automatisiert und durch Pascal manuell vollständig abgenommen
|
||||
|
||||
## Offene Entscheidungen und Fehler
|
||||
|
||||
@@ -273,9 +283,9 @@ Abnahmestatus:
|
||||
|
||||
## Nächster sinnvoller Auftrag
|
||||
|
||||
Pascal führt anhand `docs/NEXT_SESSION.md` den echten Gemini-, Ask- und
|
||||
Never-Test mit ausschließlich künstlichen beziehungsweise unpersönlichen Daten
|
||||
durch. Bis zu seiner Bestätigung nicht nach `main` mergen.
|
||||
Nach dem Merge ein separates, kleines Paket für Chatkomfort und Robustheit
|
||||
planen, beispielsweise Sitzungstitel/Suche oder Streaming. Noch keine
|
||||
Obsidian-Integration, Tools, Sprache oder Serversteuerung beginnen.
|
||||
|
||||
Keinen API-Key in Chat, Git, Obsidian, Logs oder Dokumentation übernehmen.
|
||||
|
||||
|
||||
+15
-34
@@ -1,45 +1,26 @@
|
||||
# Nächste Sitzung
|
||||
|
||||
## Ziel
|
||||
## Ausgangsstand
|
||||
|
||||
Pascal nimmt den Gemini-Free-Provider und die lokalen Fallbacks manuell ab.
|
||||
Bis dahin bleibt `feat/gemini-privacy-router` ungemergt.
|
||||
Der Core-Chat und der Gemini-/Datenschutz-Meilenstein sind automatisiert durch
|
||||
Codex und am 30.07.2026 manuell durch Pascal abgenommen. Der Featurestand ist zum
|
||||
Merge freigegeben.
|
||||
|
||||
## Vorbereitung durch Pascal
|
||||
Bestätigt sind echter Gemini-Erfolg, verdeckte Key-Speicherung, Nullkostenmodus,
|
||||
bereinigter Status, Cloudzähler, `ask`- und `never`-Routing, erzwungener lokaler
|
||||
Modus sowie das Startskript.
|
||||
|
||||
1. In Google AI Studio einen neuen, nur für Javis verwendeten Gemini-Auth-Key
|
||||
erstellen.
|
||||
2. Für das zugehörige Google-Projekt kein Billing verbinden beziehungsweise den
|
||||
externen Billingstatus selbst prüfen.
|
||||
3. Den Key niemals in Chat, Codex, Git, Obsidian oder einen sichtbaren
|
||||
Kommandozeilenparameter kopieren.
|
||||
4. Im Repository ausführen:
|
||||
## Nächstes sinnvolles Paket
|
||||
|
||||
```powershell
|
||||
& 'D:\Javis-Tools\uv\uv.exe' run javis configure gemini
|
||||
& 'D:\Javis-Tools\uv\uv.exe' run javis secrets set gemini
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\start-javis.ps1
|
||||
```
|
||||
Nur ein kleines Chatkomfort-/Robustheitsthema auswählen, zum Beispiel:
|
||||
|
||||
Die erste Einrichtung verlangt zwei lokale Bestätigungen. Der zweite Befehl liest
|
||||
den Key verdeckt ein und speichert ihn im Windows Credential Manager.
|
||||
- automatische, lokal erzeugte Sitzungstitel und Sitzungssuche oder
|
||||
- Streaming lokaler und Cloudantworten mit sauberem Abbruchverhalten
|
||||
|
||||
## Manueller Test
|
||||
|
||||
1. `/status`: Key und Konfiguration `ja`, Nullkostenmodus `aktiv`.
|
||||
2. Unpersönlich: `Erkläre in zwei Sätzen, was eine SQLite-Transaktion ist.`
|
||||
3. `/privacy`: `allowed`, Provider `gemini`, kein Fallback.
|
||||
4. Ask ohne private Details: `Ich habe eine private Alltagssituation. Nenne nur
|
||||
allgemeine Möglichkeiten.` Bei der Freigabe Enter drücken; Antwort muss lokal
|
||||
über Ollama kommen.
|
||||
5. Never mit Kunstbeispiel: `Nur lokal: Der Beispiel-API-Key ist absichtlich
|
||||
erfunden.` Es darf keine Cloudfreigabe angeboten werden; Antwort bleibt lokal.
|
||||
6. `/status`: bereinigte Zähler prüfen, danach `/exit`.
|
||||
|
||||
Erst wenn Pascal diese Punkte bestätigt, darf über den Merge nach `main`
|
||||
entschieden werden. Google-Quoten sind projektabhängig und in AI Studio zu prüfen.
|
||||
Vorher wie immer Branch, Arbeitsbaum, aktuellen Handoff und Tests prüfen.
|
||||
|
||||
## Weiterhin nicht beginnen
|
||||
|
||||
Kein Obsidian-Zugriff, SSH-/Serverzugriff, Sprache, Mobile Client, Tools oder
|
||||
Living Mind. Keine echten persönlichen Daten für Cloudtests verwenden.
|
||||
Noch keine Obsidian-Integration, Tools, SSH-/Serversteuerung, Sprache, Mobile
|
||||
Client oder Living Mind. Der vorhandene Gemini-Key bleibt im Betriebssystem-
|
||||
Keyring und darf nicht erneut in Chat, Logs oder Dokumentation erscheinen.
|
||||
|
||||
+14
-8
@@ -33,8 +33,10 @@ Stand: Gemini-Free-Feature automatisiert geprüft, 30.07.2026.
|
||||
- Core-Chat automatisiert durch Codex getestet: ja
|
||||
- Core-Chat manuell durch Pascal getestet: ja, am 30.07.2026
|
||||
- Gemini/Datenschutz/Fallback automatisiert durch Codex getestet: ja
|
||||
- Gemini/Datenschutz/Fallback manuell durch Pascal getestet: noch nicht
|
||||
- von Pascal für produktive Verwendung freigegeben: noch nicht ausdrücklich erklärt
|
||||
- Gemini/Datenschutz/Fallback manuell durch Pascal getestet: ja, am 30.07.2026
|
||||
- für Merge freigegeben: ja, nach den zwei kleinen Korrekturen
|
||||
- für produktive Nutzung freigegeben: nicht separat erklärt; keine spätere Tool-,
|
||||
Obsidian- oder Autonomiefunktion
|
||||
|
||||
## Aktuelles Featurepaket
|
||||
|
||||
@@ -60,9 +62,12 @@ Stand: Gemini-Free-Feature automatisiert geprüft, 30.07.2026.
|
||||
- `javis configure gemini` mit doppelter ausdrücklicher Bestätigung
|
||||
- echter Chat auf Hybridprovider verdrahtet; Key wird verzögert geladen
|
||||
- `/provider`, `/privacy` und `/status` geben keine Inhalte oder Keywerte aus
|
||||
- allgemeine unpersönliche Technikbegriffe werden gezielter `allowed`, während
|
||||
persönliche, interne oder geheime technische Inhalte `ask`/`never` bleiben
|
||||
- `/privacy` trennt lokalen Modus von einem technischen Provider-Fallback
|
||||
- sicheres `scripts/start-javis.ps1` mit relativer Repositoryprüfung,
|
||||
Loopback-Ollama und überschreibbaren externen Pfaden
|
||||
- 62 Unit-Tests aktuell erfolgreich; PowerShell-Syntax, Lockfile, Sync,
|
||||
- 66 Unit-Tests aktuell erfolgreich; PowerShell-Syntax, Lockfile, Sync,
|
||||
Strukturprüfung, Secret-Scan, Gitignore, Ruff-Format und Lint sauber
|
||||
|
||||
## Nicht implementiert
|
||||
@@ -74,6 +79,7 @@ Living-Mind-Frontend.
|
||||
|
||||
- stabiler Branch: `main`
|
||||
- aktueller Branch: `feat/gemini-privacy-router`
|
||||
- Feature-Branch: zu `origin/feat/gemini-privacy-router` gepusht
|
||||
- Grundgerüstcommit: `6d04171`
|
||||
- Basis des Feature-Branches: `e6eb995`
|
||||
- Toolchain: `fa90a5c`
|
||||
@@ -85,9 +91,9 @@ Living-Mind-Frontend.
|
||||
|
||||
## Bekannte Einschränkung
|
||||
|
||||
Ollama wird bewusst manuell in einem eigenen Terminal gestartet. Antworten werden
|
||||
noch nicht gestreamt; Sitzungen haben noch keine Titel, Suche oder Löschfunktion.
|
||||
Das Startskript startet Ollama bei Bedarf lokal. Antworten werden noch nicht
|
||||
gestreamt; Sitzungen haben noch keine Titel, Suche oder Löschfunktion.
|
||||
`qwen3:8b` ist nicht automatisch für die GTX 1050 des späteren Zielhosts geeignet.
|
||||
Der echte Gemini-/Fallback-Test durch Pascal und danach die Mergeentscheidung
|
||||
fehlen. Es wurde kein echter API-Key angefordert, angezeigt oder konfiguriert und
|
||||
keine Cloudanfrage ausgeführt.
|
||||
Der Gemini-Key liegt ausschließlich im Betriebssystem-Keyring; sein Wert wurde
|
||||
von Codex weder gelesen noch ausgegeben. Pascal hat den echten Cloud- und
|
||||
Fallbackablauf erfolgreich geprüft.
|
||||
|
||||
@@ -26,6 +26,8 @@ class RouteStatus:
|
||||
provider: str
|
||||
privacy_policy: CloudPolicy
|
||||
fallback: bool
|
||||
cloud_suppressed_by_local_mode: bool
|
||||
technical_fallback: bool
|
||||
reason: str
|
||||
|
||||
|
||||
@@ -94,7 +96,13 @@ class HybridProvider:
|
||||
)
|
||||
|
||||
if self.mode == "local":
|
||||
return self._local(messages, decision, "lokaler Modus", fallback=False)
|
||||
return self._local(
|
||||
messages,
|
||||
decision,
|
||||
"lokaler Modus",
|
||||
fallback=False,
|
||||
cloud_suppressed_by_local_mode=True,
|
||||
)
|
||||
if decision.policy is CloudPolicy.NEVER:
|
||||
return self._local(messages, decision, decision.reason, fallback=True)
|
||||
|
||||
@@ -148,10 +156,12 @@ class HybridProvider:
|
||||
)
|
||||
)
|
||||
self.last_route = RouteStatus(
|
||||
cloud_provider.name,
|
||||
decision.policy,
|
||||
False,
|
||||
"Cloudaufruf erfolgreich",
|
||||
provider=cloud_provider.name,
|
||||
privacy_policy=decision.policy,
|
||||
fallback=False,
|
||||
cloud_suppressed_by_local_mode=False,
|
||||
technical_fallback=False,
|
||||
reason="Cloudaufruf erfolgreich",
|
||||
)
|
||||
return response
|
||||
|
||||
@@ -192,7 +202,13 @@ class HybridProvider:
|
||||
reason = "Gemini nicht verfügbar"
|
||||
if disable_notice:
|
||||
reason = "Gemini-Schlüssel abgelehnt; Cloud für diese Sitzung deaktiviert"
|
||||
return self._local(messages, decision, reason, fallback=True)
|
||||
return self._local(
|
||||
messages,
|
||||
decision,
|
||||
reason,
|
||||
fallback=True,
|
||||
technical_fallback=True,
|
||||
)
|
||||
|
||||
def _local(
|
||||
self,
|
||||
@@ -201,6 +217,8 @@ class HybridProvider:
|
||||
reason: str,
|
||||
*,
|
||||
fallback: bool,
|
||||
cloud_suppressed_by_local_mode: bool = False,
|
||||
technical_fallback: bool = False,
|
||||
) -> str:
|
||||
if fallback:
|
||||
self._notice_callback(f"{reason} – lokale Antwort mit Ollama.")
|
||||
@@ -216,10 +234,12 @@ class HybridProvider:
|
||||
)
|
||||
)
|
||||
self.last_route = RouteStatus(
|
||||
self.local_provider.name,
|
||||
decision.policy,
|
||||
fallback,
|
||||
reason,
|
||||
provider=self.local_provider.name,
|
||||
privacy_policy=decision.policy,
|
||||
fallback=fallback,
|
||||
cloud_suppressed_by_local_mode=cloud_suppressed_by_local_mode,
|
||||
technical_fallback=technical_fallback,
|
||||
reason=reason,
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
@@ -227,9 +227,11 @@ def run_chat(
|
||||
output("Noch keine Datenschutzentscheidung in dieser Sitzung.")
|
||||
else:
|
||||
output(
|
||||
"Letzte Datenschutzentscheidung: "
|
||||
"Datenschutzentscheidung: "
|
||||
f"{route.privacy_policy.value}; Provider: {route.provider}; "
|
||||
f"Fallback: {'ja' if route.fallback else 'nein'}"
|
||||
"Cloud durch Modus local unterdrückt: "
|
||||
f"{'ja' if route.cloud_suppressed_by_local_mode else 'nein'}; "
|
||||
f"Technischer Fallback: {'ja' if route.technical_fallback else 'nein'}"
|
||||
)
|
||||
continue
|
||||
if entered == "/status":
|
||||
|
||||
@@ -75,7 +75,8 @@ _ASK_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
|
||||
),
|
||||
(
|
||||
re.compile(
|
||||
r"(?i)\b(intern(?:e[rsn]?|er)? hostname|interne? domain|rootserver|"
|
||||
r"(?i)\b(intern(?:e|er|es|en)?\s+"
|
||||
r"(?:hostname|server|domain|infrastruktur)|rootserver|"
|
||||
r"privates? projekt|nicht öffentlich|systeminformation|"
|
||||
r"private[rs]? termin)\b"
|
||||
),
|
||||
@@ -90,7 +91,9 @@ _ASK_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
|
||||
_ALLOWED_PATTERNS: tuple[re.Pattern[str], ...] = (
|
||||
re.compile(
|
||||
r"(?i)\b(python|programmier|quellcode|algorithmus|sqlite|linux|windows|"
|
||||
r"öffentliche dokumentation|allgemeine wissensfrage|was ist|wie funktioniert)\b"
|
||||
r"git|ram|arbeitsspeicher|ssd|solid state drive|http|statuscode|"
|
||||
r"transaktion|öffentliche dokumentation|allgemeine wissensfrage|"
|
||||
r"was ist|wie funktioniert)\b"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
+10
-2
@@ -70,7 +70,8 @@ class CliTests(unittest.TestCase):
|
||||
provider.last_route = SimpleNamespace(
|
||||
privacy_policy=CloudPolicy.ASK,
|
||||
provider="ollama",
|
||||
fallback=True,
|
||||
cloud_suppressed_by_local_mode=True,
|
||||
technical_fallback=False,
|
||||
)
|
||||
service = ChatService(store, provider)
|
||||
inputs = iter(
|
||||
@@ -97,7 +98,14 @@ class CliTests(unittest.TestCase):
|
||||
self.assertEqual(result, 0)
|
||||
self.assertIn("Aktiver Providermodus: auto", output)
|
||||
self.assertIn("Providermodus: local", output)
|
||||
self.assertTrue(any("ask; Provider: ollama; Fallback: ja" in line for line in output))
|
||||
self.assertTrue(
|
||||
any(
|
||||
"Datenschutzentscheidung: ask; Provider: ollama; "
|
||||
"Cloud durch Modus local unterdrückt: ja; "
|
||||
"Technischer Fallback: nein" in line
|
||||
for line in output
|
||||
)
|
||||
)
|
||||
self.assertIn("Gemini-Schlüssel vorhanden: ja", output)
|
||||
|
||||
def test_gemini_configuration_defaults_to_no(self) -> None:
|
||||
|
||||
@@ -9,11 +9,34 @@ class PrivacyRouterTests(unittest.TestCase):
|
||||
self.router = PrivacyRouter()
|
||||
|
||||
def test_general_technical_question_is_allowed(self) -> None:
|
||||
for question in (
|
||||
"Erkläre in zwei Sätzen den Unterschied zwischen RAM und SSD.",
|
||||
"Wie funktioniert eine SQLite-Transaktion?",
|
||||
"Was bedeutet HTTP 404?",
|
||||
"Wie funktioniert Git?",
|
||||
):
|
||||
with self.subTest(question=question):
|
||||
self.assertEqual(
|
||||
self.router.classify("Wie funktioniert SQLite in Python?").policy,
|
||||
self.router.classify(question).policy,
|
||||
CloudPolicy.ALLOWED,
|
||||
)
|
||||
|
||||
def test_internal_server_question_requires_approval(self) -> None:
|
||||
decision = self.router.classify(
|
||||
"Wie konfiguriere ich den internen Server mit Hostname srv-intern-01?"
|
||||
)
|
||||
self.assertEqual(decision.policy, CloudPolicy.ASK)
|
||||
|
||||
def test_technical_question_with_api_key_is_never(self) -> None:
|
||||
decision = self.router.classify("Wie nutze ich diesen API-Key in Python: AIza" + "x" * 25)
|
||||
self.assertEqual(decision.policy, CloudPolicy.NEVER)
|
||||
|
||||
def test_personal_technical_situation_requires_approval(self) -> None:
|
||||
decision = self.router.classify(
|
||||
"Meine SSD ist voll. Wie verschiebe ich meine persönlichen Dateien?"
|
||||
)
|
||||
self.assertEqual(decision.policy, CloudPolicy.ASK)
|
||||
|
||||
def test_personal_information_requires_approval(self) -> None:
|
||||
self.assertEqual(
|
||||
self.router.classify("Ich möchte meinen Alltag besser planen.").policy,
|
||||
|
||||
@@ -253,6 +253,18 @@ class HybridProviderTests(unittest.TestCase):
|
||||
self.assertEqual(answer, "Lokal")
|
||||
self.assertFalse(self.approvals)
|
||||
self.assertFalse(self.cloud.calls)
|
||||
self.assertFalse(router.last_route.fallback)
|
||||
self.assertTrue(router.last_route.cloud_suppressed_by_local_mode)
|
||||
self.assertFalse(router.last_route.technical_fallback)
|
||||
|
||||
def test_network_failure_is_reported_as_technical_fallback(self) -> None:
|
||||
self.cloud.error = CloudNetworkError("offline")
|
||||
router = self._router()
|
||||
|
||||
router.chat([ChatMessage("user", "Wie funktioniert Python?")])
|
||||
|
||||
self.assertFalse(router.last_route.cloud_suppressed_by_local_mode)
|
||||
self.assertTrue(router.last_route.technical_fallback)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user