Einführung
Ein Support-Assistent wirkt reaktionsschnell, wenn Wörter eintreffen, während das Modell noch antwortet. Ebenso vertrauenswürdig wirkt er, wenn eine Aktualisierung der Seite die Unterhaltung nicht löscht. Das sind zwei getrennte Anforderungen an die Entwicklung: Streaming liefert Antwortteile schrittweise, während die Persistenz abgeschlossene Nachrichten speichert, damit dieselbe benannte Unterhaltung später wiederhergestellt werden kann.
In diesem Lab fügen Sie beide Funktionen mit der von Cloudflare unterstützten Chat-Integration hinzu:
AIChatAgentspeichert Chat-Nachrichten und fortsetzbare Streaming-Daten im SQLite-basierten Durable Object des Agents.streamText()erzeugt eine begrenzte Workers-AI-Antwort, anstatt auf die vollständige Antwort zu warten.useAgentChat()wandelt diese Teile in eine React-Nachrichtenliste um und stellt den gespeicherten Verlauf wieder her.- Ein kurzlebiges signiertes Token begrenzt jede WebSocket- und Verlaufsanfrage auf eine benannte Unterhaltung.
Der Browser-Client wird als kleine Vorlage bereitgestellt, daher ist React keine versteckte Voraussetzung. Sie bearbeiten nur die aktuellen Hook-Aufrufe und die für dieses Agents-SDK-Konzept erforderliche Nachrichtendarstellung. Das Szenario verwendet synthetische Support-Texte, eine kurze Modellantwort und temporäre Ressourcen. Kostenlose Kontingente werden mit anderen Kontoaktivitäten geteilt. Wenn für das Konto kein Workers-AI-Kontingent mehr verfügbar ist, beenden Sie das Lab, anstatt ein kostenpflichtiges Abo zu aktivieren.
Bevor Sie diesen Kurs direkt beginnen, bearbeiten Sie LabEx mit Ihrem Cloudflare-Konto verbinden. Jede neue LabEx-VM benötigt eine eigene Wrangler-Autorisierung. S01 und S02 werden empfohlen, weil dieses Lab auf einer benannten Agent-Identität, einem SQLite-Zustand und WebSocket-Clients aufbaut. Deren VMs und Ressourcen werden hier jedoch nicht wiederverwendet.
Die VM autorisieren und den Chat-Worker konfigurieren
In diesem Schritt autorisieren Sie die neue VM und beschreiben die drei Cloudflare-Bindings, die der Chat benötigt.
Jeder benannte Chat wird von einer SQLite-Durable-Object-Instanz unterstützt. Der Worker benötigt außerdem ein Workers-AI-Binding für die Inferenz und ein Secret-Binding als Grenze für die Sitzung.
Öffnen Sie ein Terminal und wechseln Sie in das vorbereitete Projekt:
cd /home/labex/project/persistent-support-chat
Autorisieren Sie diese neue VM:
npx wrangler login
Öffnen Sie den angezeigten Link, bestätigen Sie die dokumentierten Wrangler-Berechtigungen für Ihr dediziertes Lernkonto und kehren Sie anschließend zum Terminal zurück. Prüfen Sie das strukturierte Ergebnis:
npx wrangler whoami --json
Suchen Sie nach "loggedIn": true, bestätigen Sie den Kontonamen und kopieren Sie die tatsächliche ID dieses Kontos. Speichern Sie sie zusammen mit einem eindeutigen temporären Worker-Namen:
ACCOUNT_ID="paste-your-confirmed-account-id"
RUN="labex-c11-s03-$(openssl rand -hex 6)"
cat > wrangler.jsonc <<JSON
{
"\$schema": "./node_modules/wrangler/config-schema.json",
"name": "$RUN",
"account_id": "$ACCOUNT_ID",
"main": "src/server.ts",
"compatibility_date": "2026-09-18",
"compatibility_flags": ["nodejs_compat"],
"workers_dev": true,
"preview_urls": false,
"observability": { "enabled": true },
"ai": { "binding": "AI", "remote": true },
"durable_objects": {
"bindings": [
{ "name": "SupportChatAgent", "class_name": "SupportChatAgent" }
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["SupportChatAgent"] }
]
}
JSON
Das AI-Binding ermöglicht dem Worker den Zugriff auf Workers AI, ohne einen API-Schlüssel einzubetten. Workers AI verwendet immer ein von Cloudflare gehostetes Modell, auch während der lokalen Entwicklung. Mit remote: true wird dieses Verhalten ausdrücklich festgelegt. Das Durable-Object-Binding ordnet einen Klassennamen zu. Der Browser liefert später den davon getrennten Instanznamen planning. Bisher wurde noch nichts bereitgestellt.
Einen begrenzten AIChatAgent implementieren
In diesem Schritt implementieren Sie die serverseitige Chat-Klasse, die begrenzte Inferenz und die signierte Routing-Grenze.
AIChatAgent erweitert den Basis-Agent um ein dauerhaft gespeichertes Chat-Transkript und die Speicherung fortsetzbarer Streams. Sie stellen den Modellaufruf bereit; die Integration übernimmt Chat-Protokoll und Persistenz.
Erstellen Sie src/server.ts:
cat > src/server.ts <<'TS'
import { AIChatAgent, type OnChatMessageOptions } from "@cloudflare/ai-chat";
import { convertToModelMessages, streamText } from "ai";
import { routeAgentRequest } from "agents";
import { createWorkersAI } from "workers-ai-provider";
import { verifySessionRequest } from "./session-auth";
interface Env {
AI: Ai;
SupportChatAgent: DurableObjectNamespace<SupportChatAgent>;
SESSION_SIGNING_KEY: string;
}
export class SupportChatAgent extends AIChatAgent<Env> {
maxPersistedMessages = 12;
async onChatMessage(_onFinish: unknown, options?: OnChatMessageOptions) {
console.log(JSON.stringify({
event: "support_chat_turn_started",
requestId: options?.requestId ?? "unknown",
messageCount: this.messages.length,
continuation: Boolean(options?.continuation)
}));
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/zai-org/glm-4.7-flash", {
reasoning_effort: null,
chat_template_kwargs: { enable_thinking: false }
}),
system: "You are a concise support assistant. Answer synthetic questions in one sentence and never request credentials.",
messages: await convertToModelMessages(this.messages),
maxOutputTokens: 64,
temperature: 0,
abortSignal: options?.abortSignal
});
return result.toUIMessageStreamResponse();
}
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const authorize = (candidate: Request, route: { name: string }) =>
verifySessionRequest(candidate, route.name, env.SESSION_SIGNING_KEY);
return (await routeAgentRequest(request, env, {
onBeforeConnect: authorize,
onBeforeRequest: authorize
})) ?? new Response("Not found", { status: 404 });
}
};
TS
Hier sind drei Begrenzungen wichtig. maxPersistedMessages begrenzt das Wachstum des gespeicherten Transkripts, maxOutputTokens begrenzt jede Modellantwort und der System-Prompt fordert einen einzigen Satz an. GLM 4.7 Flash kann sein Token-Budget für internes Reasoning verwenden, bevor sichtbarer Text erzeugt wird. Deshalb deaktiviert dieser kurze Support-Workflow das Denken ausdrücklich. Lernende sehen so eine knappe Antwort statt einer leeren Assistentenblase. Durch die Weitergabe von abortSignal kann das SDK die vorgelagerte Inferenz abbrechen, wenn ein Durchlauf ausdrücklich gestoppt wird.
Beide Routing-Hooks verwenden den bereitgestellten HMAC-Prüfer. onBeforeConnect schützt den WebSocket-Handshake, während onBeforeRequest zusätzlich HTTP-Hilfsfunktionen wie /get-messages schützt. Der Browser erhält einen signierten Anspruch, aber niemals das Signatur-Secret. Das Protokoll erfasst eine Request-ID und eine Anzahl, schließt den Support-Text jedoch absichtlich aus.
Die unterstützten React-Chat-Hooks verbinden
In diesem Schritt verbinden Sie das bereitgestellte Seiten-Grundgerüst mit den aktuell unterstützten React-Hooks.
Das vorbereitete HTML und die Styles bilden nur das Grundgerüst. Verbinden Sie es jetzt mit dem benannten Agent. Erstellen Sie die TypeScript- und Vite-Konfiguration:
cat > tsconfig.json <<'JSON'
{
"extends": "agents/tsconfig",
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["@cloudflare/workers-types", "vite/client", "node"]
},
"include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts", "worker-configuration.d.ts"]
}
JSON
cat > vite.config.ts <<'TS'
import { cloudflare } from "@cloudflare/vite-plugin";
import react from "@vitejs/plugin-react";
import agents from "agents/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [react(), agents(), cloudflare()]
});
TS
Erstellen Sie src/client.tsx:
cat > src/client.tsx <<'TSX'
import { useAgentChat } from "@cloudflare/ai-chat/react";
import { useAgent } from "agents/react";
import { Suspense } from "react";
import { createRoot } from "react-dom/client";
function SupportChat() {
const parameters = new URLSearchParams(window.location.search);
const session = parameters.get("session") ?? "";
const token = parameters.get("token") ?? "";
if (!session || !token) {
return <main><h1>Signed session required</h1><p className="help">Open the complete URL printed by the token command.</p></main>;
}
const agent = useAgent({
agent: "SupportChatAgent",
name: session,
host: window.location.host,
query: { token }
});
const { messages, sendMessage, status, error } = useAgentChat({ agent });
return (
<main>
<p className="eyebrow">Cloudflare Agents SDK</p>
<h1>Persistent Support Chat</h1>
<p className="session">Conversation: <strong>{session}</strong></p>
<p className="status">Status: <strong>{status}</strong></p>
<section className="messages" aria-live="polite">
{messages.length === 0 && <p className="empty">No saved messages in this conversation.</p>}
{messages.map((message) => (
<article className={`message ${message.role}`} key={message.id}>
<span className="role">{message.role}</span>
{message.parts.map((part, index) =>
part.type === "text" ? <span key={index}>{part.text}</span> : null
)}
</article>
))}
</section>
<form => {
event.preventDefault();
const input = event.currentTarget.elements.namedItem("message") as HTMLInputElement;
const text = input.value.trim();
if (!text) return;
sendMessage({ text });
input.value = "";
}}>
<input name="message" defaultValue="What does pending invoice status mean?" maxLength={160} aria-label="Support question" />
<button type="submit" disabled={status === "streaming" || status === "submitted"}>Send</button>
</form>
{error && <p className="error" role="alert">{error.message}</p>}
</main>
);
}
createRoot(document.getElementById("root")!).render(
<Suspense fallback={<main><p>Restoring the signed conversation…</p></main>}>
<SupportChat />
</Suspense>
);
TSX
useAgent() verwaltet die signierte WebSocket-Verbindung zu SupportChatAgent:<session>. useAgentChat() legt das AI-Chat-Protokoll über diese Verbindung: Nachrichten, Streaming-Status, das Senden und die anfängliche Wiederherstellung des Verlaufs. Das Token wird in der Verbindungs-URL übertragen, weil Browser-WebSocket-Handshakes keinen benutzerdefinierten Authorization-Header hinzufügen können. Es läuft nach zehn Minuten ab und ist auf eine synthetische Unterhaltung begrenzt.
Typen generieren und beide Seiten erstellen
In diesem Schritt generieren Sie exakte Umgebungstypen und kompilieren beide Teile, bevor Sie eine Laufzeit starten.
Wrangler kann anhand Ihrer Konfiguration exakte Binding-Typen generieren. Führen Sie den Befehl vor den normalen TypeScript- und Vite-Builds aus:
npx wrangler types
npm run check
npm run build
Die Typprüfung verbindet this.env.AI, den Durable-Object-Namespace und das Secret-Binding mit dem deklarierten Env. Der Vite-Build erzeugt ein Worker-Bundle und ein Browser-Bundle. Eine erfolgreiche Ausgabe sollte dist/client/index.html enthalten.
Die signierte Grenze lokal testen
In diesem Schritt starten Sie die lokale Laufzeit und testen die Zugriffskontrolle, ohne einen Modellaufruf zu verbrauchen.
Workers AI ist ein Remote-Binding. Deshalb benötigt die lokale Vite-Laufzeit den bereits von Wrangler gespeicherten OAuth-Zugriff. Lesen Sie ihn direkt in eine kurzlebige Shell-Variable ein, übergeben Sie ihn nur an den untergeordneten Prozess und löschen Sie die Shell-Kopie sofort:
DEV_PROXY_TOKEN="$(npx wrangler auth token --json | node -e 'let data="";process.stdin.on("data",chunk=>data+=chunk).on("end",()=>process.stdout.write(JSON.parse(data).token))')"
CLOUDFLARE_API_TOKEN="$DEV_PROXY_TOKEN" CI=true npm run dev > .labex/dev.log 2>&1 < /dev/null &
echo $! > .labex/dev.pid
unset DEV_PROXY_TOKEN
Geben Sie diesen Wert nicht aus und speichern Sie ihn nicht in .dev.vars. Es handelt sich um den bereits vorhandenen temporären Wrangler-OAuth-Zugriff, nicht um ein neu erstelltes API-Token. CI=true und die umgeleitete Standardeingabe sorgen dafür, dass der Vite-Prozess nach der Rückkehr des Terminals getrennt weiterläuft.
Warten Sie, bis die URL erscheint:
until curl -fsS http://127.0.0.1:5173/ >/dev/null; do sleep 1; done
tail -n 12 .labex/dev.log
Führen Sie die unabhängige lokale Prüfung aus:
python3 .labex/verify.py local
Dabei wird absichtlich kein Modellaufruf ausgeführt. Die Prüfung bestätigt, dass eine korrekt signierte neue Sitzung ihren leeren Verlauf lesen kann, während eine unsignierte Anfrage und ein gültiges Token für einen anderen Namen beide HTTP 401 erhalten. Local Miniflare verwendet dieselben Routing-Hooks und dasselbe Secret aus .dev.vars.
Persistentes Streaming bereitstellen und beobachten
In diesem Schritt stellen Sie die Anwendung bereit, beobachten eine echte gestreamte Antwort, stellen sie nach einer Aktualisierung wieder her und weisen die Sitzungsisolierung nach.
Stellen Sie den Produktions-Build bereit und laden Sie anschließend den generierten Signaturschlüssel als Worker-Secret hoch:
npm run deploy
npx wrangler secret bulk .dev.vars
Der Secret-Befehl sendet den Wert an Cloudflare, ohne ihn in wrangler.jsonc oder im Bundle abzulegen. Geben Sie .dev.vars nicht aus.
Speichern Sie den beim erfolgreichen Deployment ausgegebenen exakten workers.dev-Ursprung und erstellen Sie anschließend ein zehn Minuten gültiges Token für die Unterhaltung planning:
WORKER_URL="https://paste-the-workers-dev-origin-printed-by-deploy"
TOKEN="$(node scripts/create-session-token.mjs planning)"
printf '%s/?session=planning&token=%s\n' "${WORKER_URL%/}" "$TOKEN"
WORKER_URL enthält nur den Ursprung, ohne abschließenden Schrägstrich oder Pfad. Bewahren Sie das Token in dieser Terminalsitzung auf und fügen Sie es nicht in Notizen oder Screenshots ein.
Öffnen Sie die vollständige URL. Der anfängliche Status sollte sich auf ready einpendeln, und die Seite sollte anzeigen, dass keine gespeicherten Nachrichten vorhanden sind. Senden Sie die vorbereitete synthetische Frage. Beobachten Sie, wie sich submitted zu streaming und anschließend wieder zu ready ändert, während Text eintrifft.

