OpenCode session archiválás frontenddel

Session archive hero

Ha nap mint nap AI-kódoló asszisztensekkel dolgozol, ismered a problémát: a beszélgetési napló — minden munkamenet, minden prompt, minden eszközhívás — csendben több gigabájtos adatbázissá növekszik. A keresés lassúvá válik, a biztonsági mentések örökké tartanak, és amire valójában emlékezni akarsz (az a javítás egy trükkös hibához, az a refaktor-minta, az az egy parancs) ezer másik sor alá temetődik.

Ez a cikk egy olyan példát mutat be, amelyet élesben is használunk: egy önüzemeltetett archívumrendszert, amely az opencode régi munkameneteket kimásolja az élő adatbázisból egy külön, kompakt, teljes szövegű kereshető archívumba — majd egy gyors webes felületen teszi elérhetővé, amit az egész csapat kereshet, egyszeri bejelentkezéssel (SSO) védve.

A legjobb az egészben: az egész unalmas, bevált építőelemekből áll — SQLite, FTS5, egy kis Python-szkript, cron és egy vékony webes réteg. Nincs Elasticsearch-fürt, nincs felügyelt keresőszolgáltatás, nincs szállítói bezártság.

Megmutatom, hogyan építheted meg.


A probléma: az élő adatbázis nem könyvtár

Rendszerarchitektúra

Az AI-kódoló asszisztensek jellemzően egy helyi adatbázisban tárolják a beszélgetési előzményeket. Hónapokig tartó napi használat után az adatbázis felhalmoz:

  • Munkameneteket (session) — egyet minden beszélgetéshez
  • Üzeneteket — a párbeszédet közted és az asszisztens között
  • Részeket (part) — üzenetszegmenseket (szöveg, eszközhívás, kódblokk)
  • Eseményeket — belső telemetriai sorokat, amelyek a felfúvódás oroszlánrészét adják

A számok gyorsan összeadódnak. Egy év napi munka könnyedén több tízezer munkamenetet és egymilliónál több eseménysort eredményez. Az élő adatbázis gigabájtosra nő, és minden lekérdezés — beleértve azokat is, amelyeket maga az asszisztens futtat — egyre lassabb lesz.

A kulcsfelismerés: az élő adatbázis munkahely, nem könyvtár. Annak kell benne maradnia, ami friss és aktívan használt. A régi munkamenetek az archívumba tartoznak — oda, ahol kompakt, indexelt, kereshető és örökre olcsón megtartható.


Az architektúra egy pillantásra

A terv három logikai részből áll:

  1. Az élő rendszer — a termelési adatbázis, kicsiben és gyorsan tartva.
  2. Az éjszakai export — egy cron-feladat, amely a régi munkameneteket az archívumba másolja, kitisztítja, és opcionálisan megnyesegeti az élő adatbázist.
  3. Az archívum — egy csak olvasható SQLite-adatbázis teljes szövegű indexszel, amelyet egy kisméretű webalkalmazás szolgál ki csevegés-stílusú felülettel.
Éjszakai folyamat

Ez a szétválasztás négy, élesben fontos tulajdonságot ad:

TulajdonságHogyan érjük el
Az élő adatbázis gyors maradA régi munkameneteket éjszakánként (vagy ütemezve) eltávolítjuk
Semmi sem vész el sohaA nyesés csak olyan munkameneteket töröl, amelyek már az archívumban vannak
A keresés azonnaliFTS5 teljes szövegű index az archívumon
Az archívum biztonságosan megoszthatóA titkokat már azelőtt kitakarjuk, hogy elhagynák az élő adatbázist

Az éjszakai export-feladat

Az élő adatbázis biztonságos olvasása

Az első szabály: soha ne zárold le az élő adatbázist. Ha az alkalmazás WAL módban (write-ahead logging) használja az SQLite-ot, külön folyamatból csak olvasásra nyithatod meg az adatbázist, és olvashatsz, miközben az alkalmazás ír. Nincs állásidő, nincs blokkolás.

