diff --git a/apps/server/CLAUDE.md b/apps/server/CLAUDE.md new file mode 100644 index 000000000..31bc3d946 --- /dev/null +++ b/apps/server/CLAUDE.md @@ -0,0 +1,67 @@ +# Server CLAUDE.md + +Agent-facing guide for `apps/server`. Detailed topic docs live in `docs/ai-context/` — read the relevant file before modifying that area. + +## Overview + +Hono-based Node.js backend. Owns auth, billing, chat sync, LLM gateway forwarding, and observability. **Multi-instance deployed on Railway** — design all features assuming N>1 instances sharing the same Postgres and Redis. + +## Deployment Model + +- Hosted on **Railway**, multiple instances behind a load balancer. +- Each instance runs one CLI role: `api` or `billing-consumer` (see `src/bin/run.ts`). +- Stateless per-instance: no local state that matters across requests. +- Cross-instance coordination via Redis Pub/Sub (WebSocket broadcast) and Redis Streams (billing events). +- Rate limiting is currently **in-memory** (not distributed) — keep this in mind when adding rate-sensitive features. + +## Tech Stack + +Hono, Better Auth (OIDC provider, RS256 JWT), Drizzle ORM, PostgreSQL, Redis, Stripe, OpenTelemetry, Valibot, injeca (DI), tsx. + +## Commands + +```sh +pnpm -F @proj-airi/server dev # dev with dotenvx (.env.local) +pnpm -F @proj-airi/server typecheck +pnpm -F @proj-airi/server exec vitest run # all server tests +pnpm exec vitest run apps/server/src/... # single test file +pnpm -F @proj-airi/server db:generate # drizzle-kit generate +pnpm -F @proj-airi/server db:push # drizzle-kit push +pnpm -F @proj-airi/server auth:generate # better-auth → src/schemas/accounts.ts +``` + +Local observability: `docker compose -f apps/server/docker-compose.otel.yml up -d` + +## Architecture Summary + +**Entry & DI**: `src/app.ts` (`createApp()`) → logger, env, OTel, Postgres/Redis, DB migrations, services via `injeca`, routes/middleware. CLI entry `src/bin/run.ts`. + +**Layering**: +- **Routes** (`src/routes/`): thin — param validation (Valibot), auth guards, error mapping. No business logic here. +- **Services** (`src/services/`): core business logic and DB transactions. +- **Schemas** (`src/schemas/`): Drizzle table definitions. Migrations in `@proj-airi/server-schema`. + +**Middleware chain** (`/api/*`): CORS → hono/logger → optional otel → sessionMiddleware → bodyLimit(1MB) → per-route guards. WebSocket `/ws/chat` registered before bodyLimit. + +**Error model**: `ApiError(statusCode, errorCode, message, details)` in `src/utils/error.ts`. + +## Key Design Decisions + +- **Flux read/write separation**: `FluxService` reads (Redis cache-aside), `BillingService` writes (Postgres tx + Redis Stream XADD). Never put write-balance logic in `flux.ts`. +- **LLM gateway proxy**: `/api/v1/openai` forwards to `GATEWAY_BASE_URL`. Server handles auth/billing/logging — not model execution. +- **Redis is cache + messaging, not truth**: balance cache, app_settings read cache, WS cross-instance pub/sub, billing event streams. Truth is always Postgres. +- **Auth**: Better Auth + OIDC. `sessionMiddleware` fills context but doesn't block; `authGuard` returns 401. +- **Multi-instance safe**: all writes go through Postgres transactions; cross-instance messaging uses Redis Pub/Sub and Streams. No in-process singletons that hold mutable state across requests. + +## Detailed Context Docs + +See `docs/ai-context/README.md` for the full index. Key files: +- `architecture-overview.md` — entry, DI, assembly, boundaries +- `transport-and-routes.md` — API surface, route→service mapping +- `data-model-and-state.md` — tables, state ownership, caching +- `billing-architecture.md` — Flux/Stripe/outbox/Streams +- `redis-boundaries-and-pubsub.md` — Redis key/channel boundaries +- `auth-and-oidc.md` — auth flows, OIDC, trusted clients +- `config-and-naming-conventions.md` — configKV, naming rules +- `workers-and-runtime.md` — CLI roles, outbox, Streams consumer +- `observability-conventions.md` — OTel naming, custom attributes diff --git a/apps/server/docs/ai-context/billing-architecture.md b/apps/server/docs/ai-context/billing-architecture.md index 760373230..cc8135e94 100644 --- a/apps/server/docs/ai-context/billing-architecture.md +++ b/apps/server/docs/ai-context/billing-architecture.md @@ -49,6 +49,10 @@ Stream: `billing-events` - `api` — HTTP 服务 - `billing-consumer` — 消费 Redis Stream,异步写入 transaction log、LLM 请求日志到 DB +### Stripe 定价 + +Flux 充值定价完全由 Stripe Product/Price 管理,详见 [stripe-pricing.md](stripe-pricing.md)。 + ## 关键服务 ### BillingService (`services/billing-service.ts`) diff --git a/apps/server/docs/ai-context/stripe-pricing.md b/apps/server/docs/ai-context/stripe-pricing.md new file mode 100644 index 000000000..1506b3d6b --- /dev/null +++ b/apps/server/docs/ai-context/stripe-pricing.md @@ -0,0 +1,137 @@ +# Stripe Pricing Architecture + +## 设计决策 + +Stripe 是 Flux 充值定价的**单一真相源**。服务端不再在 Redis 维护 `FLUX_PACKAGES`,所有 package 信息直接从 Stripe API 获取。 + +### 为什么不用 Redis 维护 packages + +之前的设计在 Redis ConfigKV 中维护 `FLUX_PACKAGES`(含 amount、label、price 等),导致: + +- 价格信息在 Stripe 和 Redis 之间重复维护 +- currency 硬编码为 USD,无法支持微信支付(需要 CNY/GBP) +- 新增/修改 package 需要同时改 Stripe 和 Redis + +现在只需在 Stripe Dashboard 操作 Product/Price,服务端自动同步。 + +## 数据模型 + +### Stripe 侧 + +- **Product** — 代表 "Flux 充值" 这个商品(一个即可) +- **Price** — 代表一个具体的价格方案,每个 Price 包含: + - `unit_amount` + `currency`(如 300 USD = $3) + - `currency_options`(可选)— 支持多币种展示,如 `cny: { unit_amount: 2200 }` + - `metadata.fluxAmount` — 购买此 Price 获得的 Flux 数量 + - `metadata.recommended` — (可选)设为 `'true'` 时前端会高亮展示为推荐套餐 + +Product ID 存储在 ConfigKV `STRIPE_FLUX_PRODUCT_ID` 中(运营配置,非环境变量)。 + +### 多币种支持 + +通过 Stripe Price 的 `currency_options` 实现。一个 USD Price 可以同时支持 CNY 结算: + +- 前端展示所有可用货币(从 `currency_options` 自动提取),用户通过 SelectTab 切换 +- 前端 checkout 时传 `{ stripePriceId, currency }` 给服务端 +- 服务端在 Checkout Session 上设 `currency` 参数,Stripe 自动用对应 `currency_options` 的金额 +- Stripe Checkout 页面根据货币自动展示兼容的支付方式(如 CNY → 微信支付) + +**创建带多币种的 Price:** + +```bash +curl https://api.stripe.com/v1/prices \ + -u "$STRIPE_API_KEY:" \ + -d "product=prod_xxx" \ + -d "unit_amount=300" \ + -d "currency=usd" \ + -d "metadata[fluxAmount]=500" \ + -d "currency_options[cny][unit_amount]=2200" +``` + +> 注意:Stripe CLI 的 `prices create` 对嵌套参数支持不好,`currency_options` 需要用 `curl` 直接调 API。 + +### 支付方式 + +`STRIPE_PAYMENT_METHODS` 在 ConfigKV 中为可选配置: + +- **未设置(推荐)**:不传 `payment_method_types`,Stripe 根据 Dashboard 设置和货币自动决定 +- **已设置**:覆盖 Stripe 自动选择,如 `["card", "wechat_pay", "alipay"]` + +如果手动指定了 `wechat_pay`,还需设 `STRIPE_PAYMENT_METHOD_OPTIONS` 为 `{"wechat_pay":{"client":"web"}}`。 + +## 缓存 + +Stripe Price 列表通过 Redis 缓存(key: `cache:stripe:prices`,TTL 5 分钟),所有实例共享。 + +- 命中:直接返回缓存的 Price 列表 +- 未命中:调 Stripe API `prices.list` (含 `expand: ['data.currency_options']`),按 `unit_amount` 升序排列后写入缓存 +- Checkout 时如果 priceId 不在缓存中,fallback 到 `prices.retrieve` 并 invalidate 缓存 + +## API 流程 + +### GET /api/v1/stripe/packages + +1. 从 ConfigKV 读取 `STRIPE_FLUX_PRODUCT_ID` +2. 从 Redis 缓存或 Stripe API 获取 active prices +3. 返回每个 price 的所有可用货币价格: + ```json + { + "stripePriceId": "price_xxx", + "label": "500 Flux", + "defaultCurrency": "usd", + "currencies": { "usd": "$3.00", "cny": "¥22.00" }, + "recommended": false + } + ``` + +### POST /api/v1/stripe/checkout + +1. 前端发送 `{ stripePriceId, currency? }` +2. 服务端验证 price 归属和 `fluxAmount` metadata +3. 创建 Checkout Session: + - `currency` 参数(如有)让 Stripe 用 `currency_options` 中的金额 + - `payment_method_types` 根据 ConfigKV 是否配置决定传或不传 +4. Webhook 收到 `checkout.session.completed` 后从 metadata 读取 fluxAmount 充值 + +## 运营操作 + +### 新增价格 + +用 curl 创建带多币种的 Price(Stripe CLI 不支持嵌套参数): + +```bash +curl https://api.stripe.com/v1/prices \ + -u "$STRIPE_API_KEY:" \ + -d "product=prod_xxx" \ + -d "unit_amount=1200" \ + -d "currency=usd" \ + -d "metadata[fluxAmount]=2000" \ + -d "metadata[recommended]=true" \ + -d "currency_options[cny][unit_amount]=8800" +``` + +无需修改代码或 Redis,`/packages` 端点在缓存过期后自动返回新 Price。 + +### 下架价格 + +在 Stripe Dashboard 将 Price 设为 inactive,缓存过期后 `/packages` 自动不再返回。 + +### 修改 Product ID + +```bash +redis-cli SET "config:STRIPE_FLUX_PRODUCT_ID" '"prod_new_id"' +``` + +### 手动清缓存(立即生效) + +```bash +redis-cli DEL "cache:stripe:prices" +``` + +## ConfigKV 配置清单 + +| Key | 类型 | 默认 | 说明 | +|-----|------|------|------| +| `STRIPE_FLUX_PRODUCT_ID` | `string?` | 无 | Stripe Product ID,未设置时 top-up 不可用 | +| `STRIPE_PAYMENT_METHODS` | `string[]?` | 无 | 不设则 Stripe 自动决定;设了则覆盖 | +| `STRIPE_PAYMENT_METHOD_OPTIONS` | `Record?` | `{}` | 支付方式选项,如 `{"wechat_pay":{"client":"web"}}` | diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index b550f4bf7..cdd5d275e 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -132,7 +132,12 @@ export async function buildApp(deps: AppDeps) { }) .onError((err, c) => { if (err instanceof ApiError) { - logger.withError(err).warn('API error occurred') + if (err.statusCode >= 500) { + logger.withError(err).error('API error occurred') + } + else if (err.statusCode !== 401) { + logger.withError(err).warn('API error occurred') + } return c.json({ error: err.errorCode, @@ -193,7 +198,7 @@ export async function buildApp(deps: AppDeps) { /** * Stripe routes. */ - .route('/api/v1/stripe', createStripeRoutes(deps.fluxService, deps.stripeService, deps.billingService, deps.configKV, deps.env, deps.otel?.revenue)) + .route('/api/v1/stripe', createStripeRoutes(deps.fluxService, deps.stripeService, deps.billingService, deps.configKV, deps.env, deps.redis, deps.otel?.revenue)) return { app: builtApp, injectWebSocket } } diff --git a/apps/server/src/routes/flux/index.ts b/apps/server/src/routes/flux/index.ts index 44c2aeda0..8f2739811 100644 --- a/apps/server/src/routes/flux/index.ts +++ b/apps/server/src/routes/flux/index.ts @@ -19,6 +19,11 @@ export function createFluxRoutes( const flux = await fluxService.getFlux(user.id) return c.json(flux) }) + .get('/stats', async (c) => { + const user = c.get('user')! + const stats = await fluxTransactionService.getStats(user.id) + return c.json(stats) + }) .get('/history', async (c) => { const user = c.get('user')! const { limit, offset } = parse(LimitOffsetPaginationQuerySchema, { diff --git a/apps/server/src/routes/openai/v1/route.test.ts b/apps/server/src/routes/openai/v1/route.test.ts index abda74f09..8a55ef87a 100644 --- a/apps/server/src/routes/openai/v1/route.test.ts +++ b/apps/server/src/routes/openai/v1/route.test.ts @@ -406,6 +406,46 @@ describe('v1CompletionsRoutes', () => { }) }) + describe('gET /api/v1/openai/audio/models', () => { + it('should return configured TTS model from config', async () => { + const app = createTestApp(createMockFluxService(), createMockConfigKV({ DEFAULT_TTS_MODEL: 'microsoft/v1' })) + + const res = await app.fetch( + new Request('http://localhost/api/v1/openai/audio/models', { method: 'GET' }), + { user: testUser } as any, + ) + + expect(res.status).toBe(200) + const data = await res.json() as { models: { id: string, name: string }[] } + expect(data.models).toHaveLength(1) + expect(data.models[0].id).toBe('microsoft/v1') + }) + + it('should return 401 when unauthenticated', async () => { + const app = createTestApp(createMockFluxService(), createMockConfigKV()) + + const res = await app.request('/api/v1/openai/audio/models', { method: 'GET' }) + expect(res.status).toBe(401) + }) + + it('should return 503 when DEFAULT_TTS_MODEL is not configured', async () => { + const configKV = createMockConfigKV() + configKV.getOptional = vi.fn(async (key: string) => { + if (key === 'DEFAULT_TTS_MODEL') + return null + return (configKV as any).__defaults?.[key] ?? null + }) + + const app = createTestApp(createMockFluxService(), configKV) + + const res = await app.fetch( + new Request('http://localhost/api/v1/openai/audio/models', { method: 'GET' }), + { user: testUser } as any, + ) + expect(res.status).toBe(503) + }) + }) + describe('route matching', () => { it('gET /api/v1/openai/chat/completions should return 404', async () => { const app = createTestApp(createMockFluxService(), createMockConfigKV()) diff --git a/apps/server/src/routes/stripe/index.ts b/apps/server/src/routes/stripe/index.ts index 8f2bcf831..899285bef 100644 --- a/apps/server/src/routes/stripe/index.ts +++ b/apps/server/src/routes/stripe/index.ts @@ -1,3 +1,5 @@ +import type Redis from 'ioredis' + import type { Env } from '../../libs/env' import type { RevenueMetrics } from '../../libs/otel' import type { BillingService } from '../../services/billing/billing-service' @@ -13,57 +15,158 @@ import { Hono } from 'hono' import { safeParse } from 'valibot' import { authGuard } from '../../middlewares/auth' -import { configGuard } from '../../middlewares/config-guard' import { rateLimiter } from '../../middlewares/rate-limit' import { createBadRequestError, createServiceUnavailableError } from '../../utils/error' import { errorMessageFromUnknown } from '../../utils/error-message' import { resolveTrustedRequestOrigin } from '../../utils/origin' +import { createRedisKey } from '../../utils/redis-keys' import { CheckoutBodySchema } from './schema' const logger = useLogger('stripe') +const PRICES_CACHE_KEY = createRedisKey('cache', 'stripe', 'prices') +const PRICES_CACHE_TTL_SEC = 5 * 60 + +interface CachedCurrencyOption { + unitAmount: number | null +} + +interface CachedPrice { + id: string + unitAmount: number | null + currency: string + product: string + active: boolean + metadata: Record + currencyOptions: Record +} + export function createStripeRoutes( fluxService: FluxService, stripeService: StripeService, billingService: BillingService, configKV: ConfigKVService, env: Env, + redis: Redis, metrics?: RevenueMetrics | null, ) { const stripe = env.STRIPE_SECRET_KEY ? new Stripe(env.STRIPE_SECRET_KEY) : null - const fluxConfigGuard = configGuard(configKV, ['FLUX_PACKAGES'], 'Top-up is not available yet') + async function getActivePrices(productId: string): Promise { + // Try Redis cache first + const cached = await redis.get(PRICES_CACHE_KEY) + if (cached) { + try { + const parsed = JSON.parse(cached) as { productId: string, prices: CachedPrice[] } + if (parsed.productId === productId) + return parsed.prices + } + catch { /* corrupted cache, refetch */ } + } + + let result: Stripe.ApiList + try { + result = await stripe!.prices.list({ product: productId, active: true, expand: ['data.currency_options'] }) + } + catch (err) { + logger.withError(err).warn('Failed to fetch prices from Stripe') + return [] + } + const prices: CachedPrice[] = result.data + .sort((a, b) => (a.unit_amount ?? 0) - (b.unit_amount ?? 0)) + .map(p => ({ + id: p.id, + unitAmount: p.unit_amount, + currency: p.currency, + product: typeof p.product === 'string' ? p.product : p.product.id, + active: p.active, + metadata: p.metadata, + currencyOptions: Object.fromEntries( + Object.entries(p.currency_options ?? {}).map(([cur, opt]) => [cur, { unitAmount: opt.unit_amount }]), + ), + })) + + await redis.set(PRICES_CACHE_KEY, JSON.stringify({ productId, prices }), 'EX', PRICES_CACHE_TTL_SEC) + return prices + } return new Hono() .get('/packages', async (c) => { - const packages = await configKV.get('FLUX_PACKAGES') - return c.json(packages) + const fluxProductId = await configKV.getOptional('STRIPE_FLUX_PRODUCT_ID') + if (!stripe || !fluxProductId) + return c.json([]) + + const prices = await getActivePrices(fluxProductId) + + // Build per-currency price map for each package + return c.json(prices.map((p) => { + const currencies: Record = { + [p.currency]: formatPrice(p.unitAmount, p.currency), + } + for (const [cur, opt] of Object.entries(p.currencyOptions)) { + currencies[cur] = formatPrice(opt.unitAmount, cur) + } + + return { + stripePriceId: p.id, + label: `${p.metadata.fluxAmount ?? '?'} Flux`, + defaultCurrency: p.currency, + currencies, + recommended: p.metadata.recommended === 'true', + } + })) }) - .post('/checkout', authGuard, rateLimiter({ max: 10, windowSec: 60 }), fluxConfigGuard, async (c) => { - if (!stripe) + .post('/checkout', authGuard, rateLimiter({ max: 10, windowSec: 60 }), async (c) => { + const fluxProductId = await configKV.getOptional('STRIPE_FLUX_PRODUCT_ID') + if (!stripe || !fluxProductId) throw createServiceUnavailableError('Stripe is not configured', 'STRIPE_NOT_CONFIGURED') const user = c.get('user')! const body = await c.req.json() - const maxCheckoutAmount = await configKV.get('MAX_CHECKOUT_AMOUNT_CENTS') const result = safeParse(CheckoutBodySchema, body) if (!result.success) - throw createBadRequestError('Invalid checkout amount', 'INVALID_REQUEST', result.issues) + throw createBadRequestError('Invalid checkout request', 'INVALID_REQUEST', result.issues) - const { amount } = result.output - if (amount > maxCheckoutAmount) { - throw createBadRequestError('Invalid checkout amount', 'INVALID_REQUEST', { - amount, - maxCheckoutAmount, - }) + const { stripePriceId, currency } = result.output + + // Validate against cached prices first, fall back to direct Stripe API + const cachedPrices = await getActivePrices(fluxProductId) + let price = cachedPrices.find(p => p.id === stripePriceId) + + if (!price) { + // Cache miss — price may have just been created + let fetched: Stripe.Price + try { + fetched = await stripe.prices.retrieve(stripePriceId) + } + catch { + throw createBadRequestError('Invalid price', 'INVALID_PACKAGE', { stripePriceId }) + } + + if (!fetched.active || (typeof fetched.product === 'string' ? fetched.product : fetched.product.id) !== fluxProductId) { + throw createBadRequestError('Invalid price', 'INVALID_PACKAGE', { stripePriceId }) + } + + price = { + id: fetched.id, + unitAmount: fetched.unit_amount, + currency: fetched.currency, + product: typeof fetched.product === 'string' ? fetched.product : fetched.product.id, + active: fetched.active, + metadata: fetched.metadata, + currencyOptions: Object.fromEntries( + Object.entries(fetched.currency_options ?? {}).map(([cur, opt]) => [cur, { unitAmount: opt.unit_amount }]), + ), + } + + // Invalidate cache so all instances pick up the new price + await redis.del(PRICES_CACHE_KEY) } - // Match amount to a configured package so we know the fluxAmount - const packages = await configKV.get('FLUX_PACKAGES') - const pkg = packages.find(p => p.amount === amount) - if (!pkg) { - throw createBadRequestError('No matching package for the given amount', 'INVALID_PACKAGE', { amount }) + const fluxAmount = Number(price.metadata.fluxAmount) + if (!Number.isFinite(fluxAmount) || fluxAmount <= 0) { + throw createBadRequestError('Price is missing fluxAmount metadata', 'INVALID_PACKAGE', { stripePriceId }) } // Reuse existing stripe customer if available @@ -75,20 +178,17 @@ export function createStripeRoutes( throw createBadRequestError('Missing trusted request origin', 'INVALID_ORIGIN') } + const paymentMethods = await configKV.getOptional('STRIPE_PAYMENT_METHODS') + const paymentMethodOptions = await configKV.getOptional('STRIPE_PAYMENT_METHOD_OPTIONS') ?? {} + const session = await stripe.checkout.sessions.create({ - payment_method_types: ['card'], - line_items: [ - { - price_data: { - currency: 'usd', - product_data: { - name: 'Flux Top-up', - }, - unit_amount: amount, - }, - quantity: 1, - }, - ], + // When STRIPE_PAYMENT_METHODS is not set, omit payment_method_types to let Stripe + // automatically determine available methods based on currency and Dashboard settings + ...(paymentMethods && { payment_method_types: paymentMethods as any }), + ...(Object.keys(paymentMethodOptions).length > 0 && { payment_method_options: paymentMethodOptions as any }), + // When currency is specified, Stripe uses the matching currency_options on the Price + ...(currency && { currency }), + line_items: [{ price: stripePriceId, quantity: 1 }], mode: 'payment', allow_promotion_codes: true, success_url: `${redirectBase}/settings/flux?success=true`, @@ -97,7 +197,7 @@ export function createStripeRoutes( customer_email: stripeCustomerId ? undefined : user.email, metadata: { userId: user.id, - fluxAmount: String(pkg.fluxAmount), + fluxAmount: String(fluxAmount), }, }) @@ -377,3 +477,19 @@ async function handleInvoiceEvent( logger.withFields({ userId: customer.userId, invoiceId: invoice.id, amountPaid: invoice.amount_paid }).warn('Subscription invoice paid but flux crediting for subscriptions is not yet implemented') } } + +/** Format Stripe smallest-unit amount into a human-readable price string */ +export function formatPrice(unitAmount: number | null, currency: string): string { + if (unitAmount == null) + return currency.toUpperCase() + + try { + const formatter = new Intl.NumberFormat('en-US', { style: 'currency', currency }) + const fractionDigits = formatter.resolvedOptions().minimumFractionDigits ?? 2 + const amount = unitAmount / (10 ** fractionDigits) + return formatter.format(amount) + } + catch { + return `${unitAmount / 100} ${currency.toUpperCase()}` + } +} diff --git a/apps/server/src/routes/stripe/route.test.ts b/apps/server/src/routes/stripe/route.test.ts index fee5f3e9a..99f3b80f3 100644 --- a/apps/server/src/routes/stripe/route.test.ts +++ b/apps/server/src/routes/stripe/route.test.ts @@ -8,7 +8,7 @@ import type { HonoEnv } from '../../types/hono' import { Hono } from 'hono' import { describe, expect, it, vi } from 'vitest' -import { createStripeRoutes } from '.' +import { createStripeRoutes, formatPrice } from '.' import { ApiError } from '../../utils/error' // --- Mock helpers --- @@ -46,8 +46,8 @@ function createMockBillingService(): BillingService { function createMockConfigKV(overrides: Record = {}): ConfigKVService { const defaults: Record = { - FLUX_PACKAGES: [{ amount: 500, fluxAmount: 5000, label: '5000 Flux', price: '$5' }], - MAX_CHECKOUT_AMOUNT_CENTS: 1_000_000, + STRIPE_FLUX_PRODUCT_ID: 'prod_test_flux', + STRIPE_PAYMENT_METHODS: ['card'], ...overrides, } return { @@ -62,6 +62,15 @@ function createMockConfigKV(overrides: Record = {}): ConfigKVServic } as any } +function createMockRedis(): any { + const store = new Map() + return { + get: vi.fn(async (key: string) => store.get(key) ?? null), + set: vi.fn(async (key: string, value: string) => { store.set(key, value) }), + del: vi.fn(async (key: string) => { store.delete(key) }), + } +} + const testEnv = { STRIPE_SECRET_KEY: 'sk_test_fake', STRIPE_WEBHOOK_SECRET: 'whsec_test_fake', @@ -123,8 +132,9 @@ function createTestApp( stripeService: StripeService, billingService: BillingService, configKV: ConfigKVService, + envOverrides: Record = {}, ) { - const routes = createStripeRoutes(fluxService, stripeService, billingService, configKV, testEnv) + const routes = createStripeRoutes(fluxService, stripeService, billingService, configKV, { ...testEnv, ...envOverrides }, createMockRedis()) const app = new Hono() app.onError((err, c) => { @@ -153,30 +163,43 @@ function createTestApp( // --- Tests --- +describe('formatPrice', () => { + it('formats USD cents correctly', () => { + expect(formatPrice(300, 'usd')).toBe('$3.00') + expect(formatPrice(1200, 'usd')).toBe('$12.00') + expect(formatPrice(2500, 'usd')).toBe('$25.00') + }) + + it('formats CNY cents correctly', () => { + expect(formatPrice(2100, 'cny')).toBe('CN¥21.00') + }) + + it('formats JPY (zero-decimal currency) correctly', () => { + expect(formatPrice(500, 'jpy')).toBe('¥500') + }) + + it('formats GBP correctly', () => { + expect(formatPrice(1599, 'gbp')).toBe('£15.99') + }) + + it('returns currency code for null amount', () => { + expect(formatPrice(null, 'usd')).toBe('USD') + }) + + it('handles zero amount', () => { + expect(formatPrice(0, 'usd')).toBe('$0.00') + }) +}) + describe('stripeRoutes', () => { describe('gET /api/v1/stripe/packages', () => { - it('returns configured packages', async () => { + it('returns empty array when Stripe is not configured', async () => { const app = createTestApp( createMockFluxService(), createMockStripeService(), createMockBillingService(), - createMockConfigKV(), - ) - - const res = await app.request('/api/v1/stripe/packages') - expect(res.status).toBe(200) - - const data = await res.json() - expect(data).toEqual([{ amount: 500, fluxAmount: 5000, label: '5000 Flux', price: '$5' }]) - }) - - it('returns empty array when no packages configured', async () => { - const configKV = createMockConfigKV({ FLUX_PACKAGES: [] }) - const app = createTestApp( - createMockFluxService(), - createMockStripeService(), - createMockBillingService(), - configKV, + createMockConfigKV({ STRIPE_FLUX_PRODUCT_ID: undefined }), + { STRIPE_SECRET_KEY: '' }, ) const res = await app.request('/api/v1/stripe/packages') @@ -197,12 +220,12 @@ describe('stripeRoutes', () => { const res = await app.request('/api/v1/stripe/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ amount: 500 }), + body: JSON.stringify({ stripePriceId: 'price_test_500' }), }) expect(res.status).toBe(401) }) - it('returns 400 for invalid amount (zero)', async () => { + it('returns 400 for empty stripePriceId', async () => { const app = createTestApp( createMockFluxService(), createMockStripeService(), @@ -214,14 +237,14 @@ describe('stripeRoutes', () => { new Request('http://localhost/api/v1/stripe/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ amount: 0 }), + body: JSON.stringify({ stripePriceId: '' }), }), { user: testUser } as any, ) expect(res.status).toBe(400) }) - it('returns 400 for invalid amount (negative)', async () => { + it('returns 400 for missing stripePriceId', async () => { const app = createTestApp( createMockFluxService(), createMockStripeService(), @@ -233,65 +256,7 @@ describe('stripeRoutes', () => { new Request('http://localhost/api/v1/stripe/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ amount: -100 }), - }), - { user: testUser } as any, - ) - expect(res.status).toBe(400) - }) - - it('returns 400 for amount exceeding max ($10,000)', async () => { - const app = createTestApp( - createMockFluxService(), - createMockStripeService(), - createMockBillingService(), - createMockConfigKV(), - ) - - const res = await app.fetch( - new Request('http://localhost/api/v1/stripe/checkout', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ amount: 1_000_001 }), - }), - { user: testUser } as any, - ) - expect(res.status).toBe(400) - }) - - it('respects configured max checkout amount', async () => { - const app = createTestApp( - createMockFluxService(), - createMockStripeService(), - createMockBillingService(), - createMockConfigKV({ MAX_CHECKOUT_AMOUNT_CENTS: 500 }), - ) - - const res = await app.fetch( - new Request('http://localhost/api/v1/stripe/checkout', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ amount: 501 }), - }), - { user: testUser } as any, - ) - - expect(res.status).toBe(400) - }) - - it('returns 400 for non-integer amount', async () => { - const app = createTestApp( - createMockFluxService(), - createMockStripeService(), - createMockBillingService(), - createMockConfigKV(), - ) - - const res = await app.fetch( - new Request('http://localhost/api/v1/stripe/checkout', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ amount: 9.99 }), + body: JSON.stringify({}), }), { user: testUser } as any, ) @@ -299,30 +264,22 @@ describe('stripeRoutes', () => { }) it('returns 503 when Stripe is not configured', async () => { - const routes = createStripeRoutes( + const app = createTestApp( createMockFluxService(), createMockStripeService(), createMockBillingService(), - createMockConfigKV(), - { ...testEnv, STRIPE_SECRET_KEY: '' } as any, + createMockConfigKV({ STRIPE_FLUX_PRODUCT_ID: undefined }), + { STRIPE_SECRET_KEY: '' }, ) - const app = new Hono() - app.onError((err, c) => { - if (err instanceof ApiError) - return c.json({ error: err.errorCode }, err.statusCode) - return c.json({ error: 'Internal Server Error' }, 500) - }) - app.use('*', async (c, next) => { - c.set('user', testUser as any) - await next() - }) - app.route('/api/v1/stripe', routes) - const res = await app.request('/api/v1/stripe/checkout', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ amount: 500 }), - }) + const res = await app.fetch( + new Request('http://localhost/api/v1/stripe/checkout', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ stripePriceId: 'price_test_500' }), + }), + { user: testUser } as any, + ) expect(res.status).toBe(503) }) }) @@ -478,20 +435,13 @@ describe('stripeRoutes', () => { }) it('returns 503 when Stripe is not configured', async () => { - const routes = createStripeRoutes( + const app = createTestApp( createMockFluxService(), createMockStripeService(), createMockBillingService(), createMockConfigKV(), - { ...testEnv, STRIPE_SECRET_KEY: '', STRIPE_WEBHOOK_SECRET: '' } as any, + { STRIPE_SECRET_KEY: '', STRIPE_WEBHOOK_SECRET: '' }, ) - const app = new Hono() - app.onError((err, c) => { - if (err instanceof ApiError) - return c.json({ error: err.errorCode }, err.statusCode) - return c.json({ error: 'Internal Server Error' }, 500) - }) - app.route('/api/v1/stripe', routes) const res = await app.request('/api/v1/stripe/webhook', { method: 'POST', diff --git a/apps/server/src/routes/stripe/schema.ts b/apps/server/src/routes/stripe/schema.ts index caeb67e8d..a285c3772 100644 --- a/apps/server/src/routes/stripe/schema.ts +++ b/apps/server/src/routes/stripe/schema.ts @@ -1,5 +1,6 @@ -import { integer, minValue, number, object, pipe } from 'valibot' +import { minLength, object, optional, pipe, string } from 'valibot' export const CheckoutBodySchema = object({ - amount: pipe(number(), integer(), minValue(1)), + stripePriceId: pipe(string(), minLength(1)), + currency: optional(string()), }) diff --git a/apps/server/src/services/config-kv.ts b/apps/server/src/services/config-kv.ts index a85b99026..60807b706 100644 --- a/apps/server/src/services/config-kv.ts +++ b/apps/server/src/services/config-kv.ts @@ -1,24 +1,11 @@ import type Redis from 'ioredis' import type { InferOutput } from 'valibot' -import { array, number, object, optional, parse, string } from 'valibot' +import { any, array, number, optional, parse, record, string } from 'valibot' import { createServiceUnavailableError } from '../utils/error' import { configRedisKey } from '../utils/redis-keys' -export interface FluxPackage { - /** Amount in cents sent to Stripe */ - amount: number - /** How much Flux the buyer receives for this package */ - fluxAmount: number - /** Display label, e.g. "500 Flux" */ - label: string - /** Display price, e.g. "$5" */ - price: string -} - -const FluxPackageSchema = object({ amount: number(), fluxAmount: number(), label: string(), price: string() }) - /** * Config entry schemas are the single source of truth for: * - runtime validation @@ -30,13 +17,16 @@ const ConfigEntrySchemas = { FLUX_PER_REQUEST_TTS: number(), FLUX_PER_REQUEST_ASR: number(), INITIAL_USER_FLUX: optional(number(), 0), - FLUX_PACKAGES: optional(array(FluxPackageSchema), []), FLUX_PER_1K_TOKENS: optional(number(), 1), - MAX_CHECKOUT_AMOUNT_CENTS: optional(number(), 1_000_000), GATEWAY_BASE_URL: string(), DEFAULT_CHAT_MODEL: string(), AUTH_RATE_LIMIT_MAX: optional(number(), 20), AUTH_RATE_LIMIT_WINDOW_SEC: optional(number(), 60), + // No default — absent means top-up is not available yet + STRIPE_FLUX_PRODUCT_ID: optional(string()), + // No default — absent lets Stripe auto-select payment methods via Dashboard config + STRIPE_PAYMENT_METHODS: optional(array(string())), + STRIPE_PAYMENT_METHOD_OPTIONS: optional(record(string(), any()), {}), } as const type ConfigDefinitions = { diff --git a/apps/server/src/services/flux-transaction.ts b/apps/server/src/services/flux-transaction.ts index a46dc4dde..73d3d5601 100644 --- a/apps/server/src/services/flux-transaction.ts +++ b/apps/server/src/services/flux-transaction.ts @@ -1,7 +1,7 @@ import type { Database } from '../libs/db' import { useLogger } from '@guiiai/logg' -import { desc, eq } from 'drizzle-orm' +import { desc, eq, sql } from 'drizzle-orm' import * as schema from '../schemas/flux-transaction' @@ -46,6 +46,30 @@ export function createFluxTransactionService(db: Database) { return { records, hasMore } }, + + async getStats(userId: string) { + const rows = await db.select({ + type: schema.fluxTransaction.type, + total: sql`sum(${schema.fluxTransaction.amount})`.mapWith(Number), + }) + .from(schema.fluxTransaction) + .where(eq(schema.fluxTransaction.userId, userId)) + .groupBy(schema.fluxTransaction.type) + + let totalReceived = 0 + let totalConsumed = 0 + + for (const row of rows) { + if (row.type === 'credit' || row.type === 'initial') { + totalReceived += row.total + } + else if (row.type === 'debit') { + totalConsumed += row.total + } + } + + return { totalReceived, totalConsumed } + }, } } diff --git a/apps/server/src/services/tests/config-kv.test.ts b/apps/server/src/services/tests/config-kv.test.ts index dd6d04075..1ef8f00c8 100644 --- a/apps/server/src/services/tests/config-kv.test.ts +++ b/apps/server/src/services/tests/config-kv.test.ts @@ -84,49 +84,6 @@ describe('configKVService', () => { expect(value).toBe(500) }) - // --- FLUX_PACKAGES (JSON) --- - - it('get FLUX_PACKAGES should parse JSON array', async () => { - const packages = [ - { amount: 500, fluxAmount: 5000, label: '5000 Flux', price: '$5' }, - { amount: 1000, fluxAmount: 12000, label: '12000 Flux', price: '$10' }, - ] - redis._store.set(configRedisKey('FLUX_PACKAGES'), JSON.stringify(packages)) - - const value = await service.getOrThrow('FLUX_PACKAGES') - expect(value).toEqual(packages) - }) - - it('set FLUX_PACKAGES should serialize as JSON', async () => { - const packages = [{ amount: 500, fluxAmount: 5000, label: '5000 Flux', price: '$5' }] - await service.set('FLUX_PACKAGES', packages) - - const stored = redis._store.get(configRedisKey('FLUX_PACKAGES')) - expect(stored).toBe(JSON.stringify(packages)) - }) - - it('fLUX_PACKAGES round-trip should preserve structure', async () => { - const packages = [ - { amount: 500, fluxAmount: 5000, label: '5000 Flux', price: '$5' }, - { amount: 1000, fluxAmount: 12000, label: '12000 Flux', price: '$10' }, - { amount: 5000, fluxAmount: 75000, label: '75000 Flux', price: '$50' }, - ] - await service.set('FLUX_PACKAGES', packages) - - const value = await service.getOrThrow('FLUX_PACKAGES') - expect(value).toEqual(packages) - }) - - it('getOptional FLUX_PACKAGES should return schema default when not set', async () => { - const value = await service.getOptional('FLUX_PACKAGES') - expect(value).toEqual([]) - }) - - it('get MAX_CHECKOUT_AMOUNT_CENTS should return schema default when not set', async () => { - const value = await service.get('MAX_CHECKOUT_AMOUNT_CENTS') - expect(value).toBe(1_000_000) - }) - it('set should store string values as JSON strings', async () => { await service.set('GATEWAY_BASE_URL', 'https://gateway.example.com') diff --git a/packages/i18n/src/locales/en/settings.yaml b/packages/i18n/src/locales/en/settings.yaml index add24fe7f..0e533339d 100644 --- a/packages/i18n/src/locales/en/settings.yaml +++ b/packages/i18n/src/locales/en/settings.yaml @@ -568,6 +568,7 @@ pages: flux: title: Flux buy: Charge + currency: Currency description: Current Flux packagesError: Failed to load packages. Please try again later. checkout: @@ -587,6 +588,7 @@ pages: empty: No transaction records yet. loadMore: Load More delayHint: Usage records may be delayed by up to 1 minute + ttsRequests: requests packages: title: Flux Packages buy: Charge diff --git a/packages/i18n/src/locales/zh-Hans/settings.yaml b/packages/i18n/src/locales/zh-Hans/settings.yaml index 296405044..e88369e38 100644 --- a/packages/i18n/src/locales/zh-Hans/settings.yaml +++ b/packages/i18n/src/locales/zh-Hans/settings.yaml @@ -538,6 +538,7 @@ pages: flux: title: Flux buy: 充值 + currency: 货币 description: 充值 Flux packagesError: 暂时无法获取可用的 Flux 电量包。可以之后再试试吗? checkout: @@ -557,6 +558,7 @@ pages: empty: 暂无消耗记录 loadMore: 更多信息 delayHint: Flux 电量历史记录是缓慢更新的,可能有延迟 + ttsRequests: 次请求 packages: title: Flux 电池包 buy: 充值 diff --git a/packages/stage-pages/src/pages/settings/flux.vue b/packages/stage-pages/src/pages/settings/flux.vue index e6e858202..78bbde768 100644 --- a/packages/stage-pages/src/pages/settings/flux.vue +++ b/packages/stage-pages/src/pages/settings/flux.vue @@ -1,9 +1,9 @@ @@ -137,34 +256,67 @@ async function handleBuy(amount: number) { {{ message.text }} -
-
-