Die dargestellte Ressource und Antwort stammen aus dem getesteten temporären Lauf. Ihre genaue Formulierung kann abweichen, da die Modellausgabe nicht deterministisch ist.
Aktualisieren Sie dieselbe URL. Die abgeschlossenen Nachrichten des Benutzers und des Assistenten sollten aus SQLite zurückkehren, anstatt dass die Unterhaltung neu beginnt:

Weisen Sie nun die Namensisolierung nach. Erzeugen und öffnen Sie eine separat signierte URL:
PRIVATE_TOKEN="$(node scripts/create-session-token.mjs private)"
printf '%s/?session=private&token=%s\n' "${WORKER_URL%/}" "$PRIVATE_TOKEN"
Die Seite private ist autorisiert, gehört jedoch zu einer anderen benannten Agent-Instanz. Deshalb ist ihr Verlauf leer:

Führen Sie abschließend eine unabhängige, für diesen Lauf eindeutige Remote-Prüfung aus. Sie führt einen zusätzlichen begrenzten Modellaufruf aus, bestätigt mehrere Stream-Teile, ruft die gespeicherten Benutzer- und Assistentennachrichten nach einer erneuten Verbindung ab, prüft eine zweite autorisierte Sitzung mit leerem Verlauf und weist den sitzungsübergreifenden Zugriff zurück:
python3 .labex/verify.py deployed
Die Chat-Ressourcen untersuchen und entfernen
In diesem Schritt verknüpfen Sie das Laufzeitverhalten mit den Nachweisen im Dashboard und entfernen anschließend nur die Ressourcen dieses Labs.
Öffnen Sie im Cloudflare-Dashboard Workers & Pages, wählen Sie Ihren exakten Worker labex-c11-s03-... aus und untersuchen Sie seine Bindings. Sie sollten sowohl das AI-Binding als auch das Durable-Object-Binding SupportChatAgent sehen:

