162 lines
9.8 KiB
Markdown
162 lines
9.8 KiB
Markdown
# KisAssistant (kisa's assistant)
|
||
|
||
Private ChatGPT/Claude-like chat. React + Tailwind + lucide frontend (rounded,
|
||
Signal-style, Inter font, light/dark/auto themes), Bun backend with
|
||
class-oriented LLM **drivers** and a **SQLite/Postgres** database (Bun native drivers) for
|
||
accounts, server-side chats and public shares. No cookies — Bearer keys in
|
||
`localStorage`.
|
||
|
||
Driver priority:
|
||
|
||
```
|
||
1. ollama → 2. mistral API → 3. glm API → 4. arcaic ×3 (arcaic-openai → arcaic-gemini → arcaic-qwen)
|
||
→ 5. deepseek → 6. anthropic → 7. openai
|
||
```
|
||
|
||
Docs: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) (backend, drivers, DB) ·
|
||
[`docs/FRONTEND.md`](docs/FRONTEND.md) (components, theming) ·
|
||
[`docs/OPERATIONS.md`](docs/OPERATIONS.md) (env, dev, accounts, NixOS) ·
|
||
[`docs/NIXOS-HOSTING.md`](docs/NIXOS-HOSTING.md) (server hosting tutorial).
|
||
Agent instructions: [`AGENTS.md`](AGENTS.md).
|
||
|
||
## Quick start
|
||
|
||
Requirements: [Bun](https://bun.sh) ≥ 1.1, Node 20+ (frontend build), Ollama
|
||
reachable at `OLLAMA_HOST` (default `http://10.66.66.4:11434`).
|
||
|
||
```bash
|
||
cp .env.example .env
|
||
# edit .env → set APP_PASSWORD (openssl rand -base64 24), check OLLAMA_HOST
|
||
|
||
bun install
|
||
bun run --cwd frontend build # builds React app into public/
|
||
bun run dev # http://localhost:3000 (or `bun run start`)
|
||
# frontend dev with proxy: bun run --cwd frontend dev # http://localhost:5173
|
||
```
|
||
|
||
Log in as `admin` with the `APP_PASSWORD` key, then create No-KYC user accounts
|
||
from the **Accounts** panel (each gets a one-time access key to hand over).
|
||
|
||
## Accounts (No-KYC, admin-managed)
|
||
|
||
- `admin` authenticates with `APP_PASSWORD` from `.env`.
|
||
- Admin creates users (`username`, display name, avatar URL, theme) — the API
|
||
returns a `ka_…` access key **once**; keys are stored hashed (sha256).
|
||
- Users log in with `{ username, key }`. Regenerating a key revokes old sessions.
|
||
- Per-account: display name, avatar (uploaded to the built-in CDN from profile
|
||
settings), theme (`auto`/`light`/`dark`), accent color, feature flags.
|
||
|
||
## API
|
||
|
||
| Method | Route | Auth | Notes |
|
||
|---|---|---|---|
|
||
| POST | `/api/auth/login` | rate-limited (5/10min/IP) | `{username, key}` → `{token, expiresAt, user}` |
|
||
| POST | `/api/auth/register` | public, rate-limited | self-registration (403 when disabled by admin) → `{user, key}` |
|
||
| GET | `/api/auth/verify` | Bearer | → `{ok, user}` |
|
||
| POST | `/api/auth/logout` | Bearer | revokes token |
|
||
| GET/PATCH | `/api/me` | Bearer | profile: displayName, avatarUrl, theme |
|
||
| GET/POST | `/api/admin/users` | admin | create returns `{user, key}` (key shown once) |
|
||
| PATCH/DELETE | `/api/admin/users/:id` | admin | edit profile / delete + cascade |
|
||
| POST | `/api/admin/users/:id/regenerate` | admin | new key, old sessions revoked |
|
||
| GET/PATCH | `/api/admin/settings` | admin | feature flags: registrationEnabled, ocrEnabled |
|
||
| GET/POST | `/api/conversations` | Bearer | server-persisted chats (title, topic, model) |
|
||
| GET | `/api/conversations/search?q=` | Bearer | search titles, topics + old prompts (with snippet) |
|
||
| POST | `/api/admin/ollama/pull` | admin | pull a model (library tag or `hf.co/…`, `{name, nodeId?}`, NDJSON progress) |
|
||
| DELETE | `/api/admin/ollama/models/:name` | admin | remove a local Ollama model (`?node=` for a pool node) |
|
||
| GET/POST | `/api/admin/ollama/nodes` | admin | Ollama pool: list (+effective order) / register a host |
|
||
| PATCH/DELETE | `/api/admin/ollama/nodes/:id` | admin | rename / reweight / enable / delete a node |
|
||
| GET | `/api/admin/ollama/nodes/:id/status`, `…/stats?window=` | admin | live probe (version, running, VRAM) / load charts 1h–30d |
|
||
| GET | `/api/admin/ollama/probe?host=` | admin | test a host before registering it |
|
||
| GET | `/api/admin/platform` | admin | this host facts: hostname, kernel, uptime, load, memory, IPs |
|
||
| GET | `/api/tools/tts/info` | Bearer | neural TTS provider + voices (null = browser fallback) |
|
||
| POST | `/api/tools/tts` | Bearer | synthesize `{text, voice?}` → audio bytes (cached, 30/hour) |
|
||
| GET | `/api/admin/ollama/status` | admin | Ollama connectivity probe: reachability, version, on-disk models |
|
||
| POST | `/api/access/request` | public | reserve username+email with a motivation → ticket (`/review/:id`) |
|
||
| GET/POST | `/api/access/ticket/:id` (+`/message`) | ticket bearer | follow + reply on an access request |
|
||
| WS | `/api/access/ws/:id` | ticket bearer | live ticket messages + status (ping/pong heartbeat) |
|
||
| WS | `/api/admin/ws?token=…` | admin | firehose of all access + report events (badges + triage lists, full payloads, no polling) |
|
||
| GET/PATCH/POST | `/api/admin/access…` | admin | wishlist triage: list, accept/refuse/review, reply |
|
||
| POST | `/api/reports` | Bearer | flag an AI response (copyright/gore/falseinfo/bug) → `{report}` (201) |
|
||
| GET/PATCH | `/api/admin/reports`, `/api/admin/reports/:id` | admin | report triage: list, reviewing/resolved/dismissed + note |
|
||
| GET | `/api/admin/mail` | admin | SMTP credentials viewer (password blurred by default in UI) |
|
||
| POST | `/api/admin/mail/verify`, `/api/admin/mail/test` | admin | verify SMTP connectivity / send test mail `{to}` |
|
||
| POST / DELETE | `/api/me/key/rotate`, `/api/me` | Bearer | rotate own key (sessions revoked) / delete own account |
|
||
| GET/POST/DELETE | `/api/me/totp`, `/api/me/totp/setup`, `/api/me/totp/verify` | Bearer | TOTP two-factor status/setup/verify/disable |
|
||
| POST | `/api/auth/totp` | challenge | second login step `{totpToken, code}` → session |
|
||
| GET/PATCH/DELETE | `/api/conversations/:id` | owner | incl. messages on GET |
|
||
| POST/GET/DELETE | `/api/conversations/:id/share` | owner | public link `/share/:publicId` |
|
||
| GET | `/api/conversations/archived` | Bearer | archived chats (hidden from sidebar + search) |
|
||
| POST | `/api/conversations/:id/archive`, `/api/conversations/:id/unarchive` | owner | archive (read-only) / restore to the list |
|
||
| GET | `/api/share/:publicId` | **public** | read-only shared chat JSON |
|
||
| GET | `/api/models` | Bearer | `[{id: "driver:model", …}]` across enabled drivers + your own-key providers |
|
||
| GET/PUT/DELETE | `/api/me/providers`, `/api/me/providers/:id` | Bearer | personal providers (BYOK: openai/anthropic/deepseek/gemini) — keys stored server-side, never returned |
|
||
| GET/PATCH | `/api/admin/model-policy` | admin | per-model kill-switch + hourly/daily limits (users filtered, enforced in chat) |
|
||
| POST | `/api/chat` | Bearer | `{model, messages, conversationId?, stream?}` → JSON or SSE |
|
||
|
||
`POST /api/chat` with `conversationId` persists the user message + assistant
|
||
reply into that conversation (ownership enforced) and auto-titles new chats.
|
||
|
||
## Database (SQLite default, Postgres fallback — both Bun native)
|
||
|
||
`$DATA_DIR/kisassistant.db` (WAL mode), or Postgres when `POSTGRESQL_URL` is
|
||
set (e.g. a docker container — see `docs/OPERATIONS.md`). Tables: `users` (id, username,
|
||
display_name, avatar_url, theme, email, key_hash), `sessions` (hashed Bearer tokens),
|
||
`conversations` (id, user, title, topic, model, archived_at), `messages`, `shares`,
|
||
`resets` (recovery tokens), `settings` (feature flags),
|
||
`user_provider_keys` (per-user BYOK API keys: user_id, provider, api_key)
|
||
(conv ↔ public id). No migration system — schema is created idempotently at boot.
|
||
|
||
## Drivers (backend layout)
|
||
|
||
```
|
||
src/drivers/types.ts LLMDriver interface + DriverModel + errors
|
||
src/drivers/ollama.ts OllamaDriver (priority #1) — /api/tags discovery, NDJSON stream
|
||
src/drivers/apiBase.ts ApiDriverBase (OpenAI-compatible SSE) base class
|
||
src/drivers/apis.ts Mistral / DeepSeek / Anthropic / OpenAI / Gemini drivers (global key or per-user override)
|
||
src/drivers/browser.ts ArcaicBrowserEngine + site configs (openai/qwen/gemini) — throwaway Firefox profile
|
||
src/drivers/arcaic.ts ArcaicSubDriver ×3 — arcaic-openai / arcaic-gemini / arcaic-qwen (ARCAIC_ENABLED)
|
||
src/drivers/registry.ts DriverRegistry — priority order + "driver:model" routing
|
||
src/userProviders.ts personal providers (BYOK): per-user keys, own-key driver factory
|
||
src/db.ts async store (users, sessions, convs, messages, shares) — SQLite/Postgres via DbClient
|
||
src/auth.ts login/sessions/rate-limit (admin key = APP_PASSWORD)
|
||
```
|
||
|
||
Enable an API driver later: `MISTRAL_ENABLED=true` + `MISTRAL_API_KEY=…`, restart.
|
||
|
||
## NixOS hosting (chat.kisakay.com)
|
||
|
||
Full tutorial: **[`docs/NIXOS-HOSTING.md`](docs/NIXOS-HOSTING.md)**.
|
||
|
||
Flake exposes `packages.<system>.default` (backend + prebuilt frontend) and
|
||
`nixosModules.default`. The frontend is built offline inside Nix with Bun from
|
||
`frontend/bun.lock`; on a deps hash mismatch, paste the `got: sha256-…` value
|
||
Nix reports into `bunDepsHash` in `flake.nix`.
|
||
|
||
```nix
|
||
kisassistant.url = "http://git.kisakay.com/k/chat"; # adjust to real repo path
|
||
|
||
imports = [ kisassistant.nixosModules.default ];
|
||
services.kisassistant = {
|
||
enable = true;
|
||
package = kisassistant.packages.${pkgs.system}.default; # required
|
||
enableNginx = true; # nginx reverse proxy + ACME
|
||
domain = "chat.kisakay.com"; # vhost name (required with enableNginx)
|
||
passwordFile = "/run/secrets/kisassistant-password"; # agenix/sops-nix
|
||
ollamaHost = "http://10.66.66.4:11434";
|
||
};
|
||
security.acme.acceptTerms = true; # required for the TLS certificate
|
||
```
|
||
|
||
Nginx terminates TLS; the app listens on `127.0.0.1:3000` with
|
||
`NODE_ENV=production`. SQLite lives in `/var/lib/kisassistant/data`, avatars
|
||
(file CDN) in `/var/lib/kisassistant/cdn`.
|
||
|
||
## Frontend
|
||
|
||
`frontend/` — React 18 + Vite + Tailwind 3 + lucide-react + react-markdown/gfm,
|
||
Inter Variable (self-hosted, no external requests). `bun run --cwd frontend build`
|
||
outputs to `public/` (wiped first). Collapsible sidebar, right-click context menu
|
||
(rename / topic / share / delete), share modals, confirm dialogs, admin accounts
|
||
panel, profile settings, public read-only `/share/:id` page, per-conversation
|
||
URLs (`/chat/:id`, deep-linkable). English only.
|