A Fuzhou University intelligent Q&A system with student authentication and educational system integration, built with LangGraph, FastAPI, and React.
Current tagged release: v7.30
Release notes: CHANGELOG.md
FZU-Chat provides a ChatGPT-style conversation experience for Fuzhou University students. Each student logs in with their student ID to get isolated conversations and access to educational system tools (grade queries, course schedules, etc.); external visitors can use third-party guest login for public Q&A and campus-life guidance.
- Student authentication: Per-student login with conversation isolation
- Independent passkey login: Create a discoverable WebAuthn passkey and enter as a new visitor, without a username, email, password or third-party account. Existing users can also add passkeys to their current identity.
- Third-party guest mode: WeChat, QQ, Alipay, Microsoft, Apple, and GitHub OAuth can create visitor sessions with public Q&A, knowledge retrieval, web search, and campus-life recommendations, without binding personal grade, schedule, selection, or academic-affairs tools
- Educational system tools: Query grades, courses, exam scores, and student info via the FZU academic affairs system (based on west2-online/jwch)
- Educational session cleanup: The app only keeps educational-system session cookies on the server side and never stores the raw password; logging out clears both the site login state and the cached educational-session cookies
- Professional privacy controls: Versioned privacy terms, account data statistics, saved-data reset, and irreversible account deletion that removes service data, revokes every active session, clears local preferences, and signs out
- ChatGPT-style interface: Modern dark UI with sidebar history, quick actions, and streaming replies
- Message editing and regeneration: Icon-only actions for copying replies, regenerating an assistant answer, or editing a sent user message and rebuilding the following response branch
- Accessible interaction polish: Keyboard-friendly focus rings, skip link, screen-reader status updates, dialog focus handling, and live chat-log announcements
- Low-intrusion campus intelligence: After login or educational reconnect, the backend refreshes cached course, exam, selection, and grade-summary snapshots; each new conversation freezes one compact runtime context so the model can decide whether a gentle end-of-answer reminder is useful without changing old messages
- Rich tool cards: Visual display of tool calls with structured data tables for grades and courses
- Multi-model support: Huawei Cloud MaaS GLM-5.3, Kimi K2.6, and DeepSeek V4.1 Flash selection, with qwen3-30b-a3b for title summarization
- FZU-aware personalized memory: Confirmed long-term preferences for names, answer style, course-selection habits, academic-query presentation, campus-life needs, and dining/campus preferences, while volatile educational facts remain live tool queries
- Knowledge + web search: FAISS retrieval plus Bocha web search fallback
- Launch-ready single-node hardening: Strict SPA fallback, scanner-path 404s, Redis-backed sessions/rate limits, readiness and Prometheus-style metrics
- Docker deployment: Multi-stage build for React frontend + Python backend, plus production Compose with internal Redis
fzu-chat/
├── app/
│ ├── server.py # FastAPI backend with auth + per-user conversations
│ ├── graph.py # LangGraph workflow with edu tools
│ ├── auth.py # Token-based authentication & session management
│ ├── oauth.py # Third-party visitor OAuth helpers
│ ├── jwch_client.py # FZU undergraduate system client (Python port)
│ ├── edu_tools.py # LangGraph tools for educational queries
│ ├── campus_recommendations.py # Contextual dining/study recommendation service
│ ├── campus_dynamic_context.py # Low-latency dynamic context and reminder suppression
│ ├── user_memory_tools.py # Confirmed personalized-memory tools
│ ├── memory_store.py # SQLite-backed long-term memory store
│ ├── data/ # Knowledge base documents
│ ├── faiss/ # FAISS vector database
│ ├── png/ # Static assets
│ └── storage/ # Per-user conversation storage
├── frontend/
│ ├── src/App.jsx # React chat UI with login
│ ├── src/App.css # Modern dark theme styles
│ └── vite.config.js # Vite config with API proxy
├── Dockerfile
├── docker-compose.prod.yml
├── scripts/ # Deploy, rollback, and SQLite backup helpers
├── docker-compose.yml
└── requirements.txt
HUAWEICLOUD_MAAS_API_KEY– Huawei Cloud MaaS OpenAI-compatible API for GLM-5.3, Kimi K2.6, DeepSeek V4.1 Flash, and qwen3-30b-a3b title summarizationDASHSCOPE_API_KEY– Alibaba Cloud DashScope embeddings for the local knowledge baseBOCHA_API_KEY– Bocha web searchLANGSMITH_API_KEY– LangSmith tracingAMAP_WEB_SERVICE_KEY– Optional AMap Web Service key for reverse geocoding browser location into text, POI supplementation, and walking/bicycling route distance; local development can also useamap_web_service_key.txtorAMAP_WEB_SERVICE_KEY_FILE; requests are throttled to at most 5 QPS by default; reverse-geocoding uses a short timeout and 10-minute in-process cache; when unset, campus recommendations fall back to the built-in FZU place library and estimated distanceFZU_CHAT_OAUTH_PROVIDERS– Optional comma-separated visible visitor-login provider allowlist, e.g.microsoft,apple,githubfor production while keeping WeChat and QQ available in the codebaseFZU_CHAT_WECHAT_CLIENT_ID/FZU_CHAT_WECHAT_CLIENT_SECRET,FZU_CHAT_QQ_CLIENT_ID/FZU_CHAT_QQ_CLIENT_SECRET,FZU_CHAT_MICROSOFT_CLIENT_ID/FZU_CHAT_MICROSOFT_CLIENT_SECRET,FZU_CHAT_APPLE_CLIENT_ID/FZU_CHAT_APPLE_CLIENT_SECRET,FZU_CHAT_GITHUB_CLIENT_ID/FZU_CHAT_GITHUB_CLIENT_SECRET– Optional visitor OAuth credentials; the matching*_REDIRECT_URIvariables can override the default/api/auth/oauth/{provider}/callbackcallback URL. Apple uses the generated client-secret JWT forFZU_CHAT_APPLE_CLIENT_SECRET; Microsoft can also setFZU_CHAT_MICROSOFT_TENANT, defaulting tocommon.
For local development, the backend also reads root-level key files such as huaweicloud_maas_api_key.txt, dashscope_api_key.txt, and bocha_api_key.txt when container secrets and environment variables are not set.
# 1. Install backend dependencies
pip install -r requirements.txt
# 2. Set environment variables
export HUAWEICLOUD_MAAS_API_KEY=...
export DASHSCOPE_API_KEY=...
export BOCHA_API_KEY=...
export LANGSMITH_API_KEY=...
export AMAP_WEB_SERVICE_KEY=... # optional, for text location and campus routes
export FZU_CHAT_OAUTH_PROVIDERS=microsoft,apple,github # optional visible provider allowlist
export FZU_CHAT_GITHUB_CLIENT_ID=... # optional, for visitor GitHub login
export FZU_CHAT_GITHUB_CLIENT_SECRET=...
# 3. Start backend
uvicorn app.server:app --host 0.0.0.0 --port 8000
# 4. Start frontend dev server
cd frontend && npm ci && npm run dev
# 5. Open http://localhost:5173# 1. Create API key files
echo "your-key" > huaweicloud_maas_api_key.txt
echo "your-key" > dashscope_api_key.txt
echo "your-key" > bocha_api_key.txt
echo "your-key" > langsmith_api_key.txt
echo "your-key" > amap_web_service_key.txt # optional, for contextual recommendations
# 2. Build and run local Compose
docker compose up -d --build
# 3. Visit http://localhost:80Production deployment can use docker-compose.prod.yml with an internal Redis container. Set a URL-safe REDIS_PASSWORD such as openssl rand -hex 32, provision the session encryption key below, then run FZU_CHAT_VERSION=v7.27 ./scripts/deploy-ghcr.sh; if GHCR image pull fails, the script falls back to a local production image build. Hosts using additional Compose files (resource limits or Alipay secrets) must include all override files in their deployment commands rather than using this single-file helper.
Frontend builds use npm ci to install the committed lockfile and run npm run audit:security before producing the Docker image. Known npm advisories at low severity or above block the build; resolve them by updating compatible dependencies rather than disabling the check. This check covers npm dependencies, not a full Python or operating-system security audit.
Before starting either Docker Compose configuration, create the persistent session encryption key once:
python scripts/generate-session-key.pyThe script creates session_encryption_key.txt with owner-only permissions and refuses to overwrite an existing key. Compose mounts it read-only as a secret; it is excluded from Git. Keep this file across releases and protect its backup separately from Redis. Do not regenerate it during ordinary deployments: losing or replacing it invalidates encrypted sessions.
Redis session values (including education cookies) use authenticated encryption, and website bearer tokens are hashed before use as Redis keys. Startup migrates existing plaintext sessions without changing browser tokens or extending their lifetime. A missing/invalid key blocks startup rather than silently storing plaintext. Rolling back to a release predating encrypted sessions requires users to sign in again; conversation data is unaffected. Encryption does not protect against an attacker who controls the application server and can also read its key.
Education connections are shared across authenticated sessions for the same education account. Undergraduate and graduate identities have separate storage and credentials. A revision check prevents stale requests from invalidating a newly connected session. The device event stream sends only connection status, never credentials; normal logout affects that device only, while account deletion revokes all devices and removes shared credentials. Browser focus/online events and periodic refresh reconcile missed status updates.
Graduate login follows west2-online/yjsy v0.0.11 and exposes grades, semester schedules, student profiles and exam rooms. Unsupported personal tools are not bound to graduate sessions. Login and reconnect share a conservative five-attempt budget per account per 30 minutes and never retry automatically. The login-page selector retains the existing layout. Validation uses synthetic upstream responses; authenticated graduate login and real page parsing still require a student-account check. See the protocol mapping and verification boundaries.
Teaching-system and CAPTCHA requests validate TLS certificates. A private trusted CA bundle can be configured through FZU_CHAT_JWCH_CA_BUNDLE for the teaching-system client; disabling certificate verification is not supported.
Useful production environment variables:
FZU_CHAT_REDIS_URL– Redis URL for session, rate-limit, stream-slot, and background-task dedupe stateFZU_CHAT_PUBLIC_DOCS=false– keep/openapi.json, Swagger, and ReDoc private by defaultFZU_CHAT_GLOBAL_STREAM_LIMIT=80,FZU_CHAT_USER_STREAM_LIMIT=5– active SSE generation caps for initial campus-scale launchFZU_CHAT_STATIC_FALLBACK=strict– return the SPA only for/andindex.htmlFZU_CHAT_METRICS_ENABLED=true– enable restricted/api/metricsFZU_CHAT_OAUTH_PROVIDERS=microsoft,apple,github– show only Microsoft, Apple, and GitHub in production; omit it to show all repository-supported providersFZU_CHAT_WECHAT_CLIENT_ID/FZU_CHAT_WECHAT_CLIENT_SECRET,FZU_CHAT_QQ_CLIENT_ID/FZU_CHAT_QQ_CLIENT_SECRET,FZU_CHAT_MICROSOFT_CLIENT_ID/FZU_CHAT_MICROSOFT_CLIENT_SECRET,FZU_CHAT_APPLE_CLIENT_ID/FZU_CHAT_APPLE_CLIENT_SECRET,FZU_CHAT_GITHUB_CLIENT_ID/FZU_CHAT_GITHUB_CLIENT_SECRET– enable the matching visitor login entry; unconfigured providers are shown as unavailable in the login UI
Alipay is optional and disabled until the application has passed platform review.
Configure RSA2 (2048-bit or stronger) on a web application with member-information
authorization, and register https://mylingxi.cn/api/auth/oauth/alipay/callback
using full-address matching. Official OAuth guide
and official Python SDK protocol reference.
- Keep the PKCS8/PKCS1 PEM application private key outside the repository and Docker build context, with owner-only permissions. Upload only its matching public key to Alipay.
- Download the distinct Alipay public key from the platform for response verification (base64 DER or PEM). Do not substitute the application public key.
- Set
FZU_CHAT_ALIPAY_APP_ID,FZU_CHAT_ALIPAY_REDIRECT_URI,FZU_CHAT_ALIPAY_PRIVATE_KEY_HOST_FILE, andFZU_CHAT_ALIPAY_PUBLIC_KEY_HOST_FILEin the host.envand addalipaytoFZU_CHAT_OAUTH_PROVIDERS. - Include
docker-compose.alipay.ymlalongside production/resource-limit Compose files. It mounts both key files as read-only secrets. No new Python dependency is required. - Keep
FZU_CHAT_ALIPAY_ENABLED=falseuntil review is approved. Configured but pending providers render as a disabled “待上线” entry. After approval, set it totrueand recreate only the app container with all overrides.
docker compose -f docker-compose.prod.yml -f docker-compose.2g.override.yml -f docker-compose.alipay.yml pull --policy always fzu-chat
docker compose -f docker-compose.prod.yml -f docker-compose.2g.override.yml -f docker-compose.alipay.yml up -d --no-deps fzu-chatThe callback accepts auth_code, checks browser-bound state, atomically consumes it
in Redis, exchanges the code through a signed RSA2 request, and verifies the exact
UTF-8 response before trusting identity. New applications use open_id; legacy
user_id is supported only when open_id is absent. Profile identity must match
the token response. Only hashed identity, nickname and HTTPS avatar are retained;
access/refresh tokens, phone numbers and real-name fields are not stored. No payment
or transfer APIs are implemented.
Uvicorn access logs strip OAuth query parameters. For Nginx, install
deploy/nginx-oauth-logging.conf in its HTTP context and use access_log /var/log/nginx/fzu-chat.access.log fzu_privacy; in the site's server blocks. This
retains request paths/status/timing without logging authorization codes, state or
referrer URLs. Existing historical logs are not altered. Verify nginx -t before
reloading. The Alipay glyph uses Simple Icons (CC0); the Alipay trademark belongs
to its respective owner.
Mobile Safari/Chrome/Edge cannot directly display Alipay's H5 authorization page. The current login button opens a small same-origin entry inside Alipay, then starts Alipay's official web authorization. The entry checks the existing legal-consent marker before navigation. The server binds a one-time OAuth state to an HttpOnly cookie in that Alipay client and verifies the provider's signed response before creating a session. No native JavaScript bridge is required. On success the user continues chatting inside Alipay. The original Edge/Safari tab is not logged in; no browser-return button, polling or clipboard is required. Failures return to the regular login page with a normal error message, never a debug panel or an automatic retry loop.
An explicit “打开支付宝” link remains if automatic launching is blocked. Users who prefer their normal browser can choose Passkey or another supported login. Alipay may still display its own external-site warnings; our code cannot suppress them. App launch, cookie persistence and authorization must still be accepted on real devices, separately from automated signed-callback tests.
Experimental native/handoff endpoints, return-browser pages and temporary
diagnostics have been removed. No session credential is placed in a URL, and the
strict same-origin script policy is retained. The v7.30 image includes the tested
server hotfix; retire docker-compose.hotfix.yml when upgrading from v7.29, while
keeping the production, resource-limit and Alipay-secret Compose files.
- The login page offers 通行密钥 · Passkey alongside OAuth. First-time users choose “创建通行密钥并进入”; a random visitor identity is created only after successful WebAuthn verification. No username, email or prior account is needed.
- Returning users choose an existing discoverable passkey. Creating again while logged out creates another identity, not a recovery or automatic account merge. Public Q&A is available, but independent visitors have no academic-affairs access.
- Account settings allow adding/removing keys for the current identity after a login within the last ten minutes. Maximum ten keys; the last key of a passkey-only identity cannot be removed except by deleting that identity.
- Keep a synced passkey or enroll an additional device. Losing all usable keys with no other login method means the identity cannot be recovered. In-app browsers may not support WebAuthn; use a supported secure system browser.
FZU_CHAT_WEBAUTHN_ORIGINdefaults tohttps://mylingxi.cn; RP ID is derived from this trusted configuration, not client headers. Keep it stable between releases. Custom deployments must set their canonical HTTPS origin and preserve that public origin through their reverse proxy. Passkeys are domain-bound.app/storage/passkeys.sqlitepersists public keys, random handles, profile metadata and short-lived challenges. Preserve the storage volume and include it in encrypted SQLite backups (the existing wildcard backup script covers it). No private keys, biometrics or device-unlock secrets reach the server.- Verification requires the exact origin/RP, signature, user verification and cookie-bound one-use challenge. Enrollment is additionally bound to the recent session; key changes and account deletion are serialized. Requests are subject to same-origin JSON/custom-header checks and rate limits. Deleting the account removes server-side keys and revokes all sessions, but not the device-manager entry.
POST /api/auth/login– Login with student ID + passwordGET /api/auth/oauth/providers– Report visible visitor-login availabilityGET /api/auth/oauth/{provider}/start– Start a visible visitor OAuth redirectGET|POST /api/auth/oauth/{provider}/callback– OAuth callback that creates a visitor session without educational tools; POST is used for Appleform_postPOST /api/auth/logout– Logout and clear both the site login state and the server-side educational-session cookiesGET /api/auth/me– Current user infoGET /api/user-data– Current account conversation, message, and confirmed-memory countsDELETE /api/user-data– Clear saved service data while keeping the account signed inDELETE /api/account– Irreversibly delete service-side account data, revoke all sessions, and sign out
GET /api/models– Available chat modelsGET /api/health– Lightweight liveness checkGET /api/ready– Readiness check for Redis, SQLite writeability, and configured runtime limitsGET /api/metrics– Restricted Prometheus-style operational metricsGET /api/conversations?limit=50&cursor=...– Paginated conversation list with preview and message countsPOST /api/conversations– Create new conversationGET /api/conversations/{id}– Conversation detailDELETE /api/conversations/{id}– Delete conversationPOST /api/conversations/{id}/messages– Stream assistant response (SSE)POST /api/conversations/{id}/messageswithrerun_message_id– Edit or regenerate from an existing user message while preserving the SSE event formatPOST /api/conversations/{id}/messagesmay includecontext.location– One-message transient location context for dynamic reminders and campus recommendations, never persisted to chat historyPOST /api/conversations/{id}/feedback– Save feedbackPOST /api/conversations/{id}/memory-proposals/{tool_id}– Confirm or dismiss a memory save/delete proposal
GET /api/recommendations/locations– Built-in manual campus/location optionsPOST /api/recommendations/signal-refresh– Asynchronously refresh non-sensitive academic summary snapshots used by low-intrusion remindersPOST /api/recommendations/contextual– Generate one-time campus recommendations fromscenario, optional browserlocation,manual_location_id, and optionalseen_grade_digest
Low-intrusion reminders no longer render automatic homepage cards and do not synchronously fetch slow educational-system data when a user sends a message. The backend reads cached non-sensitive summaries plus reminder cooldown state, injects a few dynamic events into a second short SystemMessage, and stores that runtime context once per conversation so later turns append messages without rewriting prior prompt content. Conversation history uses LangChain's approximate token counter and is only trimmed around the 200k-token boundary, preserving tool results for long chats. Browser coordinates are transient per message: they are used server-side for campus recommendations and AMap reverse geocoding, while only the resulting text location can be injected into the current model prompt. Coordinates and text locations are not persisted to conversation storage or long-term memory. Grade summaries store only digests, term labels, and recorded counts, never concrete scores. Mobile geolocation requires an HTTPS origin; plain HTTP server URLs will not show the browser permission prompt. The AMap key stays server-side through environment variables or Docker secrets.
The LLM agent can automatically call these tools only in undergraduate educational-login sessions; visitor mode never binds these personal academic-affairs tools:
query_grades– Course grades and GPAquery_courses– Course schedulequery_student_info– Student profilequery_exam_scores– CET and unified exam scoresrecommend_campus_context– Contextual campus academic, dining, and study recommendations when the user supplies or authorizes a location
# Frontend
cd frontend && npm run lint && npm run build
# Backend
python -m compileall app