- {{ credits }} -

-

- {{ t('settings.pages.flux.description') }} -

+ +
+ +
+ + +
+
+

+ {{ formatNumber(credits) }} +

+

+ {{ t('settings.pages.flux.description') }} +

+
-
-
-
- {{ pkg.label }} -
-
- {{ pkg.price }} -
- +
@@ -206,84 +358,196 @@ async function handleBuy(amount: number) { - - - {{ formatDate(record.createdAt) }} - - - - {{ record.type === 'debit' - ? t('settings.pages.flux.audit.typeConsumption') - : record.type === 'credit' - ? t('settings.pages.flux.audit.typeAddition') - : t('settings.pages.flux.audit.typeInitial') }} - - - - {{ record.description }} - - ({{ record.metadata.promptTokens }}+{{ record.metadata.completionTokens }} tokens) - - - - - {{ displayAmount(record) }} - - - +
-
-
- - {{ record.type === 'debit' - ? t('settings.pages.flux.audit.typeConsumption') - : record.type === 'credit' - ? t('settings.pages.flux.audit.typeAddition') - : t('settings.pages.flux.audit.typeInitial') }} - - - {{ displayAmount(record) }} - +
@@ -297,6 +561,24 @@ async function handleBuy(amount: number) {
+ + meta: layout: settings diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10a8722fa..008e469af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1375,7 +1375,7 @@ importers: version: 3.0.2(electron@40.8.5) '@electron-toolkit/tsconfig': specifier: ^2.0.0 - version: 2.0.0(@types/node@25.6.0) + version: 2.0.0(@types/node@24.12.2) '@electron-toolkit/utils': specifier: ^4.0.0 version: 4.0.0(electron@40.8.5) @@ -1414,7 +1414,7 @@ importers: version: 3.1.0 '@intlify/unplugin-vue-i18n': specifier: ^11.0.7 - version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.0(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) + version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.0(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) '@modelcontextprotocol/sdk': specifier: 'catalog:' version: 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6) @@ -1450,10 +1450,10 @@ importers: version: link:../../packages/ui-transitions '@proj-airi/unplugin-fetch': specifier: 'catalog:' - version: 0.2.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 0.2.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@proj-airi/unplugin-live2d-sdk': specifier: ^0.1.6 - version: 0.1.6(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + version: 0.1.6(@types/node@24.12.2)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) '@types/audioworklet': specifier: 'catalog:' version: 0.0.97 @@ -1477,7 +1477,7 @@ importers: version: 2.10.3 '@vitejs/plugin-vue': specifier: ^6.0.5 - version: 6.0.5(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)) + version: 6.0.5(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)) '@vue-macros/volar': specifier: ^3.1.2 version: 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)) @@ -1507,7 +1507,7 @@ importers: version: 6.8.3 electron-vite: specifier: ^5.0.0 - version: 5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 5.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) get-port-please: specifier: 'catalog:' version: 3.2.0 @@ -1528,34 +1528,34 @@ importers: version: 2.2.6 unocss-preset-scrollbar: specifier: ^4.0.0 - version: 4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) + version: 4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) unplugin-info: specifier: ^1.3.2 - version: 1.3.2(esbuild@0.27.7)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 1.3.2(esbuild@0.27.7)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) unplugin-vue-router: specifier: ^0.19.2 version: 0.19.2(@vue/compiler-sfc@3.5.32)(vue-router@5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) unplugin-yaml: specifier: ^4.1.0 - version: 4.1.0(esbuild@0.27.7)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 4.1.0(esbuild@0.27.7)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vite: specifier: 'catalog:' - version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + version: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vite-bundle-visualizer: specifier: ^1.2.1 version: 1.2.1(rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(rollup@4.60.1) vite-plugin-mkcert: specifier: 'catalog:' - version: 2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + version: 2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vite-plugin-vue-devtools: specifier: ^8.1.1 - version: 8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)) + version: 8.1.1(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)) vite-plugin-vue-layouts: specifier: ^0.11.0 - version: 0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) + version: 0.11.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) vue-macros: specifier: ^3.1.2 - version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.7)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)) + version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.7)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)) vue-tsc: specifier: ^3.2.6 version: 3.2.6(typescript@5.9.3) @@ -19214,9 +19214,9 @@ snapshots: dependencies: electron: 40.8.5 - '@electron-toolkit/tsconfig@2.0.0(@types/node@25.6.0)': + '@electron-toolkit/tsconfig@2.0.0(@types/node@24.12.2)': dependencies: - '@types/node': 25.6.0 + '@types/node': 24.12.2 '@electron-toolkit/utils@4.0.0(electron@40.8.5)': dependencies: @@ -20102,6 +20102,31 @@ snapshots: - supports-color - typescript + '@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.0(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) + '@intlify/bundle-utils': 11.0.7(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3))) + '@intlify/shared': 11.3.2 + '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.3.2)(@vue/compiler-dom@3.5.32)(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) + '@rollup/pluginutils': 5.3.0(rollup@4.60.1) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3) + debug: 4.4.3(supports-color@10.2.2) + fast-glob: 3.3.3 + pathe: 2.0.3 + picocolors: 1.1.1 + unplugin: 2.3.11 + vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vue: 3.5.32(typescript@5.9.3) + optionalDependencies: + vue-i18n: 11.3.2(vue@3.5.32(typescript@5.9.3)) + transitivePeerDependencies: + - '@vue/compiler-dom' + - eslint + - rollup + - supports-color + - typescript + '@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.0(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.0(jiti@2.6.1)) @@ -22072,11 +22097,34 @@ snapshots: transitivePeerDependencies: - magicast + '@proj-airi/unplugin-fetch@0.2.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + ofetch: 1.5.1 + vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + '@proj-airi/unplugin-fetch@0.2.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: ofetch: 1.5.1 vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + '@proj-airi/unplugin-live2d-sdk@0.1.6(@types/node@24.12.2)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)': + dependencies: + ofetch: 1.5.1 + vite: 7.3.2(@types/node@24.12.2)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + yauzl: 3.3.0 + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + '@proj-airi/unplugin-live2d-sdk@0.1.6(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)': dependencies: ofetch: 1.5.1 @@ -23024,6 +23072,7 @@ snapshots: '@types/node@25.6.0': dependencies: undici-types: 7.19.2 + optional: true '@types/nprogress@0.2.3': {} @@ -23049,7 +23098,7 @@ snapshots: '@types/pg@8.20.0': dependencies: - '@types/node': 25.6.0 + '@types/node': 24.12.2 pg-protocol: 1.13.0 pg-types: 2.2.0 @@ -23110,7 +23159,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.6.0 + '@types/node': 24.12.2 '@types/xast@2.0.4': dependencies: @@ -23118,7 +23167,7 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 25.6.0 + '@types/node': 24.12.2 '@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: @@ -23460,19 +23509,6 @@ snapshots: unplugin-utils: 0.3.1 vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - '@unocss/vite@66.6.8(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': - dependencies: - '@jridgewell/remapping': 2.3.5 - '@unocss/config': 66.6.8 - '@unocss/core': 66.6.8 - '@unocss/inspector': 66.6.8 - chokidar: 5.0.0 - magic-string: 0.30.21 - pathe: 2.0.3 - tinyglobby: 0.2.16 - unplugin-utils: 0.3.1 - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - '@unrteljs/eval@0.2.1': dependencies: builtin-modules: 5.0.0 @@ -23596,6 +23632,12 @@ snapshots: vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vue: 3.5.32(typescript@5.9.3) + '@vitejs/plugin-vue@6.0.5(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.2 + vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vue: 3.5.32(typescript@5.9.3) + '@vitejs/plugin-vue@6.0.5(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.2 @@ -23898,6 +23940,15 @@ snapshots: transitivePeerDependencies: - vue + '@vue-macros/devtools@3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + sirv: 3.0.2 + vue: 3.5.32(typescript@5.9.3) + optionalDependencies: + vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + transitivePeerDependencies: + - typescript + '@vue-macros/devtools@3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: sirv: 3.0.2 @@ -26002,7 +26053,7 @@ snapshots: transitivePeerDependencies: - supports-color - electron-vite@5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + electron-vite@5.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) @@ -26010,7 +26061,7 @@ snapshots: esbuild: 0.25.12 magic-string: 0.30.21 picocolors: 1.1.1 - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -26780,8 +26831,7 @@ snapshots: extsprintf@1.3.0: {} - extsprintf@1.4.1: - optional: true + extsprintf@1.4.1: {} fast-deep-equal@3.1.3: {} @@ -31841,7 +31891,8 @@ snapshots: undici-types@7.16.0: {} - undici-types@7.19.2: {} + undici-types@7.19.2: + optional: true undici@6.24.1: {} @@ -31951,11 +32002,6 @@ snapshots: '@unocss/preset-mini': 66.6.8 unocss: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - unocss-preset-scrollbar@4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))): - dependencies: - '@unocss/preset-mini': 66.6.8 - unocss: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@unocss/cli': 66.6.8 @@ -32004,30 +32050,6 @@ snapshots: - '@emnapi/runtime' - vite - unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): - dependencies: - '@unocss/cli': 66.6.8 - '@unocss/core': 66.6.8 - '@unocss/preset-attributify': 66.6.8 - '@unocss/preset-icons': 66.6.8 - '@unocss/preset-mini': 66.6.8 - '@unocss/preset-tagify': 66.6.8 - '@unocss/preset-typography': 66.6.8 - '@unocss/preset-uno': 66.6.8 - '@unocss/preset-web-fonts': 66.6.8 - '@unocss/preset-wind': 66.6.8 - '@unocss/preset-wind3': 66.6.8 - '@unocss/preset-wind4': 66.6.8 - '@unocss/transformer-attributify-jsx': 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) - '@unocss/transformer-compile-class': 66.6.8 - '@unocss/transformer-directives': 66.6.8 - '@unocss/transformer-variant-group': 66.6.8 - '@unocss/vite': 66.6.8(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - - vite - unpack-string@0.0.2: {} unpipe@1.0.0: {} @@ -32040,6 +32062,14 @@ snapshots: unplugin: 2.3.11 vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + unplugin-combine@2.3.0(esbuild@0.27.7)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + optionalDependencies: + esbuild: 0.27.7 + rolldown: 1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + rollup: 4.60.1 + unplugin: 2.3.11 + vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + unplugin-combine@2.3.0(esbuild@0.27.7)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): optionalDependencies: esbuild: 0.27.7 @@ -32074,6 +32104,19 @@ snapshots: transitivePeerDependencies: - supports-color + unplugin-info@1.3.2(esbuild@0.27.7)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + ci-info: 4.4.0 + git-url-parse: 16.1.0 + simple-git: 3.35.2 + unplugin: 2.3.11 + optionalDependencies: + esbuild: 0.27.7 + rollup: 4.60.1 + vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + transitivePeerDependencies: + - supports-color + unplugin-info@1.3.2(esbuild@0.27.7)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: ci-info: 4.4.0 @@ -32172,6 +32215,17 @@ snapshots: rollup: 2.80.0 vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + unplugin-yaml@4.1.0(esbuild@0.27.7)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.60.1) + unplugin: 3.0.0 + yaml: 2.8.3 + optionalDependencies: + esbuild: 0.27.7 + rolldown: 1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + rollup: 4.60.1 + vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + unplugin-yaml@4.1.0(esbuild@0.27.7)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.60.1) @@ -32337,7 +32391,7 @@ snapshots: dependencies: assert-plus: 1.0.0 core-util-is: 1.0.2 - extsprintf: 1.3.0 + extsprintf: 1.4.1 verror@1.10.1: dependencies: @@ -32383,12 +32437,22 @@ snapshots: - rollup - supports-color + vite-dev-rpc@1.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + birpc: 2.9.0 + vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite-hot-client: 2.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + vite-dev-rpc@1.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: birpc: 2.9.0 vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vite-hot-client: 2.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + vite-hot-client@2.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite-hot-client@2.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) @@ -32435,6 +32499,21 @@ snapshots: - tsx - yaml + vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + ansis: 4.2.0 + debug: 4.4.3(supports-color@10.2.2) + error-stack-parser-es: 1.0.5 + ohash: 2.0.11 + open: 10.2.0 + perfect-debounce: 2.1.0 + sirv: 3.0.2 + unplugin-utils: 0.3.1 + vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite-dev-rpc: 1.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + transitivePeerDependencies: + - supports-color + vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: ansis: 4.2.0 @@ -32466,6 +32545,13 @@ snapshots: - typescript - ws + vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + debug: 4.4.3(supports-color@10.2.2) + supports-color: 10.2.2 + undici: 8.0.2 + vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: debug: 4.4.3(supports-color@10.2.2) @@ -32484,6 +32570,20 @@ snapshots: transitivePeerDependencies: - supports-color + vite-plugin-vue-devtools@8.1.1(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)): + dependencies: + '@vue/devtools-core': 8.1.1(vue@3.5.32(typescript@5.9.3)) + '@vue/devtools-kit': 8.1.1 + '@vue/devtools-shared': 8.1.1 + sirv: 3.0.2 + vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite-plugin-inspect: 11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + vite-plugin-vue-inspector: 5.4.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + transitivePeerDependencies: + - '@nuxt/kit' + - supports-color + - vue + vite-plugin-vue-devtools@8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)): dependencies: '@vue/devtools-core': 8.1.1(vue@3.5.32(typescript@5.9.3)) @@ -32498,6 +32598,21 @@ snapshots: - supports-color - vue + vite-plugin-vue-inspector@5.4.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.0) + '@vue/compiler-dom': 3.5.32 + kolorist: 1.8.0 + magic-string: 0.30.21 + vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + transitivePeerDependencies: + - supports-color + vite-plugin-vue-inspector@5.4.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@babel/core': 7.29.0 @@ -32513,6 +32628,16 @@ snapshots: transitivePeerDependencies: - supports-color + vite-plugin-vue-layouts@0.11.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)): + dependencies: + debug: 4.4.3(supports-color@10.2.2) + fast-glob: 3.3.3 + vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vue: 3.5.32(typescript@5.9.3) + vue-router: 5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) + transitivePeerDependencies: + - supports-color + vite-plugin-vue-layouts@0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)): dependencies: debug: 4.4.3(supports-color@10.2.2) @@ -32541,6 +32666,24 @@ snapshots: tsx: 4.21.0 yaml: 2.8.3 + vite@7.3.2(@types/node@24.12.2)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.9 + rollup: 4.60.1 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 24.12.2 + fsevents: 2.3.3 + jiti: 2.6.1 + less: 4.6.4 + lightningcss: 1.32.0 + terser: 5.46.1 + tsx: 4.21.0 + yaml: 2.8.3 + vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.27.7 @@ -32771,6 +32914,54 @@ snapshots: - vue-tsc - webpack + vue-macros@3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.7)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)): + dependencies: + '@vue-macros/better-define': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/boolean-prop': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/chain-call': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/common': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/config': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/define-emit': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/define-models': 3.1.2(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/define-prop': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/define-props': 3.1.2(@vue-macros/reactivity-transform@3.1.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/define-props-refs': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/define-render': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/define-slots': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/define-stylex': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/devtools': 3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vue-macros/export-expose': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/export-props': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/export-render': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/hoist-static': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/jsx-directive': 3.1.2(typescript@5.9.3) + '@vue-macros/named-template': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/reactivity-transform': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/script-lang': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/setup-block': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/setup-component': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/setup-sfc': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/short-bind': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/short-emits': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/short-vmodel': 3.1.2(vue@3.5.32(typescript@5.9.3)) + '@vue-macros/volar': 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)) + unplugin: 2.3.11 + unplugin-combine: 2.3.0(esbuild@0.27.7)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + unplugin-vue-define-options: 3.1.2(vue@3.5.32(typescript@5.9.3)) + vue: 3.5.32(typescript@5.9.3) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + - '@rspack/core' + - '@vueuse/core' + - esbuild + - rolldown + - rollup + - typescript + - vite + - vue-tsc + - webpack + vue-macros@3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.7)(rolldown@1.0.0-rc.12(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)): dependencies: '@vue-macros/better-define': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3))