Öffnen Sie Durable Objects und wählen Sie den SQL-basierten Namespace aus, der diesem Worker gehört. Der Namespace ist die Ressourcenansicht von Cloudflare. planning, private und die Namen der Prüfungen sind darin isolierte Instanzen:

Öffnen Sie die Logs oder die Observability-Ansicht des Workers und suchen Sie nach support_chat_turn_started. Das Ereignis zeigt begrenzte Metadaten wie die Nachrichtenanzahl, aber weder die Eingabe der lernenden Person noch die Modellantwort:

Erstellen Sie nach der Untersuchung eine Löschmigration, die nur den Klassen-Namespace dieses Labs entfernt:
python3 - <<'PY'
import json
from pathlib import Path
path = Path('wrangler.jsonc')
data = json.loads(path.read_text())
data.pop('durable_objects', None)
data['migrations'].append({'tag': 'v2', 'deleted_classes': ['SupportChatAgent']})
Path('wrangler.cleanup.jsonc').write_text(json.dumps(data, indent=2) + '\n')
PY
npx wrangler deploy --config wrangler.cleanup.jsonc
npx wrangler delete --config wrangler.cleanup.jsonc --force
Bestätigen Sie, dass der Worker unter Workers & Pages nicht mehr vorhanden ist:

Bestätigen Sie anschließend, dass der zugehörige SupportChatAgent-Namespace unter Durable Objects nicht mehr vorhanden ist:

