Commit Graph
81 Commits
Author SHA1 Message Date
RainbowBird 11b820a77f feat(admin-ui): implement standalone admin UI with Cloudflare Pages deployment 2026-06-09 17:09:06 +08:00
RainbowBird abb6d16b67 feat(server): track blocked TTS preflight 2026-06-06 00:22:11 +08:00
RainbowBird e6fa39ed4b feat(server): voice pack and tts routing (#1905)
Track per-app TTS concurrency in Redis, route capped upstreams by
available pool capacity, and surface pool saturation metrics. Document
the Voice Pack plan so the remaining backend and card-binding work has
an explicit implementation map.
2026-06-05 22:39:30 +08:00
RainbowBird cf5ed0766b fix(server): make analytics best-effort
Remove server-side PostHog wiring so request handlers no longer wait on third-party analytics delivery.

Keep auth activity and product event writes best-effort so degraded analytics do not fail session creation.

Signed-off-by: RainbowBird <git@luoling.moe>

Commit-Message-Assisted-by: Codex (via Codex)
2026-06-05 16:27:10 +08:00
Liet Blue dc6807a21f feat(server-dashboard): add admin UI for AIRI operations (#1942) 2026-06-04 16:16:19 +08:00
RainbowBird 7ac69db4ef feat(server): add product analytics events (#1941) 2026-06-03 23:03:33 +08:00
RainbowBirdandNeko 6f6fe01b3e refactor(server): openai route gateway
Co-authored-by: Neko <neko@ayaka.moe>
2026-06-03 00:21:25 +08:00
RainbowBird f2897f7663 feat(server): total user metrics 2026-05-30 21:20:36 +08:00
RainbowBird dc1037f349 feat(server): user metrics 2026-05-30 00:37:51 +08:00
RainbowBird 0ac9b709a2 refactor(server): integrate better-auth admin plugin (#1873) 2026-05-26 02:04:25 +08:00
RainbowBird be2c355b0e refactor(server): merge STREAMING_TTS_UPSTREAM + UNSPEECH_REST_BASE_URL into UNSPEECH_UPSTREAM
One unspeech deployment = one configKV entry. The split into two keys
modelled a hypothetical split deployment (operators pointing REST and
WS at different unspeech instances) that nobody actually runs, and the
explanatory comments justifying the split were exactly the migration-
narrative anti-pattern we just banned from source.

Schema:

  UNSPEECH_UPSTREAM = {
    restBaseURL: string,                  // required, http(s)://host:port
    streaming?: {                         // optional, only when ws TTS is on
      baseURL: string,                    // ws(s)://host:port/...
      keys: [{ id, ciphertext }],
      adapterParams: {},
      models?: [{ id, name?, description? }],
    },
  }

Admin slice surface flattens to one `kind: 'unspeech'`:

  { kind: 'unspeech', restBaseURL,
    streaming?: { upstreamURL, plaintextKey, keyEntryId? } }

Read-site changes:
- routeTts + listTtsVoices read UNSPEECH_UPSTREAM.restBaseURL via
  getOrThrow; absent entry → 503 CONFIG_NOT_SET.
- audio-speech-ws dials UNSPEECH_UPSTREAM.streaming and 1008-closes
  with streaming_tts_not_configured when the subtree is absent.
- handleListStreamingVoices reads .restBaseURL directly (no more
  ws→http scheme swap) and 503s on missing streaming subtree.
- handleListStreamingTTSModels reads .streaming.models.
- config-sync-subscriber listens for UNSPEECH_UPSTREAM invalidations
  instead of UNSPEECH_REST_BASE_URL.

Drive-by: tighten ConfigKVService.getOrThrow return to
`Exclude<ConfigDefinitions[K], undefined>` so call sites stop needing
non-null assertions on optional schema entries — the runtime already
throws, the type now reflects it.

Migration: operators repost a single admin slice to rewrite the merged
entry. Old STREAMING_TTS_UPSTREAM and UNSPEECH_REST_BASE_URL rows
become dead data and can be deleted from configKV; nothing reads them.
2026-05-19 23:28:42 +08:00
RainbowBird 615e0441e8 refactor(server): proxy TTS through unspeech, drop implicit fallbacks
Frontend voice picker silently lost Azure voices because /audio/voices
was called without ?model=, so the server fell back to DEFAULT_TTS_MODEL
and only ever returned one model's catalog. /audio/models also hardcoded
a single `auto` alias, so the client could never request a specific
model id even if it wanted to. On top of that, the Azure adapter shipped
only 2 hand-typed voices in voices/azure.json — the rest of Microsoft's
catalog was unreachable.

Fixed in one pass:

- Drop voices/*.json. Every getVoiceCatalog now calls unspeech REST
  (backend=microsoft|alibaba|volcengine). Azure proxies Microsoft's live
  voices/list; cosyvoice + volcengine use unspeech's embedded catalogs.
- Drop direct upstream provider calls from send(). All three adapters
  POST to <UNSPEECH_REST_BASE_URL>/v1/audio/speech with the openai-shaped
  body unspeech expects (model: `<backend>/<id>`). Azure SSML still built
  on our side so <prosody rate> speed survives — unspeech accepts
  pre-built <speak> documents.
- Fail-fast on every voices endpoint: empty ?model= → 400 MISSING_MODEL
  (no implicit DEFAULT_TTS_MODEL fallback), only `auto` resolves;
  streaming upstream missing → 503; unspeech non-2xx / network err → 502;
  malformed body → 502 with grepable message. No silent `{voices: []}`.
- /audio/models lists every LLM_ROUTER_CONFIG.tts.models key + `auto`.
- Frontend providerOfficialSpeech.listVoices passes ?model= and throws
  on upstream failure instead of returning [].
- New UNSPEECH_REST_BASE_URL configKV entry (no default, missing → 503).
  config-sync subscriber invalidates the voice cache when this key OR
  LLM_ROUTER_CONFIG changes.
- Router voice catalog Redis cache covers all providers with per-provider
  TTL (azure 6h live, alibaba/volcengine 24h since unspeech embeds them
  at build time).

Pending billing-SKU verification: DashScope cosyvoice now goes through
unspeech's WS-internal alibaba backend (was REST `/SpeechSynthesizer`).
Functional output equivalent — confirm bill after deploy, revert via
git history if WS pricing is materially higher.
2026-05-19 23:14:16 +08:00
RainbowBird c627bce9c9 refactor(server): split services into domain/adapter layers, drop dead code
Why
- src/services/ was an unordered mix of single-file services and module
  directories with no shared classification axis, plus several long-dead
  admin batch helpers that survived the move to the simpler synchronous
  admin-flux-grants flow.

What
- services/ now has two top-level layers:
    domain/   — DB state + business rules (billing, characters, chats,
                flux, flux-transaction, llm-router, providers, request-log,
                stripe, user-deletion, admin/{flux-grants,router-config})
    adapters/ — thin wrappers over external SDKs / infra (config-kv, email,
                posthog, tts/)
- admin/* moved under domain/admin/ with consistent plural names
  (flux-grants, router-config).
- tts-adapters/ collapsed to adapters/tts/ (no redundant -adapters suffix
  once nested under adapters/).
- 63 src files + scripts/e2e-llm-router.ts + tests/verifications/_harness.ts
  had relative imports rewritten; git mv preserves blame.
- apps/server/CLAUDE.md and docs/ai-context/*.md updated to match new paths.

Dead code removed
- services/admin-flux-grant-batches/ (service + worker + tests, 1090 LOC) —
  superseded by admin-flux-grants and never wired into app.ts.
- routes/admin/flux-grant-batches/ — same.
- utils/redis-compressed.ts + test — zero production call sites.
- llm-router/index.ts re-exports trimmed from 26 to 6; only symbols with
  external consumers are kept.

Intentionally kept
- schemas/flux-grant-batch.ts and its schemas/index.ts export remain so the
  drizzle-kit generate diff stays empty. Removing them is a separate PR
  that owns the drop-table migration for flux_grant_batch /
  flux_grant_batch_recipient.

Verification
- pnpm -F @proj-airi/server typecheck: passes.
- pnpm exec eslint apps/server: 49 errors, identical to main baseline
  (all are pre-existing node/prefer-global/buffer in envelope-crypto and
  scripts/e2e-llm-router; untouched by this change).
- Vitest passes per-file; the 6 mockDB hook timeouts under full-parallel
  run are the known pushSchema-per-worker infra cost, not a regression.
2026-05-18 23:36:45 +08:00
RainbowBird 45fb765df7 fix(server): ws conn 401 2026-05-18 23:36:08 +08:00
RainbowBird 8ef5844928 refactor(server): drop seed-router-config / seed-streaming-tts scripts
The two seed scripts are fully superseded by the new admin endpoint
`POST /api/admin/config/router` — same encryption, same configKV
writes, same `configkv:invalidate` publish, plus auth + audit + body
limits. Keeping both code paths created a drift risk on the AAD label
and the merge semantics.

Doc + test fallout:
- `e2e-llm-router.ts` now points readers to the admin endpoint for
  the prerequisite seed step.
- `docs/ai-context/verifications/llm-router.md` and
  `streaming-tts.md` get curl-based seed instructions; the 2026-05-15
  llm-router evidence stays intact with a note that the script it
  used has since been removed.
- The U9 follow-up entry in `llm-router.md` flips from "not shipped"
  to "partially shipped" — ETag + HMAC publish are still deferred,
  so the `config_write` / `config_invalid_hmac` Grafana panels stay
  parked.
- Self-edit on the admin route + `app.ts` docstrings to drop the
  earlier "scripts stay as break-glass" wording.
2026-05-18 23:36:07 +08:00
RainbowBird c1b88701db feat(server): admin endpoint for seeding/patching LLM router config
Replaces routine use of `scripts/seed-router-config.ts` and
`scripts/seed-streaming-tts.ts` with `POST /api/admin/config/router`.
Operators can now patch one provider at a time without shelling into
the Railway runner; the seed scripts stay as break-glass tools for
cold-boot and disaster recovery.

The endpoint accepts a discriminated-union slice list (openrouter /
azure / dashscope-cosyvoice / streaming-tts), envelope-encrypts
plaintext keys in-process (never echoed back), and supports
merge/reset modes plus dryRun. Writes go through the existing
configKV + Redis `configkv:invalidate` channel so multi-instance
deployments pick up changes within the pub/sub propagation window.

Guarded by the existing `authGuard + adminGuard` pair
(`ADMIN_EMAILS` allowlist + verified email).
2026-05-18 23:36:07 +08:00
RainbowBird ba9247fb47 feat(server,stage-ui): bidirectional streaming TTS + audio path refactor
Why:
- Add a real bidirectional streaming TTS path: raw LLM tokens are
  forwarded to the upstream model (Volcengine v3 via the unspeech ws
  bridge) without client-side segmentation, so the model owns sentence
  splitting and audio chunks play as they arrive.
- Move audio endpoints out of /api/v1/openai/. `/audio/voices`,
  `/audio/models`, `/audio/voices/streaming` are not real OpenAI public
  APIs, and the streaming TTS surface has nothing to do with OpenAI —
  keeping them under /openai/ mislabelled the contract.
- Introduce `capabilities.speech.transport` on ProviderDefinition so
  future streaming providers (ElevenLabs / Cartesia / OpenAI Realtime)
  opt in without touching Stage.vue or the session factory.
- Unify Stage.vue's TTS path through a single StageTtsSession so the
  chat-orchestrator hooks no longer branch on provider id.

What:
- apps/server: new ws proxy /api/v1/audio/speech/ws bridges client ↔
  unspeech with auth, pre-flight flux check, billing from upstream
  session.finished.usage, OTel spans.
- apps/server: audio routes moved from /api/v1/openai/audio/* to
  /api/v1/audio/* (hard cutover; 404 sentinel tests added).
- apps/server: new /api/v1/audio/voices/streaming proxy reads voices
  from unspeech /api/voices?provider=volcengine.
- apps/server: new STREAMING_TTS_UPSTREAM configKV entry +
  scripts/seed-streaming-tts.ts.
- stage-ui: new libs/speech/streaming-pipeline.ts opens one ws per LLM
  intent (appendText / finish / cancel + onSentence / onError / onDone).
- stage-ui: new libs/speech/tts-session.ts — StageTtsSession interface
  with segmenter and streaming adapters; factory dispatches by
  capabilities.speech.transport instead of hard-coded provider id.
- stage-ui: providerOfficialSpeechStreaming with capabilities.speech =
  { transport: 'bidirectional-ws' }; settings page with model/voice
  picker + ws-based preview.
- stage-ui: Stage.vue chat hooks collapsed to a single currentSession;
  hot-swap watcher cancels mid-session on provider/voice/model change;
  unmount cancels and drains playback.

Tests:
- 9 streaming-pipeline tests (happy path / buffered / error / cancel /
  truncation)
- 11 tts-session tests (factory branch coverage + adapter contracts)
- 4 audio-speech-ws route tests (forwarding / billing / pre-flight /
  config-missing)
- 3 legacy-path 404 sentinels in v1 route tests
- Verification doc updated to reflect automated coverage.
2026-05-18 23:34:35 +08:00
RainbowBird bf1aba757c feat(server): stream tts provider 2026-05-18 23:33:24 +08:00
RainbowBird 46fb391330 feat(server/llm-router): propagate upstream attempts via ApiError.cause
When `mapUpstreamError` produced the final 502/503/504, it only carried
`{triedKeys, triedUpstreams, lastStatusCode}` in `details`. The upstream
response body was `.cancel()`'d on the wire and the network error
message vanished into the catch arm — operators staring at a 502 had no
way to tell "OpenRouter region-blocked us" from "key revoked" from
"DNS failed" without re-probing the upstream by hand.

Now each recorded failure carries the diagnostic snippet:
- chat upstreams read at most 256 bytes of the failed body via a
  drain-aware reader before cancelling the rest (socket still returns
  to the pool, no fallback-storm pool exhaustion).
- TTS upstreams reuse `errorMessageFrom(err)` — adapters already bake
  the status + body snippet into `err.message`, so one field carries
  both.
- network / timeout attempts record `errorMessageFromUnknown(err)` so
  "attempt-timeout" vs "ECONNRESET" vs "DNS failed" stays
  distinguishable.

The collected `UpstreamAttempt[]` is attached to `ApiError.cause`
rather than `details`. SEC-5 (no upstream content in client-facing
response body) still holds — only the server-side logger + OTel pick
the cause up. `app.onError` now logs `{details, cause}` together so a
single log line tells the operator both the contract-level summary
and the actual upstream payload.

Adds a router.test.ts regression covering both shapes (HTTP 401 body
snippet + network ECONNRESET errorMessage), with an explicit assertion
that `details` does NOT contain the body text so SEC-5 doesn't drift.
2026-05-18 23:32:34 +08:00
RainbowBird 9aef35948c refactor(server): llm router config sync subscriber 2026-05-18 23:32:33 +08:00
RainbowBird 4da4a72703 feat(server): finalize in-process LLM/TTS router cutover
End-state of the multi-step KTD-5 / KTD-6 / U8 work. The knoway sidecar
is no longer reachable from server code; the router is required at boot
and now owns chat completions, TTS synthesis, and voice catalog listing.

Highlights:
- LLM_ROUTER_MASTER_KEY becomes required; app.ts drops the graceful-
  skip branch and the chat fallback fetch path is gone.
- /audio/speech and /audio/voices route through new routeTts /
  listTtsVoices entries that reuse the chat key-rotator + per-attempt
  timeout + abort propagation.
- DEFAULT_CHAT_MODEL / DEFAULT_TTS_MODEL move from env to configKV so
  default-model swaps are hot-reloadable via Pub/Sub.
- GATEWAY_BASE_URL removed from env schema, .env, .env.local, smoke,
  verification harness. Redis upstream-voices cache deleted — catalogs
  come from in-process adapter JSON.
- routeTts splits adapter error contract by ApiError statusCode:
  4xx propagates without fallback; 5xx folds into the network-failure
  fallback path. handleTTS wraps billing + span attribute in try/finally
  to plug a span leak when ttsMeter.accumulate() throws.
- seed-router-config.ts rewritten with --merge (default) / --reset /
  --dry-run modes and env-var key handoff (OPENROUTER_KEY / AZURE_KEY /
  DASHSCOPE_KEY) so prod seed flows never put plaintext on the CLI.
  Adds DashScope CosyVoice seeding.

Docs (CLAUDE.md, architecture-overview.md, transport-and-routes.md)
reflect the new boundary. verifications/llm-router.md replaces the
overstated "U1-U9 shipped" line with an evidence-vs-pending table.

Tests: full 40-file / 343-case server suite green. New regressions pin
ApiError 4xx → no-fallback, ApiError 5xx → fallback, TTS billing
failure → span closed and error propagated.
2026-05-18 23:32:33 +08:00
RainbowBird 5d256e4951 feat(server): replace legacy health endpoints with K8s-style /livez and /readyz probes 2026-05-18 23:32:20 +08:00
RainbowBird af17e9cbc2 feat(server): llm & tts gateway (#1837) 2026-05-18 23:31:07 +08:00
RainbowBird 31376fc6f1 feat(server, stage-ui): metrics 2026-05-18 23:29:14 +08:00
RainbowBird 982da671ef fix(server): ws conn 401 2026-05-18 17:05:49 +08:00
RainbowBird a3a401d79d feat(server): new metrics for auth
- Added a new ObservableGauge for distinct active users to track real active user count, mitigating session row inflation issues.
- Updated the Grafana dashboard to reflect changes, including the removal of redundant WS Connections panel and the addition of new metrics for active sessions and distinct users.
- Improved documentation for verification automation processes, outlining a structured approach to automate verification steps and maintain evidence of tests.
2026-05-15 18:54:05 +08:00
RainbowBird 3984677b01 feat(analytics): integrate PostHog for server-side event tracking
- Added a new PostHog client for capturing server-side business events such as Stripe webhooks and subscription state changes.
- Implemented various tracking functions for pricing funnel steps, character creation, and chat session starts.
- Enhanced the flux meter tests to handle partial charges and report unbilled flux correctly.
- Updated the CharacterDialog and Flux settings pages to track user interactions with analytics events.
- Introduced a mechanism to identify users on PostHog based on authentication state to ensure accurate funnel tracking.
- Added necessary dependencies for PostHog integration in the project.
2026-05-15 16:20:47 +08:00
Lulu 98b1b9c34f feat(server): env-based trusted origins for Capacitor dev (#1763)
## Description

Adds optional env **`ADDITIONAL_TRUSTED_ORIGINS`**: comma-separated
browser origins that are trusted for **CORS (`/api/*`)**, **Stripe
return URLs**, **Better Auth `trustedOrigins`**, and **dynamic web OIDC
redirect URIs**.

LAN / non-localhost Capacitor dev (e.g. Pocket + Vite on
`https://10.x:5273`) no longer relies on broad private-IP regex;
operators list exact origins in `.env.local` and restart the API server
after changes.

## Linked Issues

<!-- N/A -->

## Additional Context

Pocket iOS dev workflow: `cap`/`capacitor.config` often points at a LAN
HTTPS origin; without this allowlist the API rejects those
`Origin`/`Referer`/`redirect_uri` bases. Review can stay focused on
**`apps/server/src/libs/env.ts`**,
**`apps/server/src/utils/origin.ts`**, and wiring in **`app.ts`**,
**Stripe**, **auth routes**.
2026-05-14 16:17:13 +08:00
RainbowBird 272cdae03b feat(server/otel): restructure observability metrics and add active sessions gauge
- Moved RateLimitMetrics import path to a more centralized location.
- Introduced a new file for active sessions gauge to track user sessions in the database.
- Updated index.ts to include new metrics and ensure proper initialization of observability metrics.
- Modified various routes and services to utilize the new observability structure.
- Added smoke tests for HTTP and WebSocket metrics to ensure proper metric registration and functionality.
- Enhanced error handling for metrics reading failures to improve observability.
2026-05-12 23:10:13 +08:00
RainbowBird d24b22a561 refactor(server/otel): hono otel integrate 2026-05-11 12:01:57 +08:00
RainbowBird 21b180ced7 refactor(server/otel): enhance otel setup with improved SDK lifecycle management 2026-05-10 23:43:18 +08:00
RainbowBird 917450fdfb feat(server/otel): enhance observability with new metrics for email, rate limiting, and billing
- Added new metrics for email service including send, failures, and duration tracking.
- Introduced rate limit metrics to monitor blocked requests and improve abuse detection.
- Enhanced billing metrics to track credited and unbilled flux, as well as TTS character processing.
- Updated OpenAI and Stripe routes to utilize new metrics for better revenue tracking and rate limiting.
- Implemented a smoke test for OpenTelemetry metrics registration to ensure visibility at startup.
2026-05-08 23:35:38 +08:00
RainbowBird f8d1fa7a64 refactor(server): drop redis stream + worker role (#1792)
The Redis Stream `billing-events` + `worker` Railway role +
advisory-lock poller layered together didn't actually buy us reliability
— `debitFlux` swallowed XADD failures, leaving the door open to "balance
updated, ledger row never written". Collapse the whole thing back to:
`creditFlux` and `debitFlux` write `flux_transaction` ledger rows inline
within the same DB transaction that mutates `user_flux`, and `(user_id,
request_id)` remains the partial unique index that keeps retries safe.

Concrete changes:
- Inline ledger inserts in `BillingService.{debitFlux, creditFlux,
creditFluxFromStripeCheckout, creditFluxFromInvoice}`; drop `billingMq`
and `publishEvent` plumbing entirely.
- `routes/openai/v1` writes `llm_request_log` synchronously via the
existing `requestLogService`; the duplicate `llm-request-log.ts` service
module is removed.
- `bin/run-worker.ts`, `libs/mq/*`,
`services/billing/billing-events.ts`,
`services/billing/billing-consumer-handler.ts`, and matching tests are
deleted. CLI now exposes only `api`.
- `BILLING_EVENTS_*` env vars and the `DEFAULT_BILLING_EVENTS_STREAM`
helper are dropped; `docker-compose.yml` no longer ships a worker
service.
- `docs/ai-context/{workers-and-runtime, billing-architecture,
redis-boundaries-and-pubsub, data-model-and-state,
architecture-overview, README}.md`, `CLAUDE.md`, and the existing
verification docs are updated to describe the single-process synchronous
pipeline.

Tests: 29 files / 247 cases pass. Production deployments need to drop
the worker Railway service after this lands.
2026-05-08 21:14:01 +08:00
hahaqwq eba047328a revert(server): remove .well-known/assetlinks.json (revert #1772) (#1789) 2026-05-08 21:14:01 +08:00
RainbowBird 99956014fa feat(server): admin api (#1788) 2026-05-08 21:14:00 +08:00
RainbowBird d0e2b450cd feat(stage-ui): chat sync (#1775) 2026-05-08 21:14:00 +08:00
hahaqwqandRainbowBird ae8552695b feat(server): add .well-known/assetlinks.json for Android deeplinks (#1772)
Co-authored-by: RainbowBird <rbxin2003@outlook.com>
2026-05-08 21:14:00 +08:00
RainbowBird 6f0b7e0b9b feat(auth): delete account (#1756) 2026-04-28 20:53:00 +08:00
RainbowBirdandLiet Blue 172e4ce59c feat(auth): email login & profile (#1745)
Co-authored-by: Liet Blue <127093491+lietblue@users.noreply.github.com>
2026-04-28 00:07:38 +08:00
RainbowBird 8967bbe211 feat(server): add TTS support with per-character billing 2026-04-17 02:59:14 +08:00
RainbowBird 57533044f7 feat(server): add gateway and model configuration to environment variables 2026-04-17 02:59:14 +08:00
RainbowBird a11727020a feat(server/billing): implement debt ledger for TTS service using FluxMeter 2026-04-17 02:59:14 +08:00
RainbowBird 93888e6935 feat(server): use stripe product as flux pricing (#1640) 2026-04-12 04:32:42 +08:00
RainbowBird d782313464 feat(auth): jwt sign 2026-04-02 17:48:12 +08:00
RainbowBird 2eb30d6081 docs(auth): ai-context 2026-04-02 17:48:12 +08:00
RainbowBird f1fe161bc0 feat(auth): OIDC (#1531) 2026-04-02 04:24:04 +08:00
RainbowBird 33bf61f3f7 fix(server): no cache 2026-03-30 19:45:43 +08:00
RainbowBird bdb01d30f4 feat(auth): Bearer token handling and OAuth token extraction (#1501) 2026-03-28 20:27:47 +08:00
RainbowBird 4a85654d9b feat(server/otel): follow opentelemetry metrics sharded 2026-03-28 04:57:28 +08:00
RainbowBird 45bc5fef22 feat(server/otel): integrate OpenTelemetry logging and emit logs with context 2026-03-28 03:59:09 +08:00