import sqlite3

LIVE_DB = "file:/path/to/live.db?mode=ro"  # csak olvasható kapcsolat
conn = sqlite3.connect(LIVE_DB, uri=True)

A vízjel: növekményes, idempotens, újraindításbiztos

Minden éjjel mindent kimásolni pazarlás. Ehelyett vízjelet követünk nyomon: a legutóbb exportált munkamenet időbélyegét. Minden futáskor csak a vízjelnél újabb munkameneteket exportálunk.

def get_watermark(archive_conn):
    row = archive_conn.execute(
        "SELECT value FROM archive_meta WHERE key = 'watermark'"
    ).fetchone()
    return row[0] if row else 0

def set_watermark(archive_conn, ts):
    archive_conn.execute(
        "INSERT OR REPLACE INTO archive_meta (key, value) VALUES ('watermark', ?)",
        (ts,),
    )

Két részlet teszi ezt robusztussá:

  • Idempotens írásokINSERT OR REPLACE-t használunk, így a futás közben módosult munkamenet újraexportálása egyszerűen felülírja a régit. A feladat kétszeri futtatása mindig biztonságos.
  • Hibazáró vízjel — a vízjel csak egy sikeresen befejezett köteg után lép tovább. Ha egy köteg félúton meghiúsul, semmi sem marad ki; a következő futás újrapróbálja.
def export_new_sessions(live_conn, archive_conn, watermark):
    cursor = live_conn.execute(
        """
        SELECT id, title, created_at, updated_at
        FROM session
        WHERE updated_at > ?
        ORDER BY updated_at
        """,
        (watermark,),
    )
    for batch in iter_batches(cursor, size=500):
        for session in batch:
            export_session(live_conn, archive_conn, session)
        archive_conn.execute("BEGIN")
        for session in batch:
            upsert_session(archive_conn, session)
        new_watermark = batch[-1]["updated_at"]
        set_watermark(archive_conn, new_watermark)
        archive_conn.commit()  # a vízjel csak siker esetén lép tovább

Kitakarás: adatvédelem tervezéssel

Itt rontják el a legtöbb archívumot. Ha a beszélgetéseket szó szerint másolod, akkor API-kulcsokat, jelszavakat, tokeneket és titkokat is másolsz, amelyek felbukkantak a beszélgetésben — és olyan helyre teszed őket, amelyet megoszthatsz a csapattal, biztonsági mentésbe kerülhet, vagy indexelhető.

A mi szabályunk: érzékeny adat soha nem kerül be az archívumba. Minden exportált sort egy kulcstudatos tisztítóval vizsgálunk, amely felismeri és eltávolítja a titkos mintákat:

import re

SECRET_PATTERNS = [
    re.compile(r"(?i)(password|passwd|pwd|token|secret|api[_-]?key|private[_-]?key)\s*[=:]\s*\S+"),
    re.compile(r"sk-[A-Za-z0-9]{20,}"),          # API-kulcs előtagok
    re.compile(r"(?i)(bearer|basic)\s+[A-Za-z0-9._~+/=-]+"),
    re.compile(r"AKIA[0-9A-Z]{16}"),             # felhő-hozzáférési kulcsok
]

def redact_text(text: str) -> str:
    for pattern in SECRET_PATTERNS:
        text = pattern.sub("[kitakarva]", text)
    return text

Két további döntés, ami sokat számít:

  1. Az eszközbemenetek és -kimenetek teljesen kimaradnak. A mi sémánkban az eszközhívás-sorok csak az eszköz nevét és egy csonkolt, kitakart bemeneti részletet őrzik. A teljes bemenet/kimenet — ahol a titkok rejtőznek — soha nem hagyja el az élő adatbázist. Ez tudatos kompromisszum: lemondasz az eszközhívás visszajátszásáról, de lemondasz a hitelesítő adatok kiszivárgásának lehetőségéről is.
  2. Hibazáró tisztítók. Ha a kitakarás valami szokatlanba ütközik (mélyen beágyazott JSON, váratlan típusok), biztonságos irányba hibázik: a tartalom kimarad, nem megy át nyersen.

