feat(server): use stripe product as flux pricing (#1640)
This commit is contained in:
@@ -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
|
||||
@@ -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`)
|
||||
|
||||
@@ -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"}}` |
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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<string, string>
|
||||
currencyOptions: Record<string, CachedCurrencyOption>
|
||||
}
|
||||
|
||||
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<CachedPrice[]> {
|
||||
// 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<Stripe.Price>
|
||||
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<HonoEnv>()
|
||||
.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<string, string> = {
|
||||
[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()}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, any> = {}): ConfigKVService {
|
||||
const defaults: Record<string, any> = {
|
||||
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<string, any> = {}): ConfigKVServic
|
||||
} as any
|
||||
}
|
||||
|
||||
function createMockRedis(): any {
|
||||
const store = new Map<string, string>()
|
||||
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<string, any> = {},
|
||||
) {
|
||||
const routes = createStripeRoutes(fluxService, stripeService, billingService, configKV, testEnv)
|
||||
const routes = createStripeRoutes(fluxService, stripeService, billingService, configKV, { ...testEnv, ...envOverrides }, createMockRedis())
|
||||
const app = new Hono<HonoEnv>()
|
||||
|
||||
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<HonoEnv>()
|
||||
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<HonoEnv>()
|
||||
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',
|
||||
|
||||
@@ -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()),
|
||||
})
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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<number>`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 }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: 充值
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { client } from '@proj-airi/stage-ui/composables/api'
|
||||
import { useAuthStore } from '@proj-airi/stage-ui/stores/auth'
|
||||
import { Button } from '@proj-airi/ui'
|
||||
import { Button, SelectTab } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
@@ -13,9 +13,27 @@ const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const { credits } = storeToRefs(authStore)
|
||||
|
||||
const loadingAmount = ref<number | null>(null)
|
||||
interface FluxPackage {
|
||||
stripePriceId: string
|
||||
label: string
|
||||
defaultCurrency: string
|
||||
currencies: Record<string, string>
|
||||
}
|
||||
|
||||
const loadingPriceId = ref<string | null>(null)
|
||||
const message = ref<{ type: 'success' | 'error', text: string } | null>(null)
|
||||
const packages = ref<{ amount: number, label: string, price: string }[]>([])
|
||||
const packages = ref<FluxPackage[]>([])
|
||||
const selectedCurrency = ref<string>('usd')
|
||||
|
||||
const currencyOptions = computed(() => {
|
||||
if (packages.value.length === 0)
|
||||
return []
|
||||
// Currencies supported by all packages
|
||||
const first = Object.keys(packages.value[0].currencies)
|
||||
return first
|
||||
.filter(c => packages.value.every(p => c in p.currencies))
|
||||
.map(c => ({ label: c.toUpperCase(), value: c }))
|
||||
})
|
||||
|
||||
// NOTICE: Manual interface instead of hono InferResponseType because hono client
|
||||
// type instantiation hits TS recursion limits ("excessively deep and possibly infinite").
|
||||
@@ -29,10 +47,15 @@ interface AuditRecord {
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
function formatNumber(num: number): string {
|
||||
return new Intl.NumberFormat().format(num)
|
||||
}
|
||||
|
||||
/** Display amount with sign: debit is negative, credit/initial are positive */
|
||||
function displayAmount(record: AuditRecord): string {
|
||||
const signed = record.type === 'debit' ? -record.amount : record.amount
|
||||
return signed >= 0 ? `+${signed}` : String(signed)
|
||||
const formatted = formatNumber(Math.abs(signed))
|
||||
return signed >= 0 ? `+${formatted}` : `-${formatted}`
|
||||
}
|
||||
|
||||
function isPositive(record: AuditRecord): boolean {
|
||||
@@ -45,6 +68,30 @@ const auditHasMore = ref(false)
|
||||
const auditOffset = ref(0)
|
||||
const AUDIT_PAGE_SIZE = 20
|
||||
|
||||
const totalReceived = ref(0)
|
||||
const totalConsumed = ref(0)
|
||||
|
||||
const fluxPercentage = computed(() => {
|
||||
if (totalReceived.value === 0)
|
||||
return credits.value > 0 ? 100 : 0
|
||||
const remaining = Math.max(0, totalReceived.value - totalConsumed.value)
|
||||
return Math.min(100, Math.round((remaining / totalReceived.value) * 100))
|
||||
})
|
||||
|
||||
async function fetchStats() {
|
||||
try {
|
||||
const res = await client.api.v1.flux.stats.$get()
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
totalReceived.value = data.totalReceived
|
||||
totalConsumed.value = data.totalConsumed
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// silently fail
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAuditHistory(loadMore = false) {
|
||||
auditLoading.value = true
|
||||
try {
|
||||
@@ -76,11 +123,83 @@ function formatDate(iso: string): string {
|
||||
return new Date(iso).toLocaleString()
|
||||
}
|
||||
|
||||
// Group consecutive TTS debit records into collapsible rows
|
||||
type GroupedRow = {
|
||||
type: 'single'
|
||||
record: AuditRecord
|
||||
} | {
|
||||
type: 'group'
|
||||
key: string
|
||||
description: string
|
||||
model: string
|
||||
count: number
|
||||
totalAmount: number
|
||||
firstTime: string
|
||||
lastTime: string
|
||||
records: AuditRecord[]
|
||||
}
|
||||
|
||||
const expandedGroups = ref<Set<string>>(new Set())
|
||||
|
||||
function toggleGroup(key: string) {
|
||||
if (expandedGroups.value.has(key))
|
||||
expandedGroups.value.delete(key)
|
||||
else
|
||||
expandedGroups.value.add(key)
|
||||
}
|
||||
|
||||
const groupedRows = computed<GroupedRow[]>(() => {
|
||||
const rows: GroupedRow[] = []
|
||||
let i = 0
|
||||
const records = auditRecords.value
|
||||
|
||||
while (i < records.length) {
|
||||
const record = records[i]
|
||||
if (record.type === 'debit' && record.description?.startsWith('tts:')) {
|
||||
// Collect consecutive TTS records with the same description
|
||||
const group: AuditRecord[] = [record]
|
||||
while (i + 1 < records.length
|
||||
&& records[i + 1].type === 'debit'
|
||||
&& records[i + 1].description === record.description) {
|
||||
i++
|
||||
group.push(records[i])
|
||||
}
|
||||
|
||||
if (group.length > 1) {
|
||||
rows.push({
|
||||
type: 'group',
|
||||
key: `tts-group-${record.id}`,
|
||||
description: record.description,
|
||||
model: (record.metadata?.model as string) || '',
|
||||
count: group.length,
|
||||
totalAmount: group.reduce((sum, r) => sum + r.amount, 0),
|
||||
firstTime: group.at(-1).createdAt,
|
||||
lastTime: group[0].createdAt,
|
||||
records: group,
|
||||
})
|
||||
}
|
||||
else {
|
||||
rows.push({ type: 'single', record })
|
||||
}
|
||||
}
|
||||
else {
|
||||
rows.push({ type: 'single', record })
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
return rows
|
||||
})
|
||||
|
||||
async function fetchPackages() {
|
||||
try {
|
||||
const res = await client.api.v1.stripe.packages.$get()
|
||||
if (res.ok)
|
||||
packages.value = await res.json() as { amount: number, label: string, price: string }[]
|
||||
if (res.ok) {
|
||||
const data = await res.json() as FluxPackage[]
|
||||
packages.value = data
|
||||
if (data.length > 0)
|
||||
selectedCurrency.value = data[0].defaultCurrency
|
||||
}
|
||||
}
|
||||
catch {
|
||||
message.value = { type: 'error', text: t('settings.pages.flux.packagesError') }
|
||||
@@ -88,7 +207,7 @@ async function fetchPackages() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
Promise.allSettled([fetchPackages(), authStore.updateCredits(), fetchAuditHistory()])
|
||||
Promise.allSettled([fetchPackages(), authStore.updateCredits(), fetchStats(), fetchAuditHistory()])
|
||||
|
||||
if (route.query.success === 'true') {
|
||||
message.value = { type: 'success', text: t('settings.pages.flux.checkout.success') }
|
||||
@@ -100,11 +219,11 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
async function handleBuy(amount: number) {
|
||||
loadingAmount.value = amount
|
||||
async function handleBuy(stripePriceId: string) {
|
||||
loadingPriceId.value = stripePriceId
|
||||
message.value = null
|
||||
try {
|
||||
const res = await client.api.v1.stripe.checkout.$post({ json: { amount } })
|
||||
const res = await client.api.v1.stripe.checkout.$post({ json: { stripePriceId, currency: selectedCurrency.value } })
|
||||
if (!res.ok) {
|
||||
const data = await res.json() as { error?: string, message?: string }
|
||||
message.value = { type: 'error', text: data.message || t('settings.pages.flux.checkout.error') }
|
||||
@@ -119,7 +238,7 @@ async function handleBuy(amount: number) {
|
||||
message.value = { type: 'error', text: t('settings.pages.flux.checkout.error') }
|
||||
}
|
||||
finally {
|
||||
loadingAmount.value = null
|
||||
loadingPriceId.value = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -137,34 +256,67 @@ async function handleBuy(amount: number) {
|
||||
{{ message.text }}
|
||||
</div>
|
||||
|
||||
<div bg="primary-500/10 dark:primary-400/10" rounded-xl p-6 text-center>
|
||||
<div i-solar:battery-charge-bold-duotone mx-auto size-16 text-primary-500 />
|
||||
<h2 mt-4 text-3xl font-bold>
|
||||
{{ credits }}
|
||||
</h2>
|
||||
<p text="sm neutral-500">
|
||||
{{ t('settings.pages.flux.description') }}
|
||||
</p>
|
||||
<!-- Battery Card -->
|
||||
<div relative overflow-hidden rounded-2xl bg="neutral-100 dark:neutral-800" p-8 text-center>
|
||||
<!-- Background Progress -->
|
||||
<div
|
||||
class="flux-progress-bar absolute inset-y-0 left-0 bg-primary-500/20 dark:bg-primary-400/20"
|
||||
/>
|
||||
|
||||
<!-- Content -->
|
||||
<div relative z-1>
|
||||
<div i-solar:battery-charge-bold-duotone mx-auto size-12 text-primary-500 />
|
||||
<h2 mt-2 text-4xl font-bold tracking-tight>
|
||||
{{ formatNumber(credits) }}
|
||||
</h2>
|
||||
<p text="sm neutral-500">
|
||||
{{ t('settings.pages.flux.description') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div grid="~ cols-1 sm:cols-3 gap-4">
|
||||
<div
|
||||
v-for="pkg in packages" :key="pkg.amount"
|
||||
border="1 neutral-200 dark:neutral-800" flex="~ col gap-2" items-center rounded-xl p-4
|
||||
>
|
||||
<div font-bold>
|
||||
{{ pkg.label }}
|
||||
</div>
|
||||
<div text="2xl" font-bold>
|
||||
{{ pkg.price }}
|
||||
</div>
|
||||
<Button
|
||||
:label="t('settings.pages.flux.buy')"
|
||||
:loading="loadingAmount === pkg.amount"
|
||||
:disabled="loadingAmount !== null && loadingAmount !== pkg.amount"
|
||||
@click="handleBuy(pkg.amount)"
|
||||
<div flex="~ col gap-4">
|
||||
<!-- Currency selector -->
|
||||
<div v-if="currencyOptions.length > 1" flex="~ justify-end">
|
||||
<SelectTab
|
||||
v-model="selectedCurrency"
|
||||
:options="currencyOptions"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div grid="~ cols-1 sm:cols-3 gap-4">
|
||||
<button
|
||||
v-for="pkg in packages" :key="pkg.stripePriceId"
|
||||
:disabled="loadingPriceId !== null"
|
||||
:class="[
|
||||
'group relative flex flex-col items-center gap-2 overflow-hidden',
|
||||
'rounded-2xl border border-neutral-200 bg-white p-6 transition-all duration-300 ease-out',
|
||||
'dark:border-neutral-800 dark:bg-neutral-900',
|
||||
'hover:-translate-y-1 hover:border-primary-400 hover:shadow-md dark:hover:border-primary-500',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
||||
loadingPriceId !== null && loadingPriceId !== pkg.stripePriceId ? 'opacity-50 grayscale-50 cursor-not-allowed' : 'cursor-pointer',
|
||||
]"
|
||||
@click="handleBuy(pkg.stripePriceId)"
|
||||
>
|
||||
<!-- Loading Overlay -->
|
||||
<div
|
||||
v-if="loadingPriceId === pkg.stripePriceId"
|
||||
class="absolute inset-0 z-10 flex items-center justify-center bg-white/60 backdrop-blur-sm dark:bg-neutral-900/60"
|
||||
>
|
||||
<div class="i-svg-spinners:90-ring-with-bg size-8 text-primary-500" />
|
||||
</div>
|
||||
|
||||
<div text="sm neutral-500 dark:neutral-400" font-medium transition-colors class="group-hover:text-primary-600 dark:group-hover:text-primary-400">
|
||||
{{ pkg.label }}
|
||||
</div>
|
||||
<div flex="~ items-baseline gap-1">
|
||||
<span text="2xl neutral-800 dark:neutral-100" font-bold>
|
||||
{{ pkg.currencies[selectedCurrency] ?? pkg.currencies[pkg.defaultCurrency] }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audit History -->
|
||||
@@ -206,84 +358,196 @@ async function handleBuy(amount: number) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="record in auditRecords"
|
||||
:key="record.id"
|
||||
border="b neutral-100 dark:neutral-800/50 last:none"
|
||||
>
|
||||
<td whitespace-nowrap px-4 py-3 text="neutral-500">
|
||||
{{ formatDate(record.createdAt) }}
|
||||
</td>
|
||||
<td px-4 py-3>
|
||||
<span
|
||||
inline-block rounded-full px-2 py-0.5 text-xs font-medium
|
||||
:class="record.type === 'debit'
|
||||
? 'bg-orange-500/10 text-orange-600 dark:text-orange-400'
|
||||
: 'bg-green-500/10 text-green-600 dark:text-green-400'"
|
||||
>
|
||||
{{ record.type === 'debit'
|
||||
? t('settings.pages.flux.audit.typeConsumption')
|
||||
: record.type === 'credit'
|
||||
? t('settings.pages.flux.audit.typeAddition')
|
||||
: t('settings.pages.flux.audit.typeInitial') }}
|
||||
</span>
|
||||
</td>
|
||||
<td px-4 py-3>
|
||||
<span>{{ record.description }}</span>
|
||||
<span
|
||||
v-if="record.metadata?.promptTokens != null"
|
||||
ml-1 text="xs neutral-400"
|
||||
>
|
||||
({{ record.metadata.promptTokens }}+{{ record.metadata.completionTokens }} tokens)
|
||||
</span>
|
||||
</td>
|
||||
<td px-4 py-3 text-right font-mono>
|
||||
<span :class="isPositive(record) ? 'text-green-600 dark:text-green-400' : 'text-orange-600 dark:text-orange-400'">
|
||||
{{ displayAmount(record) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-for="row in groupedRows" :key="row.type === 'single' ? row.record.id : row.key">
|
||||
<!-- Single record -->
|
||||
<tr
|
||||
v-if="row.type === 'single'"
|
||||
border="b neutral-100 dark:neutral-800/50 last:none"
|
||||
>
|
||||
<td whitespace-nowrap px-4 py-3 text="neutral-500">
|
||||
{{ formatDate(row.record.createdAt) }}
|
||||
</td>
|
||||
<td px-4 py-3>
|
||||
<span
|
||||
inline-block rounded-full px-2 py-0.5 text-xs font-medium
|
||||
:class="row.record.type === 'debit'
|
||||
? 'bg-orange-500/10 text-orange-600 dark:text-orange-400'
|
||||
: 'bg-green-500/10 text-green-600 dark:text-green-400'"
|
||||
>
|
||||
{{ row.record.type === 'debit'
|
||||
? t('settings.pages.flux.audit.typeConsumption')
|
||||
: row.record.type === 'credit'
|
||||
? t('settings.pages.flux.audit.typeAddition')
|
||||
: t('settings.pages.flux.audit.typeInitial') }}
|
||||
</span>
|
||||
</td>
|
||||
<td px-4 py-3>
|
||||
<span>{{ row.record.description }}</span>
|
||||
<span
|
||||
v-if="row.record.metadata?.promptTokens != null"
|
||||
ml-1 text="xs neutral-400"
|
||||
>
|
||||
({{ row.record.metadata.promptTokens }}+{{ row.record.metadata.completionTokens }} tokens)
|
||||
</span>
|
||||
<span
|
||||
v-else-if="row.record.description?.startsWith('tts:') && row.record.metadata?.model"
|
||||
ml-1 text="xs neutral-400"
|
||||
>
|
||||
({{ row.record.metadata.model }})
|
||||
</span>
|
||||
</td>
|
||||
<td px-4 py-3 text-right font-mono>
|
||||
<span :class="isPositive(row.record) ? 'text-green-600 dark:text-green-400' : 'text-orange-600 dark:text-orange-400'">
|
||||
{{ displayAmount(row.record) }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Grouped TTS records -->
|
||||
<tr
|
||||
v-else
|
||||
:class="['cursor-pointer', 'hover:bg-neutral-50', 'dark:hover:bg-neutral-800/30']"
|
||||
border="b neutral-100 dark:neutral-800/50"
|
||||
@click="toggleGroup(row.key)"
|
||||
>
|
||||
<td whitespace-nowrap px-4 py-3 text="neutral-500">
|
||||
{{ formatDate(row.lastTime) }}
|
||||
</td>
|
||||
<td px-4 py-3>
|
||||
<span
|
||||
:class="['inline-block', 'rounded-full', 'px-2', 'py-0.5', 'text-xs', 'font-medium',
|
||||
'bg-orange-500/10', 'text-orange-600', 'dark:text-orange-400']"
|
||||
>
|
||||
{{ t('settings.pages.flux.audit.typeConsumption') }}
|
||||
</span>
|
||||
</td>
|
||||
<td px-4 py-3>
|
||||
<span flex="~ items-center gap-1">
|
||||
<span
|
||||
:class="expandedGroups.has(row.key) ? 'i-solar:alt-arrow-down-line-duotone' : 'i-solar:alt-arrow-right-line-duotone'"
|
||||
inline-block size-4 text="neutral-400"
|
||||
/>
|
||||
{{ row.description }}
|
||||
<span ml-1 text="xs neutral-400">
|
||||
({{ row.count }} {{ t('settings.pages.flux.audit.ttsRequests') }})
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
<td px-4 py-3 text-right font-mono>
|
||||
<span text="orange-600 dark:orange-400">
|
||||
-{{ row.totalAmount }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Expanded group children -->
|
||||
<tr
|
||||
v-for="child in (row.type === 'group' && expandedGroups.has(row.key) ? row.records : [])"
|
||||
:key="child.id"
|
||||
border="b neutral-100 dark:neutral-800/50 last:none" bg="neutral-50/50 dark:neutral-800/20"
|
||||
>
|
||||
<td whitespace-nowrap px-4 py-2 pl-8 text="xs neutral-400">
|
||||
{{ formatDate(child.createdAt) }}
|
||||
</td>
|
||||
<td px-4 py-2 />
|
||||
<td px-4 py-2 text="xs neutral-400">
|
||||
{{ child.description }}
|
||||
</td>
|
||||
<td px-4 py-2 text-right font-mono text="xs orange-500 dark:orange-400">
|
||||
-{{ child.amount }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Mobile: card list -->
|
||||
<div v-if="auditRecords.length > 0" flex="~ col gap-2" sm:hidden>
|
||||
<div
|
||||
v-for="record in auditRecords"
|
||||
:key="record.id"
|
||||
border="1 neutral-200 dark:neutral-800" flex="~ col gap-1.5" rounded-lg px-3 py-2.5
|
||||
>
|
||||
<div flex="~ items-center justify-between">
|
||||
<span
|
||||
inline-block rounded-full px-2 py-0.5 text-xs font-medium
|
||||
:class="record.type === 'debit'
|
||||
? 'bg-orange-500/10 text-orange-600 dark:text-orange-400'
|
||||
: 'bg-green-500/10 text-green-600 dark:text-green-400'"
|
||||
>
|
||||
{{ record.type === 'debit'
|
||||
? t('settings.pages.flux.audit.typeConsumption')
|
||||
: record.type === 'credit'
|
||||
? t('settings.pages.flux.audit.typeAddition')
|
||||
: t('settings.pages.flux.audit.typeInitial') }}
|
||||
</span>
|
||||
<span text-sm font-semibold font-mono :class="isPositive(record) ? 'text-green-600 dark:text-green-400' : 'text-orange-600 dark:text-orange-400'">
|
||||
{{ displayAmount(record) }}
|
||||
</span>
|
||||
<template v-for="row in groupedRows" :key="row.type === 'single' ? row.record.id : row.key">
|
||||
<!-- Single record card -->
|
||||
<div
|
||||
v-if="row.type === 'single'"
|
||||
border="1 neutral-200 dark:neutral-800" flex="~ col gap-1.5" rounded-lg px-3 py-2.5
|
||||
>
|
||||
<div flex="~ items-center justify-between">
|
||||
<span
|
||||
inline-block rounded-full px-2 py-0.5 text-xs font-medium
|
||||
:class="row.record.type === 'debit'
|
||||
? 'bg-orange-500/10 text-orange-600 dark:text-orange-400'
|
||||
: 'bg-green-500/10 text-green-600 dark:text-green-400'"
|
||||
>
|
||||
{{ row.record.type === 'debit'
|
||||
? t('settings.pages.flux.audit.typeConsumption')
|
||||
: row.record.type === 'credit'
|
||||
? t('settings.pages.flux.audit.typeAddition')
|
||||
: t('settings.pages.flux.audit.typeInitial') }}
|
||||
</span>
|
||||
<span text-sm font-semibold font-mono :class="isPositive(row.record) ? 'text-green-600 dark:text-green-400' : 'text-orange-600 dark:text-orange-400'">
|
||||
{{ displayAmount(row.record) }}
|
||||
</span>
|
||||
</div>
|
||||
<div text="sm neutral-600 dark:neutral-300" truncate>
|
||||
{{ row.record.description }}
|
||||
<span
|
||||
v-if="row.record.metadata?.promptTokens != null"
|
||||
ml-1 text="xs neutral-400"
|
||||
>
|
||||
({{ row.record.metadata.promptTokens }}+{{ row.record.metadata.completionTokens }} tokens)
|
||||
</span>
|
||||
<span
|
||||
v-else-if="row.record.description?.startsWith('tts:') && row.record.metadata?.model"
|
||||
ml-1 text="xs neutral-400"
|
||||
>
|
||||
({{ row.record.metadata.model }})
|
||||
</span>
|
||||
</div>
|
||||
<div text="xs neutral-400">
|
||||
{{ formatDate(row.record.createdAt) }}
|
||||
</div>
|
||||
</div>
|
||||
<div text="sm neutral-600 dark:neutral-300" truncate>
|
||||
{{ record.description }}
|
||||
<span
|
||||
v-if="record.metadata?.promptTokens != null"
|
||||
ml-1 text="xs neutral-400"
|
||||
>
|
||||
({{ record.metadata.promptTokens }}+{{ record.metadata.completionTokens }} tokens)
|
||||
</span>
|
||||
|
||||
<!-- Grouped TTS card -->
|
||||
<div
|
||||
v-else
|
||||
border="1 neutral-200 dark:neutral-800" flex="~ col gap-1.5" cursor-pointer rounded-lg px-3 py-2.5
|
||||
@click="toggleGroup(row.key)"
|
||||
>
|
||||
<div flex="~ items-center justify-between">
|
||||
<span
|
||||
:class="['inline-block', 'rounded-full', 'px-2', 'py-0.5', 'text-xs', 'font-medium',
|
||||
'bg-orange-500/10', 'text-orange-600', 'dark:text-orange-400']"
|
||||
>
|
||||
{{ t('settings.pages.flux.audit.typeConsumption') }}
|
||||
</span>
|
||||
<span text-sm font-semibold font-mono text="orange-600 dark:orange-400">
|
||||
-{{ row.totalAmount }}
|
||||
</span>
|
||||
</div>
|
||||
<div flex="~ items-center gap-1" text="sm neutral-600 dark:neutral-300">
|
||||
<span
|
||||
:class="expandedGroups.has(row.key) ? 'i-solar:alt-arrow-down-line-duotone' : 'i-solar:alt-arrow-right-line-duotone'"
|
||||
inline-block size-4 text="neutral-400"
|
||||
/>
|
||||
{{ row.description }}
|
||||
<span text="xs neutral-400">({{ row.count }} {{ t('settings.pages.flux.audit.ttsRequests') }})</span>
|
||||
</div>
|
||||
<div text="xs neutral-400">
|
||||
{{ formatDate(row.lastTime) }}
|
||||
</div>
|
||||
|
||||
<!-- Expanded children -->
|
||||
<div v-if="row.type === 'group' && expandedGroups.has(row.key)" flex="~ col gap-1" mt-1 border="t neutral-200 dark:neutral-700" pt-2>
|
||||
<div
|
||||
v-for="child in row.records" :key="child.id"
|
||||
flex="~ items-center justify-between" text="xs neutral-400"
|
||||
>
|
||||
<span>{{ formatDate(child.createdAt) }}</span>
|
||||
<span font-mono>-{{ child.amount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div text="xs neutral-400">
|
||||
{{ formatDate(record.createdAt) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div v-if="auditHasMore" text-center>
|
||||
@@ -297,6 +561,24 @@ async function handleBuy(amount: number) {
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.flux-progress-bar {
|
||||
width: 100%;
|
||||
animation: flux-progress-bar-grow 1s cubic-bezier(0.4, 0, 0.2, 1) 0.5s forwards;
|
||||
}
|
||||
|
||||
@keyframes flux-progress-bar-grow {
|
||||
0% {
|
||||
width: 100%;
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
width: v-bind('`${fluxPercentage}%`');
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
|
||||
Generated
+258
-67
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user