feat(server): redis, config kv integration

This commit is contained in:
RainbowBird
2026-03-28 02:25:44 +08:00
committed by RainbowBird
parent 391fdd542a
commit 889a1e0800
9 changed files with 90 additions and 18 deletions
+1 -2
View File
@@ -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
+16
View File
@@ -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
+1
View File
@@ -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",
+26 -7
View File
@@ -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<typeof createCharacterService>
type ChatService = ReturnType<typeof createChatService>
type ProviderService = ReturnType<typeof createProviderService>
type FluxService = ReturnType<typeof createFluxService>
type ConfigKVService = ReturnType<typeof createConfigKVService>
type OtelMetrics = ReturnType<typeof initOtel>
@@ -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<HonoEnv>()
@@ -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<typeof buildApp>
@@ -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,
})
+3 -4
View File
@@ -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()),
})
+5
View File
@@ -0,0 +1,5 @@
import Redis from 'ioredis'
export function createRedis(url: string): Redis {
return new Redis(url, { lazyConnect: true })
}
+4 -2
View File
@@ -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<HonoEnv>) {
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',
+29
View File
@@ -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<K extends keyof ConfigDefinitions>(key: K): Promise<ConfigDefinitions[K]> {
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<K extends keyof ConfigDefinitions>(key: K, value: ConfigDefinitions[K]): Promise<void> {
await redis.set(`${KEY_PREFIX}${key}`, String(value))
},
}
}
export type ConfigKVService = ReturnType<typeof createConfigKVService>
+5 -3
View File
@@ -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()
}