Az archívum sémája

Az archívum-adatbázis kicsi, jól meghatározott és könnyen átlátható. Csak a fontos táblákat tükrözi, egy fontos kiegészítéssel: egy keresési tartalomtáblát, amely minden munkamenetet egyetlen kereshető sorba aggregál.

-- A kereshető tartalom: soronként egy munkamenet
CREATE TABLE session_search (
    id            INTEGER PRIMARY KEY,
    title         TEXT,
    agent         TEXT,
    model         TEXT,
    body          TEXT,          -- az összes üzenet összefűzve új sorokkal
    time_updated  INTEGER,
    message_count INTEGER
);

-- Metaadatok (vízjel stb.)
CREATE TABLE archive_meta (
    key   TEXT PRIMARY KEY,
    value TEXT
);
FTS5 séma


Azonnali keresés SQLite FTS5-tel

Itt válik az archívum igazán hasznossá. Az SQLite beépített FTS5-tel rendelkezik, egy teljes szövegű keresőmotorral, amely több tízezer dokumentumra is elég gyors, és nem igényel külső szolgáltatást.

Külső tartalmú táblák: kis index, azonnali lekérdezések

Az okos trükk a külső tartalmú (external-content) FTS5-tábla. Ahelyett, hogy a tartalmat megdupláznánk az indexben, az FTS-tábla csak az invertált indexet tárolja, és szükség esetén a fő táblából húzza be a tartalmat. Az index kicsi marad, az írások olcsók.

CREATE VIRTUAL TABLE session_search_fts USING fts5(
    title, agent, model, body,
    content='session_search',          -- külső tartalomtábla
    content_rowid='id'
);

A keresés egyszerű MATCH-lekérdezés:

SELECT s.id, s.title, s.body, s.time_updated
FROM session_search s
JOIN session_search_fts f ON f.rowid = s.id
WHERE session_search_fts MATCH 'sqlite fts5 optimization'
ORDER BY rank
LIMIT 20;

Az index szinkronban tartása triggerekkel

Mivel az FTS-tábla külső tartalmú, a szinkronizálást magadnak kell elvégezned. A bevált módszer a tartalomtáblán lévő triggerek:

CREATE TRIGGER session_search_ai AFTER INSERT ON session_search BEGIN
  INSERT INTO session_search_fts(rowid, title, agent, model, body)
  VALUES (new.id, new.title, new.agent, new.model, new.body);
END;

CREATE TRIGGER session_search_ad AFTER DELETE ON session_search BEGIN
  INSERT INTO session_search_fts(session_search_fts, rowid, title, agent, model, body)
  VALUES ('delete', old.id, old.title, old.agent, old.model, old.body);
END;

CREATE TRIGGER session_search_au AFTER UPDATE ON session_search BEGIN
  INSERT INTO session_search_fts(session_search_fts, rowid, title, agent, model, body)
  VALUES ('delete', old.id, old.title, old.agent, old.model, old.body);
  INSERT INTO session_search_fts(rowid, title, agent, model, body)
  VALUES (new.id, new.title, new.agent, new.model, new.body);
END;

Az optimalizálás, ami mindent megváltoztatott

Van egy FTS5-tel kapcsolatos buktató, ami rengeteg hibakeresésünkbe került: a töredezettség. Ahogy sorokat szúrsz be és törölsz, az FTS-index sok kis szegmenst halmoz fel. A lekérdezések — főleg a COUNT(*) — ezredmásodpercekről másodpercekre romlanak.

A megoldás egyetlen parancs, minden export után:

INSERT INTO session_search_fts(session_search_fts) VALUES ('optimize');

