From 889a1e0800b6409fae05f70f6274430e11e2f574 Mon Sep 17 00:00:00 2001 From: RainbowBird Date: Wed, 4 Mar 2026 22:44:51 +0800 Subject: [PATCH] feat(server): redis, config kv integration --- apps/server/.env | 3 +-- apps/server/docker-compose.yml | 16 ++++++++++++ apps/server/package.json | 1 + apps/server/src/app.ts | 33 +++++++++++++++++++------ apps/server/src/libs/env.ts | 7 +++--- apps/server/src/libs/redis.ts | 5 ++++ apps/server/src/routes/v1completions.ts | 6 +++-- apps/server/src/services/config-kv.ts | 29 ++++++++++++++++++++++ apps/server/src/services/flux.ts | 8 +++--- 9 files changed, 90 insertions(+), 18 deletions(-) create mode 100644 apps/server/src/libs/redis.ts create mode 100644 apps/server/src/services/config-kv.ts diff --git a/apps/server/.env b/apps/server/.env index d454b8fcc..48c07d6e4 100644 --- a/apps/server/.env +++ b/apps/server/.env @@ -1,4 +1,5 @@ DATABASE_URL="postgresql://postgres:example-PAssw0rd-xHjDYR.b7N@db:5432/postgres" +REDIS_URL="redis://localhost:6379" AUTH_GOOGLE_CLIENT_ID="change-me" AUTH_GOOGLE_CLIENT_SECRET="change-me" @@ -14,5 +15,3 @@ BACKEND_LLM_BASE_URL="change-me" CLIENT_URL="change-me" -FLUX_PER_CENT=1 -FLUX_PER_REQUEST=1 diff --git a/apps/server/docker-compose.yml b/apps/server/docker-compose.yml index e66167932..7f8bd4408 100644 --- a/apps/server/docker-compose.yml +++ b/apps/server/docker-compose.yml @@ -18,6 +18,18 @@ services: timeout: 5s retries: 10 + redis: + image: redis:7-alpine + ports: + - '6379:6379' + volumes: + - redis_data:/data + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 5s + timeout: 5s + retries: 10 + server: build: context: ../.. @@ -25,6 +37,8 @@ services: depends_on: db: condition: service_healthy + redis: + condition: service_healthy env_file: - path: .env required: false @@ -41,3 +55,5 @@ services: volumes: db_data: driver: local + redis_data: + driver: local diff --git a/apps/server/package.json b/apps/server/package.json index aed056e7a..1be4c6f1f 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -38,6 +38,7 @@ "drizzle-valibot": "catalog:", "hono": "catalog:", "injeca": "catalog:", + "ioredis": "^5.6.1", "pg": "^8.20.0", "postgres": "^3.4.8", "stripe": "^20.3.0", diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 996a2384f..94c7d8d51 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -14,7 +14,7 @@ import { createLoggLogger, injeca, lifecycle } from 'injeca' import { createAuth } from './libs/auth' import { createDrizzle, migrateDatabase } from './libs/db' import { parsedEnv } from './libs/env' -import { initOtel } from './libs/otel' +import { createRedis } from './libs/redis' import { sessionMiddleware } from './middlewares/auth' import { otelMiddleware } from './middlewares/otel' import { createCharacterRoutes } from './routes/characters' @@ -25,6 +25,7 @@ import { createStripeRoutes } from './routes/stripe' import { createV1CompletionsRoutes } from './routes/v1completions' import { createCharacterService } from './services/characters' import { createChatService } from './services/chats' +import { createConfigKVService } from './services/config-kv' import { createFluxService } from './services/flux' import { createProviderService } from './services/providers' import { ApiError, createInternalError } from './utils/error' @@ -35,6 +36,7 @@ type CharacterService = ReturnType type ChatService = ReturnType type ProviderService = ReturnType type FluxService = ReturnType +type ConfigKVService = ReturnType type OtelMetrics = ReturnType @@ -44,10 +46,11 @@ interface AppDeps { chatService: ChatService providerService: ProviderService fluxService: FluxService + configKV: ConfigKVService env: Env } -function buildApp({ auth, characterService, chatService, providerService, fluxService, env }: AppDeps) { +function buildApp({ auth, characterService, chatService, providerService, fluxService, configKV, env }: AppDeps) { const logger = useLogger('app').useGlobalConfig() const app = new Hono() @@ -115,7 +118,7 @@ function buildApp({ auth, characterService, chatService, providerService, fluxSe /** * V1 routes for official provider. */ - .route('/v1', createV1CompletionsRoutes(fluxService, env)) + .route('/v1', createV1CompletionsRoutes(fluxService, configKV, env)) /** * Flux routes. @@ -125,7 +128,7 @@ function buildApp({ auth, characterService, chatService, providerService, fluxSe /** * Stripe routes. */ - .route('/api/stripe', createStripeRoutes(fluxService, env)) + .route('/api/stripe', createStripeRoutes(fluxService, configKV, env)) } export type AppType = ReturnType @@ -181,19 +184,35 @@ async function createApp() { build: ({ dependsOn }) => createChatService(dependsOn.db), }) + const redis = injeca.provide('services:redis', { + dependsOn: { env: parsedEnv }, + build: async ({ dependsOn }) => { + const redisInstance = createRedis(dependsOn.env.REDIS_URL) + await redisInstance.connect() + logger.log('Connected to Redis') + return redisInstance + }, + }) + + const configKV = injeca.provide('services:configKV', { + dependsOn: { redis }, + build: ({ dependsOn }) => createConfigKVService(dependsOn.redis), + }) + const fluxService = injeca.provide('services:flux', { - dependsOn: { db }, - build: ({ dependsOn }) => createFluxService(dependsOn.db), + dependsOn: { db, configKV }, + build: ({ dependsOn }) => createFluxService(dependsOn.db, dependsOn.configKV), }) await injeca.start() - const resolved = await injeca.resolve({ auth, characterService, chatService, providerService, fluxService, env: parsedEnv }) + const resolved = await injeca.resolve({ auth, characterService, chatService, providerService, fluxService, configKV, env: parsedEnv }) const app = buildApp({ auth: resolved.auth, characterService: resolved.characterService, chatService: resolved.chatService, providerService: resolved.providerService, fluxService: resolved.fluxService, + configKV: resolved.configKV, env: resolved.env, }) diff --git a/apps/server/src/libs/env.ts b/apps/server/src/libs/env.ts index d553eac7e..de0edd947 100644 --- a/apps/server/src/libs/env.ts +++ b/apps/server/src/libs/env.ts @@ -4,12 +4,14 @@ import { env, exit } from 'node:process' import { useLogger } from '@guiiai/logg' import { injeca } from 'injeca' -import { nonEmpty, object, optional, parse, pipe, string, transform } from 'valibot' +import { nonEmpty, object, optional, parse, pipe, string } from 'valibot' const EnvSchema = object({ API_SERVER_URL: optional(string(), 'http://localhost:3000'), CLIENT_URL: optional(string(), 'https://airi.moerui.ai'), + DATABASE_URL: pipe(string(), nonEmpty('DATABASE_URL is required')), + REDIS_URL: pipe(string(), nonEmpty('REDIS_URL is required')), AUTH_GOOGLE_CLIENT_ID: pipe(string(), nonEmpty('AUTH_GOOGLE_CLIENT_ID is required')), AUTH_GOOGLE_CLIENT_SECRET: pipe(string(), nonEmpty('AUTH_GOOGLE_CLIENT_SECRET is required')), @@ -19,9 +21,6 @@ const EnvSchema = object({ STRIPE_SECRET_KEY: optional(string()), STRIPE_WEBHOOK_SECRET: optional(string()), - FLUX_PER_CENT: optional(pipe(string(), transform(Number)), '1'), - FLUX_PER_REQUEST: optional(pipe(string(), transform(Number)), '1'), - BACKEND_LLM_BASE_URL: optional(string()), BACKEND_LLM_API_KEY: optional(string()), }) diff --git a/apps/server/src/libs/redis.ts b/apps/server/src/libs/redis.ts new file mode 100644 index 000000000..c545849c0 --- /dev/null +++ b/apps/server/src/libs/redis.ts @@ -0,0 +1,5 @@ +import Redis from 'ioredis' + +export function createRedis(url: string): Redis { + return new Redis(url, { lazyConnect: true }) +} diff --git a/apps/server/src/routes/v1completions.ts b/apps/server/src/routes/v1completions.ts index cfdb7af85..82b05612f 100644 --- a/apps/server/src/routes/v1completions.ts +++ b/apps/server/src/routes/v1completions.ts @@ -1,6 +1,7 @@ import type { Context } from 'hono' import type { Env } from '../libs/env' +import type { ConfigKVService } from '../services/config-kv' import type { FluxService } from '../services/flux' import type { HonoEnv } from '../types/hono' @@ -17,7 +18,7 @@ const SAFE_RESPONSE_HEADERS = new Set([ 'cache-control', ]) -export function createV1CompletionsRoutes(fluxService: FluxService, env: Env) { +export function createV1CompletionsRoutes(fluxService: FluxService, configKV: ConfigKVService, env: Env) { async function handleCompletion(c: Context) { const user = c.get('user')! const flux = await fluxService.getFlux(user.id) @@ -27,7 +28,8 @@ export function createV1CompletionsRoutes(fluxService: FluxService, env: Env) { const body = await c.req.json() - await fluxService.consumeFlux(user.id, env.FLUX_PER_REQUEST) + const fluxPerRequest = await configKV.get('FLUX_PER_REQUEST') + await fluxService.consumeFlux(user.id, fluxPerRequest) const response = await fetch(`${env.BACKEND_LLM_BASE_URL}chat/completions`, { method: 'POST', diff --git a/apps/server/src/services/config-kv.ts b/apps/server/src/services/config-kv.ts new file mode 100644 index 000000000..6a00ea98c --- /dev/null +++ b/apps/server/src/services/config-kv.ts @@ -0,0 +1,29 @@ +import type Redis from 'ioredis' + +import { createServiceUnavailableError } from '../utils/error' + +interface ConfigDefinitions { + FLUX_PER_CENT: number + FLUX_PER_REQUEST: number + INITIAL_USER_FLUX: number +} + +const KEY_PREFIX = 'config:' + +export function createConfigKVService(redis: Redis) { + return { + async get(key: K): Promise { + const raw = await redis.get(`${KEY_PREFIX}${key}`) + if (raw === null) + throw createServiceUnavailableError(`Config key "${key}" is not set in Redis`, 'CONFIG_NOT_SET') + + return Number(raw) as ConfigDefinitions[K] + }, + + async set(key: K, value: ConfigDefinitions[K]): Promise { + await redis.set(`${KEY_PREFIX}${key}`, String(value)) + }, + } +} + +export type ConfigKVService = ReturnType diff --git a/apps/server/src/services/flux.ts b/apps/server/src/services/flux.ts index 3ac4ade6e..89f0f498c 100644 --- a/apps/server/src/services/flux.ts +++ b/apps/server/src/services/flux.ts @@ -1,4 +1,5 @@ import type { Database } from '../libs/db' +import type { ConfigKVService } from './config-kv' import { and, eq, gte, sql } from 'drizzle-orm' @@ -6,7 +7,7 @@ import { createPaymentRequiredError } from '../utils/error' import * as schema from '../schemas/flux' -export function createFluxService(db: Database) { +export function createFluxService(db: Database, configKV: ConfigKVService) { return { async getFlux(userId: string) { let record = await db.query.userFlux.findFirst({ @@ -14,9 +15,10 @@ export function createFluxService(db: Database) { }) if (!record) { - [record] = await db.insert(schema.userFlux).values({ + const initialFlux = await configKV.get('INITIAL_USER_FLUX') + ;[record] = await db.insert(schema.userFlux).values({ userId, - flux: 100, // Default initial flux + flux: initialFlux, }).returning() }