Führen Sie die authentifizierte Prüfung auf Abwesenheit aus, solange diese VM noch autorisiert ist:
python3 .labex/verify.py deleted
Das alleinige Löschen des Workers reicht nicht aus: Die explizite Migration mit deleted_classes macht den Lebenszyklus des zustandsbehafteten Namespace überprüfbar und verhindert, dass der gespeicherte synthetische Verlauf dieses Labs zurückbleibt.
Die Autorisierung dieser VM widerrufen
In diesem Schritt widerrufen Sie die Autorisierung der temporären VM, nachdem Sie die Bereinigung der Cloud-Ressourcen bestätigt haben.
Die Cloud-Ressourcen sind bereits entfernt. Widerrufen Sie nun die in dieser temporären VM gespeicherte OAuth-Autorisierung:
npx wrangler logout
npx wrangler whoami --json || true
Das strukturierte Ergebnis sollte "loggedIn": false melden. Alternativ kann Wrangler ein Ergebnis ohne Authentifizierung mit einem von null verschiedenen Exit-Code zurückgeben. Dieser Schritt steht absichtlich am Ende: Für die Prüfung der Bereinigung ist eine gültige Autorisierung erforderlich, während das Abmelden die anschließend verworfene VM schützt.
Zusammenfassung
Sie haben mit der aktuellen Chat-Integration von Cloudflare eine persistente, gestreamte Support-Unterhaltung erstellt. Sie haben:
AIChatAgenterweitert und einen begrenzten Workers-AI-Aufruf mitstreamText()verwendet;- ein bereitgestelltes React-Grundgerüst mit
useAgent()unduseAgentChat()verbunden; - sowohl WebSocket- als auch HTTP-Routen für den Verlauf mit einer ablaufenden, sitzungsgebundenen Signatur geschützt;
- inkrementelle Statusänderungen beobachtet, den SQLite-basierten Verlauf nach einer Aktualisierung wiederhergestellt und nachgewiesen, dass eine andere benannte Unterhaltung isoliert blieb;
- datenschutzbegrenzte Nachweise in Cloudflare untersucht; und
- den exakten Agent-Klassen-Namespace und den Worker gelöscht, bevor Sie die VM-Autorisierung widerrufen haben.
Das nächste Lab verwendet dieselbe dauerhafte Agent-Identität für geplante Support-Nachfassaktionen. Scheduling ist ein anderer Aspekt des Lebenszyklus: Dadurch kann eine Aufgabe später ausgeführt werden, auch wenn kein Browser mehr verbunden ist.