A mi esetünkben ez a COUNT(*)-ot az indexen 20 másodpercről 3 ezredmásodpercre vitte le. Ha a keresőfelületed valaha lassúnak tűnik, először ezt futtasd.


Az élő adatbázis nyesése (óvatosan)

A második fázis opcionális, de ajánlott: a régi munkamenetek törlése az élő adatbázisból, hogy az kicsi maradjon. A biztonsági szabály abszolút:

Soha ne törölj olyan munkamenetet, amely nincs már az archívumban.

És egy gyakorlati szabály: életkorától függetlenül mindig tartsd meg a legutóbbi N munkamenetet. Senki aktív beszélgetését nem szabad félbeszakítani.

def prune_sessions(live_conn, archive_conn, keep_days=30, floor=30):
    now = int(time.time())
    cutoff = now - keep_days * 86400
    # Azok a munkamenetek, amelyek RÉGIEK ÉS archívumban vannak
    cursor = live_conn.execute(
        """
        SELECT s.id FROM session s
        WHERE s.updated_at < ?
          AND s.id IN (SELECT id FROM archive.session)   -- archívumban?
        ORDER BY s.updated_at DESC
        LIMIT -1 OFFSET ?                                 -- a padló megmarad
        """,
        (cutoff, floor),
    )
    ids = [row[0] for row in cursor]
    delete_sessions(live_conn, ids)  # kötegenként, FK kaszkáddal

A tényleges törlésnek tiszteletben kell tartania a külső kulcsokat — a munkamenet törlése kaszkádoljon az üzeneteire és részeire, különben árva sorokat hagysz hátra, és az adatbázis valójában nem is zsugorodik.

Tipp: ha az alkalmazás táblái nem deklarálják a FOREIGN KEY ... ON DELETE CASCADE szabályt, töröld a gyermeksorokat explicit módon (üzenet → rész → munkamenet), vagy használj egyetlen tranzakciót PRAGMA foreign_keys = ON kapcsolóval.


A webes réteg: Datasette + csevegés-stílusú SPA

Miért Datasette?

Az archívum kiszolgálásához a Datasette-t használjuk — egy kisméretű Python-webalkalmazást, amely az SQLite-adatbázisokat böngészhető, lekérdezhető weboldalakká változtatja, ingyenes JSON API-val. Ezt adja:

  • Böngészős felületet a táblákhoz (nagyszerű hibakereséshez)
  • JSON API-t (/db.json, ?sql=...&_shape=array) a frontendhez
  • Bővítményrendszert a hitelesítéshez, átirányításokhoz és egyedi viselkedéshez
  • Csak olvasható kiszolgálást konfigurálható lekérdezési időkorlátokkal

Csevegés-stílusú frontend (a rész, amit a felhasználók látnak)

A nyers adatbázis-felület adminoknak jó, csapatnak nem. Egy kisméretű egyoldalas alkalmazást építettünk, amely magának az asszisztensnek a csevegésélményét utánozza:

  • Munkamenet-nézet — munkamenetkártyák lapozható listája (cím, ügynök, modell, utolsó frissítés, üzenetszám).
  • Keresés — teljes szövegű keresőmező; az eredmények kiemelt részletekkel mutatják a munkameneteket.
  • Beszélgetés-nézet — kattints egy munkamenetre, és olvasd úgy, mint egy csevegést: a felhasználó üzenetei jobbra, az asszisztensé balra, összecsukható gondolkodás, eszközhívás-kártyák.
  • Ugrás a találatra — amikor keresési eredményből nyitsz meg egy munkamenetet, az oldal pontosan a találatot tartalmazó üzenethez görget, és felvillantja azt.
Keresési felület

A frontend kizárólag a JSON API-val beszél:

async function search(query, offset = 0) {
  const sql = `
    SELECT s.id, s.title, s.agent, s.model, s.time_updated, s.message_count,
           snippet(session_search_fts, 5, '[', ']', '…', 12) AS snippet
    FROM session_search s
    JOIN session_search_fts ON session_search_fts.rowid = s.id
    WHERE session_search_fts MATCH :q
    ORDER BY rank LIMIT 20 OFFSET :offset`;
  const params = new URLSearchParams({ sql, q: query, _shape: 'array', offset });
  const res = await fetch(`/api/database.json?${params}`);
  return res.json();
}

