AI predajný asistent pre e-shopy. Nastaví sa z produktového feedu (Google Shopping XML, Shopify, WooCommerce, alebo bežný XML), beží na Cloudflare Workers, a neukladá obsah rozhovorov, len denné súhrnné počítadlá.
Demo a landing stránka: demo/index.html (naživo na https://arling.sk/asistent/ po nasadení).
- E-shop vloží URL feedu produktov a e-mail (
POST /v1/tenants). - Worker feed stiahne, znormalizuje, rozdelí na časti a uloží ako embeddings do Cloudflare Vectorize (
@cf/baai/bge-m3). Feed sa obnovuje automaticky raz denne (cron). - E-shop vloží jeden
<script>tag, buď nawidget/widget.js(GitHub Pages), alebo priamo naGET /widget.jsz Workera (rovnaký súbor, worker ho servíruje zo svojej vlastnej domény, viď nižšie). - Zákazník sa opýta widgetu na niečo; otázka sa zabedduje, nájde sa 8 najbližších produktov daného e-shopu vo Vectorize, a model (
@cf/meta/llama-3.3-70b-instruct-fp8-fast) odpovie výhradne z týchto produktov a kontaktných údajov obchodu, v jazyku zákazníka, do 120 slov, s najviac 3 odkazmi na produkty.
Súbory:
worker/: Cloudflare Worker (wrangler, plain JavaScript ES modules, žiadny build krok pri nasadení).widget/widget.js: vkladateľný chat widget (jeden súbor, Shadow DOM, bez závislostí). Toto je jediný zdroj pravdy pre widget;worker/src/widget-src.jsademo/widget.jssú z neho generované, viď "Widget: úprava a build" nižšie.demo/: landing stránka a živé demo (statické súbory pre GitHub Pages).scripts/build-widget.mjs: kopírujewidget/widget.jsdoworker/src/widget-src.jsademo/widget.js(npm run build:widget).legal/dpa-sk.md: vzorová zmluva o spracúvaní osobných údajov (čl. 28 GDPR).tests/:node --test, mocky pre AI/Vectorize/D1/KV, žiadna sieť.
Vlastník ešte nemá účet na Cloudflare: všetko nižšie beží a testuje sa lokálne s mockami, nasadenie príde neskôr.
cd products/arling-asistent
npm test # node --test tests/*.test.mjs, žiadna sieť, žiadny účetNa kontrolu syntaxe worker kódu a widgetu bez inštalácie čohokoľvek:
node --check widget/widget.js
node --check demo/widget.js
for f in worker/src/*.js; do node --check "$f"; donewidget/widget.js je jediný zdroj pravdy. Worker (plain ES modules, žiadny bundler) nevie priamo import-núť .js súbor ako text, a demo/ má byť nezávislá statická kópia pre GitHub Pages, preto po každej úprave widget/widget.js treba spustiť:
npm run build:widgetTento skript (scripts/build-widget.mjs) prepíše dva generované súbory:
worker/src/widget-src.js—export default \...`;s obsahom widgetu, servírovaný priamo Workerom naGET /widget.js(content-typeapplication/javascript,cache-control: public, max-age=3600, CORS*, keďže ide o statický, tenant-neutrálny kód nahrávaný z<script src>` z ľubovoľnej domény e-shopu).demo/widget.js— presná kópia pre GitHub Pages demo stránku.
Oba generované súbory sa commitujú ako bežný zdrojový kód (nasadenie samotné žiadny build krok nepotrebuje); skript treba spustiť len lokálne po úprave widget/widget.js, nie pri každom wrangler deploy.
Na lokálne vyskúšanie samotného Workera (vyžaduje len npx, nie účet, wrangler dev beží úplne offline s lokálnym D1/KV/Vectorize emulátorom):
cd worker
npx wrangler devNa lokálne prezretie demo stránky stačí otvoriť demo/index.html v prehliadači, alebo spustiť statický server (npx serve demo). Skúšobný formulár na stránke volá worker na adrese nastavenej v ?endpoint= parametri URL (predvolene placeholder https://arling-asistent.arling.workers.dev, ktorý treba nahradiť po nasadení); pri lokálnom teste pridajte ?endpoint=http://localhost:8787 a dočasne uvoľnite connect-src v CSP meta tagu v demo/index.html.
npm install -g wrangler
wrangler login
# D1 databáza (tenants + counters)
wrangler d1 create asistent
# skopírovať vrátené database_id do worker/wrangler.toml ([[d1_databases]])
wrangler d1 execute asistent --file=worker/schema.sql --remote
# Vectorize index (produktové embeddingy, 1024 dimenzií pre bge-m3, cosine)
wrangler vectorize create asistent-products --dimensions=1024 --metric=cosine
# KV namespace (rate-limit počítadlá)
wrangler kv namespace create ASISTENT_CACHE
# skopírovať vrátené id do worker/wrangler.toml ([[kv_namespaces]])
# Metadata index na Vectorize (nutné, inak filtrovaný dotaz podľa tenanta
# vždy vráti 0 výsledkov; worker sa bez neho degraduje na pomalší
# nefiltrovaný fallback, viď "Ak retrieval vracia 0 produktov" nižšie, ale
# treba ho vytvoriť čo najskôr):
wrangler vectorize create-metadata-index asistent-products --property-name=tenant --type=string
# Admin token pre POST /v1/tenants/:id/reingest a PATCH/POST
# /v1/tenants/:id/plan (ľubovoľný náhodný reťazec, napr. `openssl rand -hex
# 32`); bez neho oba endpointy odmietnu úplne všetky požiadavky, nikdy
# nepovolia re-ingest ani zmenu plánu bez neho. Rovnaká hodnota ide aj do
# products/licence-service ako ASISTENT_ADMIN_TOKEN, pozri "Platby cez
# Stripe" vyššie:
wrangler secret put ADMIN_TOKEN
cd worker
wrangler deployWidget (widget/widget.js) a demo stránku (demo/) treba nasadiť ako statické súbory (napríklad GitHub Pages pod arling.sk/asistent/, tak ako ostatné nástroje ARLing) — alebo namiesto toho použiť <script src="https://VASA-DOMENA-WORKERA/widget.js">, keďže worker po nasadení servíruje presne ten istý súbor priamo (viď "Vloženie widgetu na e-shop" nižšie), čo je jednoduchšie ako spravovať druhý statický hosting. Po nasadení Workera nahraďte placeholder https://arling-asistent.arling.workers.dev skutočnou doménou Workera v demo/app.js a demo/index.html (CSP connect-src).
Ak bol tenant vytvorený predtým, než existoval metadata index na property tenant (wrangler vectorize create-metadata-index vyššie), jeho pôvodné vektory vo Vectorize môžu byť v poriadku, ale chat.js sa degraduje na pomalší nefiltrovaný fallback dotaz (retrieveCandidates v worker/src/chat.js) namiesto zlyhania nahlas. Po vytvorení indexu stačí dotknutého tenanta manuálne pre-embednúť:
curl -X POST "https://VASA-DOMENA-WORKERA/v1/tenants/TENANT_ID/reingest" \
-H "X-Admin-Token: $ADMIN_TOKEN"Ten istý ingestFeedForTenant() beží aj v dennom crone (worker/src/cron.js), takže toto je len manuálne spustenie tej istej funkcie mimo poradia.
domain má v tenants UNIQUE obmedzenie, takže opakované odoslanie onboardingového formulára pre doménu, ktorá už tenanta má (napríklad majiteľ obchodu formulár omylom odošle dvakrát), nevráti chybu: vráti 200 s existujúcim tenantom ({..., "existing": true} namiesto 201), nikdy nie e-mail pôvodného tenanta. Ak sa odoslaná feed_url líši od uloženej, alebo posledné úspešné spracovanie feedu je staršie ako 24 hodín (alebo sa nikdy nepodarilo, tenant je v stave error), spustí sa na pozadí (ctx.waitUntil) rovnaké ingestFeedForTenant() ako pri crone. Akýkoľvek iný konflikt v D1 (nie kolízia domény) sa mapuje na 409 {"error":"conflict"}, nikdy nie na 500.
<script src="https://arling-asistent.arling.workers.dev/widget.js"
data-tenant="TENANT_ID"
data-lang="sk"
data-color="auto"
defer></script>GET /widget.js servíruje worker sám (rovnaký obsah ako widget/widget.js, viď "Widget: úprava a build" vyššie), takže e-shop nepotrebuje žiadny druhý hosting pre samotný skript.
data-tenant(povinné): id vrátené zPOST /v1/tenants.data-lang:sk,cs,en,de, aleboauto(predvolené, aj keď atribút úplne chýba). Priautosa vzhľad widgetu (tlačidlá, placeholder, pozdrav) riadi jazykom prehliadača návštevníka (s pádom na slovenčinu, ak ten nie je jeden zo štyroch podporovaných), a hodnota"auto"sa pošle aj na server vPOST /v1/chat, ktorý potom jazyk odpovede odhaduje z každej správy zákazníka zvlášť (pozriworker/src/chat.js).data-color:auto(podľa systému návštevníka, predvolené),lightalebodark.data-position:right(predvolené) aleboleft, na ktorej spodnej strane stránky sedí tlačidlo aj panel chatu.data-greeting: vlastný text prvej správy asistenta (nahradí predvolený pozdrav pre daný jazyk).data-title: vlastný názov panelu (zobrazí sa v hlavičke aj ako accessible name dialógu, nahradí predvolený názov pre daný jazyk).data-endpoint: voliteľná adresa Workera, ak sa líši od domény, z ktorej sawidget.jsnačítal.
POST /v1/tenants dnes vytvorí tenanta na pláne free bez platby. Kvóta sa zatiaľ počíta na jeden POST /v1/chat request, nie na správu v ňom (pozri komentár v worker/src/tenants.js a bod nižšie v "Čo ešte nie je hotové").
| Plán | Cena | Mesačná kvóta (predvolená) |
|---|---|---|
free |
zadarmo | 100 rozhovorov |
starter |
19 EUR/mesiac | 1 000 rozhovorov |
pro |
39 EUR/mesiac | 5 000 rozhovorov |
Toto je miesto, kde platený plán skutočne zmení, čo tenant smie používať (predtým POST /v1/tenants vytvoril vždy len free tenanta a nič ho z toho nikdy nedostalo, aj keď zaplatil). Chránené rovnako ako POST /v1/tenants/:id/reingest: hlavička X-Admin-Token musí sedieť s ADMIN_TOKEN secretom, inak 401 (a bez nastaveného ADMIN_TOKEN endpoint odmietne úplne všetko).
Telo požiadavky:
{ "plan": "starter", "monthly_quota": 1000, "billing_ref": "sub_...", "valid_until": "2026-11-05" }plan(povinné):"free","starter"alebo"pro", inak400 validation_failed.monthly_quota(voliteľné): kladné celé číslo. Bez neho sa použije predvolená kvóta daného plánu (tabuľka vyššie,DEFAULT_QUOTASvworker/src/tenants.js).billing_ref(voliteľné): ľubovoľný reťazec (napr. Stripe subscription id), uložený tak ako je. Bez neho sa nastaví nanull.valid_until(voliteľné): dátum/čas ako reťazec (ISO, napr."2026-11-05"), dokedy plán platí. Bez neho sa nastaví nanull. Vynucovanie expirácie (downgrade nafreepovalid_until) nerobí tento worker sám od seba, robí hoexpire_asistent_plans()vproducts/licence-service/app.py, volaním tohto istého endpointu splan: "free", pozri README toho projektu.
billing_ref a valid_until sú nové nullable stĺpce (billing_ref TEXT, valid_until TEXT), pridané rovnako ako product_count: guardovaným runtime ALTER TABLE (ensureBillingColumns v worker/src/tenants.js), takže existujúca nasadená databáza ich dostane automaticky pri prvom volaní tohto endpointu, bez potreby ručne spúšťať schema.sql znova. GET /v1/tenants/:id/status vracia oba stĺpce (null, kým nie sú nastavené).
Toto je presne to, čo volá Stripe webhook v products/licence-service/app.py (vlastný ASISTENT_ADMIN_TOKEN, ktorý sa musí zhodovať s týmto ADMIN_TOKEN) po úspešnej platbe alebo obnove predplatného za plán asistent-starter/asistent-pro, pozri "Platby cez Stripe" nižšie a README products/licence-service.
Samotné platenie beží v products/licence-service (homelab), nie v tomto Cloudflare Workeri: ten webhook prijme Stripe udalosť a zavolá späť sem, na PATCH /v1/tenants/:id/plan vyššie. Aby to fungovalo end-to-end, treba dve veci: nastaviť dva .env kľúče na homelabe (products/licence-service), a vyplniť dva placeholdery na strane frontendu (táto demo stránka, jej kópia v arling-sk/asistent/, a WordPress plugin).
| Kľúč | Hodnota |
|---|---|
ASISTENT_ADMIN_TOKEN |
Rovnaká hodnota ako ADMIN_TOKEN secret tohto Workera (wrangler secret put ADMIN_TOKEN vyššie): jeden zdieľaný token, dve mená v dvoch službách. |
ASISTENT_API_BASE |
https://arling-asistent.arling.workers.dev (predvolené, netreba nastavovať, ak sa doména Workera nezmenila). |
PLANS_JSON |
Doplniť o dva záznamy, jeden na cenu (pozri presný JSON nižšie). |
Presný PLANS_JSON snippet na doplnenie (zlúčiť s existujúcimi záznamami pre ostatné nástroje ARLing, nie nahradiť celý súbor):
{
"price_asistent_starter": {"plan": "asistent-starter", "days": 35},
"price_asistent_pro": {"plan": "asistent-pro", "days": 35}
}(price_asistent_starter/price_asistent_pro sú placeholder názvy, nahraďte skutočnými Stripe price id z kroku 2 nižšie. days: 35 namiesto 30/31 zámerne: pár dní rezervy, aby oneskorené invoice.paid doručenie nikdy nestihlo tenanta downgradnúť skôr, než v skutočnosti prestal platiť.)
- Products → nový produkt "ARLing Asistent".
- Na ňom dve recurring ceny: 19 EUR/mesiac a 39 EUR/mesiac, obe s DPH (tax inclusive), tax code
txcd_10000000(SaaS/softvér). - Pre každú cenu Payment Link (Dashboard → Payment links → New): v pokročilých nastaveniach zapnúť "Collect a client reference ID" (client reference ID passthrough), bez toho
?client_reference_id=...z tlačidla nižšie do Stripe Checkout Session vôbec nedorazí, a webhook potom nevie, ktorému tenantovi kvótu zdvihnúť. Success URL:https://arling.sk/asistent/?upgraded=1. - Skopírovať obe Payment Link URL do
data-stripe-linkatribútov vdemo/index.html(a rovnako doarling-sk/asistent/index.html, presná kópia) na tlačidlách#btn-plan-starter/#btn-plan-pro(miesto komentárovSTRIPE_LINK_STARTER/STRIPE_LINK_PRO), a doarling_asistent_stripe_link_starter/arling_asistent_stripe_link_profiltrov (alebo priamo doARLING_ASISTENT_DEFAULT_STRIPE_LINK_STARTER/_PROkonštánt vwordpress-plugin/arling-asistent/arling-asistent.php) pre WordPress plugin. Kým sú tieto placeholdery prázdne, príslušné tlačidlo ukazuje "Čoskoro"/"coming soon" a je neaktívne, nikdy nevedie na rozbitý odkaz. - Doplniť skutočné price id do
PLANS_JSON(krok vyššie) a reštartovaťlicenceslužbu (docker compose up -d --buildaleborestart).
Po tomto: zákazník klikne na tlačidlo s vlastným tenant_id v client_reference_id, zaplatí cez Stripe, checkout.session.completed dorazí do licence-service, ten zavolá PATCH /v1/tenants/:id/plan sem, a tenant má hneď zvýšenú kvótu, bez ručného zásahu.
| Služba | Bezplatný limit | Poznámka |
|---|---|---|
| Workers | 100 000 requestov/deň, 10 ms CPU/request | Pri prekročení CPU limitu treba platený plán (5 USD/mesiac, 30M CPU-ms) |
| Workers AI | 10 000 "neuronov"/deň (embeddingy aj chat model spolu) | Po prekročení treba platený Workers plán, doplatok 0,011 USD/1000 neuronov |
| Workers AI, bge-m3 embeddings | 0,012 USD/milión tokenov (platený plán) | Najlacnejší a viacjazyčný embedding model na Workers AI |
| Vectorize | 5M uložených dimenzií, 30M query-dimenzií/mesiac; max 100 indexov, max 1536 dimenzií/vektor, max 20 000 vektorov/batch | Mal by stačiť na katalógy malých e-shopov (limit 5000 produktov/tenant v tomto kóde) |
| D1 | 5M riadkov čítaných/deň, 100 000 zapísaných/deň, 5 GB úložisko | Ukladá len tenants + denné počítadlá, žiadne rozhovory |
| Workers KV | 100 000 čítaní/deň, 1 000 zápisov/deň, 1 GB úložisko | Len rate-limit počítadlá s krátkou expiráciou |
Najtesnejší limit je 10 000 Workers AI neuronov/deň (embeddingy pri onboardingu/dennom obnovení feedu aj chatový model zdieľajú tento limit): pri viacerých aktívnych e-shopoch treba počítať s prechodom na platený Workers plán čoskoro po prvých platiacich zákazníkoch, presne ako predpokladá ADR-04.
- Platby: zapojené na strane servera, čaká sa na Stripe účet vlastníka.
PATCH/POST /v1/tenants/:id/plan(pozri "Plány" vyššie) aproducts/licence-service's Stripe webhook (checkout/renewal preasistent-starter/asistent-proplány, dennýexpire_asistent_plans()cron) sú hotové a otestované. Chýba už len: vlastník vytvorí produkt a dve ceny v Stripe, vyplníSTRIPE_LINK_STARTER/STRIPE_LINK_PROna demo stránke a vo WordPress pluginu (pozri "Platby cez Stripe" nižšie), a nastavíPLANS_JSON/ASISTENT_ADMIN_TOKEN/ASISTENT_API_BASEna homelabe.POST /v1/tenantsnaďalej vytvorífreetenanta s pevnou kvótou bez platby, presne ako doteraz. - Kvóta na rozhovor, nie na správu. MVP zjednodušenie: každé volanie
POST /v1/chatsa počíta ako jeden rozhovor voči mesačnej kvóte (pozri komentár vworker/src/tenants.js). Presnejšie počítanie raz za reláciu (podľa in-memory session id na strane widgetu) je budúce rozšírenie. - Shoptet doplnok. Vyžaduje partnerské schválenie (Shoptet reaguje do 4 týždňov, pozri
opportunities/asistent-research.md), nie je súčasťou tohto MVP. Skript tag funguje na Shoptete aj bez doplnku. - WooCommerce plugin a Shopify aplikácia (inštalácia na klik z ich obchodov s doplnkami). Feed formáty oboch platforiem worker už vie spracovať (
worker/src/feed.js), chýba len samotný distribučný balík. - Načítanie stránok o doprave a obchodných podmienkach. ADR-04 spomína aj načítanie týchto stránok pri onboardingu; MVP spracúva len produktový feed, obchodné fakty (napríklad kontaktný e-mail) sa zatiaľ zadávajú len cez tenant záznam.
- EU-only garancia spracovania. Cloudflare verejne negarantuje, že Workers AI beží výlučne v EÚ (pozri
legal/dpa-sk.md, článok 7). Zmluva preto stojí na štandardných zmluvných doložkách (SCC) a certifikácii EU Cloud Code of Conduct, nie na technickej záruke. - Mazanie vektorov pri zmene feedu.
embed.jsvie vektory zmazať (deleteTenantVectors), alecron.jsdnes len prepíše (upsert) existujúce produkty; produkt, ktorý úplne zmizne z feedu, ostáva vo Vectorize ako zastaraný záznam. Čistenie osirotených vektorov je budúce rozšírenie. - Admin rozhranie. Žiadny prehľad tenantov, počítadiel ani logov mimo priameho dotazu do D1.
npm test (node --test tests/*.test.mjs), Node 20+, bez siete. 146 testov, 460 volaní assert.*, pokrývajúcich: parsovanie všetkých 4 formátov feedu a normalizáciu, chunkovanie a embedding pipeline, CORS allowlist (vrátane hlavičky na skutočných JSON odpovediach POST /v1/tenants a GET /v1/tenants/:id/status, nielen na OPTIONS preflighte), rate limiting a jeho fail-open správanie pri chybe KV, limity veľkosti vstupu a ich mapovanie na 413/400 namiesto 500, ochranu proti prompt injection (vrátane popisu produktu s textom "ignore previous instructions"), retrieval z Vectorize vrátane degradovaného nefiltrovaného fallbacku pri chýbajúcom metadata indexe, admin re-ingest endpoint (POST /v1/tenants/:id/reingest), admin set-plan endpoint (PATCH/POST /v1/tenants/:id/plan: autorizáciu, validáciu plánu, predvolené aj vlastné monthly_quota, ukladanie a čistenie billing_ref/valid_until, alias POST), validáciu a vytvorenie tenanta (vrátane predvoleného plánu free a jeho kvóty), idempotentné POST /v1/tenants pri opakovanej doméne (existujúci tenant, obnovenie feedu pri zmene URL alebo starnutí nad 24h, mapovanie iného D1 konfliktu na 409), product_count a billing_ref/valid_until vrátane guardovaného runtime ALTER TABLE pre existujúcu D1 databázu (ensureProductCountColumn/setProductCount, ensureBillingColumns/setTenantPlan v worker/src/tenants.js), mesačnú kvótu a počítadlá, stavbu groundovaného promptu, jazyk auto (heuristika detectLangFromText a systémový prompt, ktorý necháva model rozpoznať jazyk zákazníka), spracovanie odpovede modelu a celý chat flow s mockovaným modelom vracajúcim JSON, widget/widget.js samotný (načítanie cez node:vm s minimálnym fake DOM, bez jsdom, vrátane data-position, data-title, data-greeting a data-lang="auto" podľa navigator.language), a napokon aj wiring na úrovni HTTP routera (worker/src/index.js) so skutočnými Request/Response objektmi.
ARLing s. r. o. (Bratislava, Slovensko). andrej@arling.sk