Megjegyzés a teljesítményről: a Datasette (és a legtöbb SQLite-webes réteg) lehetővé teszi a sql_time_limit_ms és gyorsítótár-beállítások megadását. Nagy FTS-indexnél adj bőkezű lekérdezési időkorlátot (pl. 20 mp), hogy a hideg gyorsítótárú lekérdezések ne 504-es hibát adjanak.


Egyszeri bejelentkezés: OIDC a proxy megérintése nélkül

SSO-folyamat

Az archívum érzékeny előzményeket tartalmazhat, ezért az anonim hozzáférést tiltani kell, a csapattagok pedig a meglévő céges fiókjukkal jelentkezzenek be. A tiszta megoldás az OIDC a saját identitásszolgáltatód ellen (Authentik, Keycloak, Okta, Entra ID — bármelyik OpenID Connect szolgáltató).

Két bevett megközelítés létezik:

  1. Fordított proxy-szintű hitelesítés — a proxy (nginx, HAProxy, Traefik) kezeli az OIDC-t, és fejlécekben adja át az azonosságot. Erőteljes, de módosítja a proxy konfigurációját, és ütközhet más útvonalakkal.
  2. Alkalmazásszintű OIDC-bővítmény — maga az alkalmazás valósítja meg az OIDC-klienst. Egyáltalán nincs proxy-módosítás. Mi ezt választottuk.

A Datasette-hez nem létezett karbantartott OIDC-bővítmény, ezért írtunk egy kicsit (~150 sor), a jól ismert datasette-auth0 bővítmény mintájára (ugyanazt a mintát használja a datasette-auth-osm projekt is). Azért ilyen kicsi, mert a platform saját primitívjeit használja újra:

  • datasette.sign() a cookie-k és az OIDC-állapotparaméter aláírásához
  • datasette.set_cookie() httponly, secure, samesite="lax" beállításokkal
  • a szabványos actor_from_request horog a bejelentkezett szereplő beállításához

Az OIDC-folyamat 100 sorban

import secrets
import httpx
from datasette.utils.asgi import Response

AUTH_STATE_COOKIE = "auth-state"

async def start_login(datasette, request):
    # 1. Véletlen állapot generálása, cookie-ba aláírva
    state = secrets.token_hex(16)
    signed = datasette.sign({"state": state}, namespace="authentik-oidc")
    response = Response.redirect(authorization_url(state))
    response.set_cookie(
        AUTH_STATE_COOKIE,
        signed,
        httponly=True,
        secure=True,
        samesite="lax",
    )
    return response

async def callback(datasette, request):
    # 2. Állapot ellenőrzése (állandó idejű!), kód becserélése tokenekre
    expected = datasette.unsign(request.cookies.get(AUTH_STATE_COOKIE),
                                namespace="authentik-oidc")
    if not expected or not secrets.compare_digest(
        expected["state"], request.args.get("state", "")
    ):
        return Response.text("Érvénytelen állapot", status=400)

    code = request.args.get("code")
    token_resp = await _exchange_code(datasette, code)
    profile = await _fetch_profile(datasette, token_resp["access_token"])

    # 3. Aláírt szereplő-cookie beállítása — a felhasználó bejelentkezett
    actor = {"id": f"authentik-{profile['sub']}", "name": profile.get("name")}
    response = Response.redirect("/")
    response.set_cookie(
        "ds_actor",
        datasette.sign({"a": actor, "e": expiry}, namespace="actor"),
        httponly=True, secure=True, samesite="lax",
    )
    return response

A három, igazán fontos biztonsági részlet:

RészletMiért
Aláírt állapotparaméterBejelentkezési CSRF megelőzése — a támadó nem tud hamis visszahívást kovácsolni
secrets.compare_digestÁllandó idejű összehasonlítás, nincs időzítési mellékcsatorna
HttpOnly + Secure + SameSite=Lax cookie-kNincs JavaScript-hozzáférés, csak HTTPS, CSRF-ellenálló

A végpont-buktata, ami egy délutánunkba került

A különböző identitásszolgáltatók más-más elérési úton teszik elérhetővé az OIDC-végpontjaikat. A mi szolgáltatónk az authorize, token és userinfo végpontokat az alkalmazás-slug nélkül szolgálta ki — a JWKS (JSON Web Key Set) URL-t viszont vele együtt. A naiv megvalósítás, amely a végpontokat az issuer URL szöveges összefűzésével vezeti le, eltörik.

A robusztus megközelítés: minden végpontot a felfedezési dokumentumból (.well-known/openid-configuration) származtass indításkor, és soha ne tippelj:

async def discover(config):
    async with httpx.AsyncClient(timeout=10) as client:
        r = await client.get(config["issuer"].rstrip("/") + "/.well-known/openid-configuration")
        r.raise_for_status()
        doc = r.json()
    return {
        "authorization_endpoint": doc["authorization_endpoint"],
        "token_endpoint": doc["token_endpoint"],
        "userinfo_endpoint": doc["userinfo_endpoint"],
    }

Csoporttal korlátozott hozzáférés

Az egyszeri bejelentkezés önmagában még nem elég — jogosultságszabályozás is kell. Az identitásszolgáltató gondoskodjon róla, hogy csak egy adott csoport tagjai jelentkezhessenek be az archívumba:

  • Hozz létre egy csoportot (pl. archive-users) az identitásszolgáltatóban.
  • Kösd az alkalmazás hozzáférési szabályzatát ehhez a csoporthoz.
  • A felhasználókat a csoporthoz adod/veszed el — kódváltoztatás, újratelepítés nélkül.

A mi beállításunkban az alkalmazás engedélyezési szabálya egyszerűen „bármely hitelesített szereplő beléphet” — a szolgáltató dönti el, hogy ki hitelesülhet. Így az alkalmazás ostoba marad, a szabályzat pedig központosított.


Minden együtt

A cron-feladat

Egyetlen cron-bejegyzés hajtja az egész folyamatot:

# Archívum-export + nyesés — éjszakánként 02:00-kor
0 2 * * * root /opt/archive/run.sh >> /var/log/archive.log 2>&1

A run.sh pedig szándékosan unalmas:

#!/bin/bash
set -euo pipefail
python3 /opt/archive/archive_sessions.py        # növekményes export + FTS-újraépítés
python3 /opt/archive/archive_sessions.py --prune  # régi, archivált munkamenetek törlése

A teljes séma, egy fájlban

Íme a teljes archívum-indító SQL — másold ki, igazítsd az oszlopokat, és fut is:

PRAGMA journal_mode = WAL;

CREATE TABLE IF NOT EXISTS session (
    id          INTEGER PRIMARY KEY,
    title       TEXT,
    agent       TEXT,
    model       TEXT,
    created_at  INTEGER,
    updated_at  INTEGER
);

CREATE TABLE IF NOT EXISTS message (
    id          INTEGER PRIMARY KEY,
    session_id  INTEGER NOT NULL REFERENCES session(id) ON DELETE CASCADE,
    role        TEXT,           -- 'user' | 'assistant' | 'tool'
    content     TEXT,
    created_at  INTEGER
);

CREATE TABLE IF NOT EXISTS session_search (
    id            INTEGER PRIMARY KEY,
    title         TEXT,
    agent         TEXT,
    model         TEXT,
    body          TEXT,
    time_updated  INTEGER,
    message_count INTEGER
);

CREATE TABLE IF NOT EXISTS archive_meta (
    key   TEXT PRIMARY KEY,
    value TEXT
);

CREATE VIRTUAL TABLE IF NOT EXISTS session_search_fts USING fts5(
    title, agent, model, body,
    content='session_search',
    content_rowid='id'
);

-- (add ide a 4. fejezet három szinkronizáló triggerét)

A teljes export-folyamat, elejétől a végéig

def run_export(live_path, archive_path):
    live = sqlite3.connect(f"file:{live_path}?mode=ro", uri=True)
    archive = sqlite3.connect(archive_path)

    watermark = get_watermark(archive)
    for batch in fetch_new_sessions(live, watermark):
        for s in batch:
            scrub_and_export(live, archive, s)   # a kitakarás itt történik
        rebuild_search_rows(archive)             # session_search frissítése
        archive.commit()
        set_watermark(archive, batch[-1]["updated_at"])
        archive.commit()

    archive.execute("INSERT INTO session_search_fts(session_search_fts) VALUES ('optimize')")
    archive.commit()
    print("Export kész.")

Tanulságok

Ha valami hasonlót építesz, íme a tanulságok, amelyeket bárcsak az első naptól tudtunk volna:

  1. Használj WAL módot mindenhol. A csak olvasható archívumok és az egyidejű írók problémája teljesen megszűnik.
  2. INSERT OR REPLACE + vízjel = ingyenes idempotencia. A feladat újrafuttatása mindig biztonságos; ettől a cron-hibák unalmassá válnak ijesztő helyett.
  3. Futtasd az 'optimize' parancsot az FTS-indexen minden export után. A töredezettség a keresési teljesítmény néma gyilkosa. A mi COUNT(*)-unk 20 mp-ről 3 ms-ra ment le.
  4. Takarj ki tárolás előtt, ne utána. Az a titok, amely soha nem kerül be az archívumba, onnan ki sem szivároghat. A lekérdezéskor történő tisztítás csapda.
  5. Az eszközbemeneteket/-kimeneteket hagyd ki teljesen. Ott laknak a titkok. Tartsd meg a neveket és a csonkolt, kitakart részleteket.
  6. Csak azt nyesd, ami archiválva van. A „soha ne törölj nem archivált adatot” szabály a különbség archívum és adatvesztési incidens között.
  7. Tartsd meg a frissességi padlót. Életkoruktól függetlenül mindig tartsd meg a legutóbbi N munkamenetet.
  8. Az OIDC-végpontokat a felfedezésből származtasd, soha ne szövegösszefűzésből. A szolgáltatók különböznek, és ez elviheti a délutánodat.
  9. Adj bőkezű SQL-időkorlátot a hideg gyorsítótárhoz. A 20 másodperces korlát semmibe sem kerül, és megelőzi a félelmetes 504-eseket újraindítás vagy nagy import után.
  10. A felület nézzen ki úgy, mint a szerszám. A csevegés-stílusú frontend azonnal ismerős mindenkinek, aki AI-asszisztenseket használ — ettől lesz használatbavétel.

Összegzés

Nincs szükséged keresőfürtre egy valóban hasznos beszélgetésarchívumhoz. SQLite-tal, FTS5-tel, egy Python-szkripttel, cron-nal és egy vékony webes réteggel ezt kapod:

  • Egy élő adatbázist, amely gyors és kicsi marad
  • Egy kompakt, teljes szövegű kereshető archívumot, amely mindent örökre megőriz
  • Egy csevegés-stílusú felületet, amelyet a csapatod tényleg használni fog
  • SSO-védett hozzáférést csoportos jogosultságszabályozással
  • Nulla szállítói bezártságot, nulla rendszeres költséget és teljes ellenőrzést az adataid felett

A teljes folyamat — export, kitakarás, indexelés, kiszolgálás, keresés — körülbelül 500 sor Python és egy kisméretű frontend. Minden éjjel felügyelet nélkül fut, és a növekvő beszélgetési előzmények halmazát kereshető intézményi emlékezetté változtatja.

Vélemény, hozzászólás?

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük