Compare commits

..

14 Commits

Author SHA1 Message Date
huangzhenpc
66d72c7b3b fix(deploy): add compose project name to avoid cross-project conflicts
Some checks failed
CI / test (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
Security Scan / backend-security (push) Has been cancelled
Security Scan / frontend-security (push) Has been cancelled
Both projects use deploy/ directory, causing Docker Compose to share
the same project name "deploy" and volume namespace. Adding explicit
name: fireflyapi isolates volumes and containers completely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 15:22:25 +08:00
huangzhenpc
90c36bb559 feat(deploy): use external build + image instead of compose build
Some checks failed
CI / test (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
Security Scan / backend-security (push) Has been cancelled
Security Scan / frontend-security (push) Has been cancelled
Build image separately with: docker build -t fireflyapi:latest .
Consistent with StarfireAPI deployment pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 14:49:51 +08:00
huangzhenpc
8a22b95906 feat(deploy): change default port to 6680 for independent deployment
Some checks failed
CI / test (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
Security Scan / backend-security (push) Has been cancelled
Security Scan / frontend-security (push) Has been cancelled
Avoid port conflict with StarfireAPI (6580) on the same server.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 14:47:49 +08:00
huangzhenpc
51d8badb05 feat(deploy): switch to local build and rebrand docker-compose
Some checks failed
CI / test (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
Security Scan / backend-security (push) Has been cancelled
Security Scan / frontend-security (push) Has been cancelled
- Replace upstream image with local Dockerfile build
- Rename service/container names to fireflyapi
- Update default DB user/name to fireflyapi
- Update default admin email to fireflyapi.local
- Rename network to fireflyapi-network

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 14:21:05 +08:00
huangzhenpc
58081d0a68 feat(frontend): rebrand Sub2API to FireflyAPI
Some checks failed
CI / test (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
Security Scan / backend-security (push) Has been cancelled
Security Scan / frontend-security (push) Has been cancelled
- Replace all user-visible Sub2API brand names with FireflyAPI
- Remove GitHub navigation link from AppHeader dropdown
- Remove GitHub link from HomeView footer
- Simplify VersionBadge to static version display (remove update check)
- Update i18n zh/en locale strings
- Update default site name fallbacks across all views

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-07 14:11:43 +08:00
Wesley Liddick
e617b45ba3 Merge pull request #508 from touwaeriol/pr/format-time-seconds
feat(frontend): show seconds in rate limit time display
2026-02-07 12:20:29 +08:00
Wesley Liddick
20283bb55b Merge pull request #507 from touwaeriol/pr/fix-429-fallback-default
fix(antigravity): reduce 429 fallback cooldown from 5min to 30s
2026-02-07 12:19:14 +08:00
Wesley Liddick
515dbf2c78 Merge pull request #506 from touwaeriol/pr/fix-max-tokens-budget
fix(antigravity): auto-fix max_tokens <= budget_tokens causing 400 error
2026-02-07 12:18:11 +08:00
Wesley Liddick
2887e280d6 Merge pull request #505 from touwaeriol/pr/gitattributes-lf
chore: add .gitattributes to enforce LF line endings
2026-02-07 12:17:43 +08:00
erio
8826705e71 feat(frontend): show seconds in rate limit time display
Change formatTime() to include seconds (HH:MM:SS) instead of only
hours and minutes (HH:MM). This gives users more precise information
about when rate limits will reset.
2026-02-07 11:59:27 +08:00
erio
8917afab2a fix(antigravity): reduce 429 fallback cooldown from 5min to 30s
The default fallback cooldown when rate limit reset time cannot be
parsed was 5 minutes, which is too aggressive and causes accounts
to be unnecessarily locked out. Reduce to 30 seconds for faster
recovery. Config override still works (unit remains minutes).
2026-02-07 11:54:00 +08:00
erio
49233ec26a fix(antigravity): auto-fix max_tokens <= budget_tokens causing 400 error
When extended thinking is enabled, Claude API requires max_tokens >
thinking.budget_tokens. If misconfigured, this auto-adjusts max_tokens
to budget_tokens + 1000 instead of returning a 400 error.

- Add ensureMaxTokensGreaterThanBudget helper function
- Extract Gemini25FlashThinkingBudgetLimit constant (24576)
- Log adjustment for debugging
2026-02-07 11:49:03 +08:00
erio
1e1cbbee80 chore: add .gitattributes to enforce LF line endings
Ensures consistent line endings for SQL migration files, Go source,
shell scripts, YAML configs, and Dockerfiles. Fixes checksum mismatches
on Windows where CRLF line endings cause migration hash differences.
2026-02-07 11:47:03 +08:00
shaw
39a5b17d31 fix: 账号测试根据类型使用不同的 beta header
- OAuth 账号:使用完整的 DefaultBetaHeader 和 Claude Code 客户端 headers
- API Key 账号:使用 APIKeyBetaHeader(不含 oauth beta)
2026-02-07 11:33:06 +08:00
20 changed files with 120 additions and 645 deletions

15
.gitattributes vendored Normal file
View File

@@ -0,0 +1,15 @@
# 确保所有 SQL 迁移文件使用 LF 换行符
backend/migrations/*.sql text eol=lf
# Go 源代码文件
*.go text eol=lf
# Shell 脚本
*.sh text eol=lf
# YAML/YML 配置文件
*.yaml text eol=lf
*.yml text eol=lf
# Dockerfile
Dockerfile text eol=lf

View File

@@ -57,6 +57,23 @@ func DefaultTransformOptions() TransformOptions {
// webSearchFallbackModel web_search 请求使用的降级模型
const webSearchFallbackModel = "gemini-2.5-flash"
// MaxTokensBudgetPadding max_tokens 自动调整时在 budget_tokens 基础上增加的额度
// Claude API 要求 max_tokens > thinking.budget_tokens否则返回 400 错误
const MaxTokensBudgetPadding = 1000
// Gemini 2.5 Flash thinking budget 上限
const Gemini25FlashThinkingBudgetLimit = 24576
// ensureMaxTokensGreaterThanBudget 确保 max_tokens > budget_tokens
// Claude API 要求启用 thinking 时max_tokens 必须大于 thinking.budget_tokens
// 返回调整后的 maxTokens 和是否进行了调整
func ensureMaxTokensGreaterThanBudget(maxTokens, budgetTokens int) (int, bool) {
if budgetTokens > 0 && maxTokens <= budgetTokens {
return budgetTokens + MaxTokensBudgetPadding, true
}
return maxTokens, false
}
// TransformClaudeToGemini 将 Claude 请求转换为 v1internal Gemini 格式
func TransformClaudeToGemini(claudeReq *ClaudeRequest, projectID, mappedModel string) ([]byte, error) {
return TransformClaudeToGeminiWithOptions(claudeReq, projectID, mappedModel, DefaultTransformOptions())
@@ -527,11 +544,18 @@ func buildGenerationConfig(req *ClaudeRequest) *GeminiGenerationConfig {
}
if req.Thinking.BudgetTokens > 0 {
budget := req.Thinking.BudgetTokens
// gemini-2.5-flash 上限 24576
if strings.Contains(req.Model, "gemini-2.5-flash") && budget > 24576 {
budget = 24576
// gemini-2.5-flash 上限
if strings.Contains(req.Model, "gemini-2.5-flash") && budget > Gemini25FlashThinkingBudgetLimit {
budget = Gemini25FlashThinkingBudgetLimit
}
config.ThinkingConfig.ThinkingBudget = budget
// 自动修正max_tokens 必须大于 budget_tokens
if adjusted, ok := ensureMaxTokensGreaterThanBudget(config.MaxOutputTokens, budget); ok {
log.Printf("[Antigravity] Auto-adjusted max_tokens from %d to %d (must be > budget_tokens=%d)",
config.MaxOutputTokens, adjusted, budget)
config.MaxOutputTokens = adjusted
}
}
}

View File

@@ -245,17 +245,19 @@ func (s *AccountTestService) testClaudeAccountConnection(c *gin.Context, account
// Set common headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("anthropic-version", "2023-06-01")
req.Header.Set("anthropic-beta", claude.DefaultBetaHeader)
// Apply Claude Code client headers
for key, value := range claude.DefaultHeaders {
req.Header.Set(key, value)
}
// Set authentication header
// Set authentication header and beta header based on account type
if useBearer {
// OAuth 账号使用完整的 Claude Code beta header
req.Header.Set("anthropic-beta", claude.DefaultBetaHeader)
req.Header.Set("Authorization", "Bearer "+authToken)
// Apply Claude Code client headers for OAuth
for key, value := range claude.DefaultHeaders {
req.Header.Set(key, value)
}
} else {
// API Key 账号使用简化的 beta header不含 oauth
req.Header.Set("anthropic-beta", claude.APIKeyBetaHeader)
req.Header.Set("x-api-key", authToken)
}

View File

@@ -2048,11 +2048,12 @@ func (s *AntigravityGatewayService) handleUpstreamError(ctx context.Context, pre
resetAt := ParseGeminiRateLimitResetTime(body)
if resetAt == nil {
// 解析失败:使用配置的 fallback 时间,直接限流整个账户
fallbackMinutes := 5
// 默认 30 秒,可通过配置覆盖(配置单位为分钟)
fallbackSeconds := 30
if s.settingService != nil && s.settingService.cfg != nil && s.settingService.cfg.Gateway.AntigravityFallbackCooldownMinutes > 0 {
fallbackMinutes = s.settingService.cfg.Gateway.AntigravityFallbackCooldownMinutes
fallbackSeconds = s.settingService.cfg.Gateway.AntigravityFallbackCooldownMinutes * 60
}
defaultDur := time.Duration(fallbackMinutes) * time.Minute
defaultDur := time.Duration(fallbackSeconds) * time.Second
if fallbackDur, ok := antigravityFallbackCooldownSeconds(); ok {
defaultDur = fallbackDur
}

View File

@@ -1,30 +1,33 @@
# =============================================================================
# Sub2API Docker Compose Configuration
# FireflyAPI Docker Compose Configuration
# =============================================================================
# Quick Start:
# 1. Copy .env.example to .env and configure
# 2. docker-compose up -d
# 3. Check logs: docker-compose logs -f sub2api
# 1. Build image: docker build -t fireflyapi:latest . (in project root)
# 2. Copy .env.example to .env and configure
# 3. docker-compose up -d
# 4. Check logs: docker-compose logs -f fireflyapi
# 4. Access: http://localhost:8080
#
# All configuration is done via environment variables.
# No Setup Wizard needed - the system auto-initializes on first run.
# =============================================================================
name: fireflyapi
services:
# ===========================================================================
# Sub2API Application
# FireflyAPI Application
# ===========================================================================
sub2api:
image: weishaw/sub2api:latest
container_name: sub2api
fireflyapi:
image: fireflyapi:latest
container_name: fireflyapi
restart: unless-stopped
ulimits:
nofile:
soft: 100000
hard: 100000
ports:
- "${BIND_HOST:-0.0.0.0}:${SERVER_PORT:-8080}:8080"
- "${BIND_HOST:-0.0.0.0}:${SERVER_PORT:-6680}:8080"
volumes:
# Data persistence (config.yaml will be auto-generated here)
- sub2api_data:/app/data
@@ -50,9 +53,9 @@ services:
# =======================================================================
- DATABASE_HOST=postgres
- DATABASE_PORT=5432
- DATABASE_USER=${POSTGRES_USER:-sub2api}
- DATABASE_USER=${POSTGRES_USER:-fireflyapi}
- DATABASE_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
- DATABASE_DBNAME=${POSTGRES_DB:-sub2api}
- DATABASE_DBNAME=${POSTGRES_DB:-fireflyapi}
- DATABASE_SSLMODE=disable
# =======================================================================
@@ -67,7 +70,7 @@ services:
# =======================================================================
# Admin Account (auto-created on first run)
# =======================================================================
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@sub2api.local}
- ADMIN_EMAIL=${ADMIN_EMAIL:-admin@fireflyapi.local}
- ADMIN_PASSWORD=${ADMIN_PASSWORD:-}
# =======================================================================
@@ -133,7 +136,7 @@ services:
redis:
condition: service_healthy
networks:
- sub2api-network
- fireflyapi-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
@@ -146,7 +149,7 @@ services:
# ===========================================================================
postgres:
image: postgres:18-alpine
container_name: sub2api-postgres
container_name: fireflyapi-postgres
restart: unless-stopped
ulimits:
nofile:
@@ -155,14 +158,14 @@ services:
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=${POSTGRES_USER:-sub2api}
- POSTGRES_USER=${POSTGRES_USER:-fireflyapi}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
- POSTGRES_DB=${POSTGRES_DB:-sub2api}
- POSTGRES_DB=${POSTGRES_DB:-fireflyapi}
- TZ=${TZ:-Asia/Shanghai}
networks:
- sub2api-network
- fireflyapi-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-sub2api} -d ${POSTGRES_DB:-sub2api}"]
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-fireflyapi} -d ${POSTGRES_DB:-fireflyapi}"]
interval: 10s
timeout: 5s
retries: 5
@@ -175,7 +178,7 @@ services:
# ===========================================================================
redis:
image: redis:8-alpine
container_name: sub2api-redis
container_name: fireflyapi-redis
restart: unless-stopped
ulimits:
nofile:
@@ -195,7 +198,7 @@ services:
# REDISCLI_AUTH is used by redis-cli for authentication (safer than -a flag)
- REDISCLI_AUTH=${REDIS_PASSWORD:-}
networks:
- sub2api-network
- fireflyapi-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
@@ -218,5 +221,5 @@ volumes:
# Networks
# =============================================================================
networks:
sub2api-network:
fireflyapi-network:
driver: bridge

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sub2API - AI API Gateway</title>
<title>FireflyAPI - AI API Gateway</title>
</head>
<body>
<div id="app"></div>

View File

@@ -1,555 +1,11 @@
<template>
<div class="relative">
<!-- Admin: Full version badge with dropdown -->
<template v-if="isAdmin">
<button
@click="toggleDropdown"
class="flex items-center gap-1.5 rounded-lg px-2 py-1 text-xs transition-colors"
:class="[
hasUpdate
? 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400 dark:hover:bg-amber-900/50'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-dark-800 dark:text-dark-400 dark:hover:bg-dark-700'
]"
:title="hasUpdate ? t('version.updateAvailable') : t('version.upToDate')"
>
<span v-if="currentVersion" class="font-medium">v{{ currentVersion }}</span>
<span
v-else
class="h-3 w-12 animate-pulse rounded bg-gray-200 font-medium dark:bg-dark-600"
></span>
<!-- Update indicator -->
<span v-if="hasUpdate" class="relative flex h-2 w-2">
<span
class="absolute inline-flex h-full w-full animate-ping rounded-full bg-amber-400 opacity-75"
></span>
<span class="relative inline-flex h-2 w-2 rounded-full bg-amber-500"></span>
</span>
</button>
<!-- Dropdown -->
<transition name="dropdown">
<div
v-if="dropdownOpen"
ref="dropdownRef"
class="absolute left-0 z-50 mt-2 w-64 overflow-hidden rounded-xl border border-gray-200 bg-white shadow-lg dark:border-dark-700 dark:bg-dark-800"
>
<!-- Header with refresh button -->
<div
class="flex items-center justify-between border-b border-gray-100 px-4 py-3 dark:border-dark-700"
>
<span class="text-sm font-medium text-gray-700 dark:text-dark-300">{{
t('version.currentVersion')
}}</span>
<button
@click="refreshVersion(true)"
class="rounded-lg p-1.5 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-dark-700 dark:hover:text-dark-200"
:disabled="loading"
:title="t('version.refresh')"
>
<Icon
name="refresh"
size="sm"
:stroke-width="2"
:class="{ 'animate-spin': loading }"
/>
</button>
</div>
<div class="p-4">
<!-- Loading state -->
<div v-if="loading" class="flex items-center justify-center py-6">
<svg class="h-6 w-6 animate-spin text-primary-500" fill="none" viewBox="0 0 24 24">
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
</div>
<!-- Content -->
<template v-else>
<!-- Version display - centered and prominent -->
<div class="mb-4 text-center">
<div class="inline-flex items-center gap-2">
<span
v-if="currentVersion"
class="text-2xl font-bold text-gray-900 dark:text-white"
>v{{ currentVersion }}</span
>
<span v-else class="text-2xl font-bold text-gray-400 dark:text-dark-500">--</span>
<!-- Show check mark when up to date -->
<span
v-if="!hasUpdate"
class="flex h-5 w-5 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/30"
>
<svg
class="h-3 w-3 text-green-600 dark:text-green-400"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>
</span>
</div>
<p class="mt-1 text-xs text-gray-500 dark:text-dark-400">
{{
hasUpdate
? t('version.latestVersion') + ': v' + latestVersion
: t('version.upToDate')
}}
</p>
</div>
<!-- Priority 1: Update error (must check before hasUpdate) -->
<div v-if="updateError" class="space-y-2">
<div
class="flex items-center gap-3 rounded-lg border border-red-200 bg-red-50 p-3 dark:border-red-800/50 dark:bg-red-900/20"
>
<div
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/50"
>
<Icon
name="x"
size="sm"
:stroke-width="2"
class="text-red-600 dark:text-red-400"
/>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-red-700 dark:text-red-300">
{{ t('version.updateFailed') }}
</p>
<p class="truncate text-xs text-red-600/70 dark:text-red-400/70">
{{ updateError }}
</p>
</div>
</div>
<!-- Retry button -->
<button
@click="handleUpdate"
:disabled="updating"
class="flex w-full items-center justify-center gap-2 rounded-lg bg-red-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-600 disabled:cursor-not-allowed disabled:opacity-50"
>
{{ t('version.retry') }}
</button>
</div>
<!-- Priority 2: Update success - need restart -->
<div v-else-if="updateSuccess && needRestart" class="space-y-2">
<div
class="flex items-center gap-3 rounded-lg border border-green-200 bg-green-50 p-3 dark:border-green-800/50 dark:bg-green-900/20"
>
<div
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/50"
>
<svg
class="h-4 w-4 text-green-600 dark:text-green-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-green-700 dark:text-green-300">
{{ t('version.updateComplete') }}
</p>
<p class="text-xs text-green-600/70 dark:text-green-400/70">
{{ t('version.restartRequired') }}
</p>
</div>
</div>
<!-- Restart button with countdown -->
<button
@click="handleRestart"
:disabled="restarting"
class="flex w-full items-center justify-center gap-2 rounded-lg bg-green-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-green-600 disabled:cursor-not-allowed disabled:opacity-50"
>
<svg
v-if="restarting"
class="h-4 w-4 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<svg
v-else
class="h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
/>
</svg>
<template v-if="restarting">
<span>{{ t('version.restarting') }}</span>
<span v-if="restartCountdown > 0" class="tabular-nums"
>({{ restartCountdown }}s)</span
>
</template>
<span v-else>{{ t('version.restartNow') }}</span>
</button>
</div>
<!-- Priority 3: Update available for source build - show git pull hint -->
<div v-else-if="hasUpdate && !isReleaseBuild" class="space-y-2">
<a
v-if="releaseInfo?.html_url && releaseInfo.html_url !== '#'"
:href="releaseInfo.html_url"
target="_blank"
rel="noopener noreferrer"
class="group flex items-center gap-3 rounded-lg border border-amber-200 bg-amber-50 p-3 transition-colors hover:bg-amber-100 dark:border-amber-800/50 dark:bg-amber-900/20 dark:hover:bg-amber-900/30"
>
<div
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900/50"
>
<Icon
name="download"
size="sm"
:stroke-width="2"
class="text-amber-600 dark:text-amber-400"
/>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-amber-700 dark:text-amber-300">
{{ t('version.updateAvailable') }}
</p>
<p class="text-xs text-amber-600/70 dark:text-amber-400/70">
v{{ latestVersion }}
</p>
</div>
<svg
class="h-4 w-4 text-amber-500 transition-transform group-hover:translate-x-0.5 dark:text-amber-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
</svg>
</a>
<!-- Source build hint -->
<div
class="flex items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 p-2 dark:border-blue-800/50 dark:bg-blue-900/20"
>
<svg
class="h-3.5 w-3.5 flex-shrink-0 text-blue-500 dark:text-blue-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<p class="text-xs text-blue-600 dark:text-blue-400">
{{ t('version.sourceModeHint') }}
</p>
</div>
</div>
<!-- Priority 4: Update available for release build - show update button -->
<div v-else-if="hasUpdate && isReleaseBuild" class="space-y-2">
<!-- Update info card -->
<div
class="flex items-center gap-3 rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-800/50 dark:bg-amber-900/20"
>
<div
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-amber-100 dark:bg-amber-900/50"
>
<Icon
name="download"
size="sm"
:stroke-width="2"
class="text-amber-600 dark:text-amber-400"
/>
</div>
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-amber-700 dark:text-amber-300">
{{ t('version.updateAvailable') }}
</p>
<p class="text-xs text-amber-600/70 dark:text-amber-400/70">
v{{ latestVersion }}
</p>
</div>
</div>
<!-- Update button -->
<button
@click="handleUpdate"
:disabled="updating"
class="flex w-full items-center justify-center gap-2 rounded-lg bg-primary-500 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-primary-600 disabled:cursor-not-allowed disabled:opacity-50"
>
<svg v-if="updating" class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<Icon v-else name="download" size="sm" :stroke-width="2" />
{{ updating ? t('version.updating') : t('version.updateNow') }}
</button>
<!-- View release link -->
<a
v-if="releaseInfo?.html_url && releaseInfo.html_url !== '#'"
:href="releaseInfo.html_url"
target="_blank"
rel="noopener noreferrer"
class="flex items-center justify-center gap-1 text-xs text-gray-500 transition-colors hover:text-gray-700 dark:text-dark-400 dark:hover:text-dark-200"
>
{{ t('version.viewChangelog') }}
<Icon name="externalLink" size="xs" :stroke-width="2" />
</a>
</div>
<!-- Priority 5: Up to date - show GitHub link -->
<a
v-else-if="releaseInfo?.html_url && releaseInfo.html_url !== '#'"
:href="releaseInfo.html_url"
target="_blank"
rel="noopener noreferrer"
class="flex items-center justify-center gap-2 py-2 text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-dark-400 dark:hover:text-dark-200"
>
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.17 6.839 9.49.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.604-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.464-1.11-1.464-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.167 22 16.418 22 12c0-5.523-4.477-10-10-10z"
/>
</svg>
{{ t('version.viewRelease') }}
</a>
</template>
</div>
</div>
</transition>
</template>
<!-- Non-admin: Simple static version text -->
<span v-else-if="version" class="text-xs text-gray-500 dark:text-dark-400">
v{{ version }}
</span>
</div>
<span v-if="version" class="text-xs text-gray-500 dark:text-dark-400">
v{{ version }}
</span>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAuthStore, useAppStore } from '@/stores'
import { performUpdate, restartService } from '@/api/admin/system'
import Icon from '@/components/icons/Icon.vue'
const { t } = useI18n()
const props = defineProps<{
defineProps<{
version?: string
}>()
const authStore = useAuthStore()
const appStore = useAppStore()
const isAdmin = computed(() => authStore.isAdmin)
const dropdownOpen = ref(false)
const dropdownRef = ref<HTMLElement | null>(null)
// Use store's cached version state
const loading = computed(() => appStore.versionLoading)
const currentVersion = computed(() => appStore.currentVersion || props.version || '')
const latestVersion = computed(() => appStore.latestVersion)
const hasUpdate = computed(() => appStore.hasUpdate)
const releaseInfo = computed(() => appStore.releaseInfo)
const buildType = computed(() => appStore.buildType)
// Update process states (local to this component)
const updating = ref(false)
const restarting = ref(false)
const needRestart = ref(false)
const updateError = ref('')
const updateSuccess = ref(false)
const restartCountdown = ref(0)
// Only show update check for release builds (binary/docker deployment)
const isReleaseBuild = computed(() => buildType.value === 'release')
function toggleDropdown() {
dropdownOpen.value = !dropdownOpen.value
}
function closeDropdown() {
dropdownOpen.value = false
}
async function refreshVersion(force = true) {
if (!isAdmin.value) return
// Reset update states when refreshing
updateError.value = ''
updateSuccess.value = false
needRestart.value = false
await appStore.fetchVersion(force)
}
async function handleUpdate() {
if (updating.value) return
updating.value = true
updateError.value = ''
updateSuccess.value = false
try {
const result = await performUpdate()
updateSuccess.value = true
needRestart.value = result.need_restart
// Clear version cache to reflect update completed
appStore.clearVersionCache()
} catch (error: unknown) {
const err = error as { response?: { data?: { message?: string } }; message?: string }
updateError.value = err.response?.data?.message || err.message || t('version.updateFailed')
} finally {
updating.value = false
}
}
async function handleRestart() {
if (restarting.value) return
restarting.value = true
restartCountdown.value = 8
try {
await restartService()
// Service will restart, page will reload automatically or show disconnected
} catch (error) {
// Expected - connection will be lost during restart
console.log('Service restarting...')
}
// Start countdown
const countdownInterval = setInterval(() => {
restartCountdown.value--
if (restartCountdown.value <= 0) {
clearInterval(countdownInterval)
// Try to check if service is back before reload
checkServiceAndReload()
}
}, 1000)
}
async function checkServiceAndReload() {
const maxRetries = 5
const retryDelay = 1000
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch('/health', {
method: 'GET',
cache: 'no-cache'
})
if (response.ok) {
// Service is back, reload page
window.location.reload()
return
}
} catch {
// Service not ready yet
}
if (i < maxRetries - 1) {
await new Promise((resolve) => setTimeout(resolve, retryDelay))
}
}
// After retries, reload anyway
window.location.reload()
}
function handleClickOutside(event: MouseEvent) {
const target = event.target as Node
const button = (event.target as Element).closest('button')
if (dropdownRef.value && !dropdownRef.value.contains(target) && !button?.contains(target)) {
closeDropdown()
}
}
onMounted(() => {
if (isAdmin.value) {
// Use cached version if available, otherwise fetch
appStore.fetchVersion(false)
}
document.addEventListener('click', handleClickOutside)
})
onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutside)
})
</script>
<style scoped>
.dropdown-enter-active,
.dropdown-leave-active {
transition: all 0.2s ease;
}
.dropdown-enter-from,
.dropdown-leave-to {
opacity: 0;
transform: scale(0.95) translateY(-4px);
}
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@@ -122,22 +122,6 @@
{{ t('nav.apiKeys') }}
</router-link>
<a
href="https://github.com/Wei-Shaw/sub2api"
target="_blank"
rel="noopener noreferrer"
@click="closeDropdown"
class="dropdown-item"
>
<svg class="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.17 6.839 9.49.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.604-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.464-1.11-1.464-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.167 22 16.418 22 12c0-5.523-4.477-10-10-10z"
/>
</svg>
{{ t('nav.github') }}
</a>
</div>
<!-- Contact Support (only show if configured) -->

View File

@@ -65,7 +65,7 @@ import { ref, computed, onMounted } from 'vue'
import { getPublicSettings } from '@/api/auth'
import { sanitizeUrl } from '@/utils/url'
const siteName = ref('Sub2API')
const siteName = ref('FireflyAPI')
const siteLogo = ref('')
const siteSubtitle = ref('Subscription to API Conversion Platform')
@@ -74,7 +74,7 @@ const currentYear = computed(() => new Date().getFullYear())
onMounted(async () => {
try {
const settings = await getPublicSettings()
siteName.value = settings.site_name || 'Sub2API'
siteName.value = settings.site_name || 'FireflyAPI'
siteLogo.value = sanitizeUrl(settings.site_logo || '', { allowRelative: true })
siteSubtitle.value = settings.site_subtitle || 'Subscription to API Conversion Platform'
} catch (error) {

View File

@@ -43,8 +43,8 @@ export default {
// Setup Wizard
setup: {
title: 'Sub2API Setup',
description: 'Configure your Sub2API instance',
title: 'FireflyAPI Setup',
description: 'Configure your FireflyAPI instance',
database: {
title: 'Database Configuration',
description: 'Connect to your PostgreSQL database',
@@ -3066,7 +3066,7 @@ export default {
secretKeyConfiguredHint: 'Secret key configured. Leave empty to keep the current value.' },
linuxdo: {
title: 'LinuxDo Connect Login',
description: 'Configure LinuxDo Connect OAuth for Sub2API end-user login',
description: 'Configure LinuxDo Connect OAuth for FireflyAPI end-user login',
enable: 'Enable LinuxDo Login',
enableHint: 'Show LinuxDo login on the login/register pages',
clientId: 'Client ID',
@@ -3096,7 +3096,7 @@ export default {
title: 'Site Settings',
description: 'Customize site branding',
siteName: 'Site Name',
siteNamePlaceholder: 'Sub2API',
siteNamePlaceholder: 'FireflyAPI',
siteNameHint: 'Displayed in emails and page titles',
siteSubtitle: 'Site Subtitle',
siteSubtitlePlaceholder: 'Subscription to API Conversion Platform',
@@ -3155,7 +3155,7 @@ export default {
fromEmail: 'From Email',
fromEmailPlaceholder: "noreply{'@'}example.com",
fromName: 'From Name',
fromNamePlaceholder: 'Sub2API',
fromNamePlaceholder: 'FireflyAPI',
useTls: 'Use TLS',
useTlsHint: 'Enable TLS encryption for SMTP connection'
},
@@ -3434,14 +3434,14 @@ export default {
// Admin tour steps
admin: {
welcome: {
title: '👋 Welcome to Sub2API',
description: '<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">Sub2API is a powerful AI service gateway platform that helps you easily manage and distribute AI services.</p><p style="margin-bottom: 12px;"><b>🎯 Core Features:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>📦 <b>Group Management</b> - Create service tiers (VIP, Free Trial, etc.)</li><li>🔗 <b>Account Pool</b> - Connect multiple upstream AI service accounts</li><li>🔑 <b>Key Distribution</b> - Generate independent API Keys for users</li><li>💰 <b>Billing Control</b> - Flexible rate and quota management</li></ul><p style="color: #10b981; font-weight: 600;">Let\'s complete the initial setup in 3 minutes →</p></div>',
title: '👋 Welcome to FireflyAPI',
description: '<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">FireflyAPI is a powerful AI service gateway platform that helps you easily manage and distribute AI services.</p><p style="margin-bottom: 12px;"><b>🎯 Core Features:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>📦 <b>Group Management</b> - Create service tiers (VIP, Free Trial, etc.)</li><li>🔗 <b>Account Pool</b> - Connect multiple upstream AI service accounts</li><li>🔑 <b>Key Distribution</b> - Generate independent API Keys for users</li><li>💰 <b>Billing Control</b> - Flexible rate and quota management</li></ul><p style="color: #10b981; font-weight: 600;">Let\'s complete the initial setup in 3 minutes →</p></div>',
nextBtn: 'Start Setup 🚀',
prevBtn: 'Skip'
},
groupManage: {
title: '📦 Step 1: Group Management',
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>What is a Group?</b></p><p style="margin-bottom: 12px;">Groups are the core concept of Sub2API, like a "service package":</p><ul style="margin-left: 20px; margin-bottom: 12px; font-size: 13px;"><li>🎯 Each group can contain multiple upstream accounts</li><li>💰 Each group has independent billing multiplier</li><li>👥 Can be set as public or exclusive</li></ul><p style="margin-top: 12px; padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Example:</b> You can create "VIP Premium" (high rate) and "Free Trial" (low rate) groups</p><p style="margin-top: 16px; color: #10b981; font-weight: 600;">👉 Click "Group Management" on the left sidebar</p></div>'
description: '<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>What is a Group?</b></p><p style="margin-bottom: 12px;">Groups are the core concept of FireflyAPI, like a "service package":</p><ul style="margin-left: 20px; margin-bottom: 12px; font-size: 13px;"><li>🎯 Each group can contain multiple upstream accounts</li><li>💰 Each group has independent billing multiplier</li><li>👥 Can be set as public or exclusive</li></ul><p style="margin-top: 12px; padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 Example:</b> You can create "VIP Premium" (high rate) and "Free Trial" (low rate) groups</p><p style="margin-top: 16px; color: #10b981; font-weight: 600;">👉 Click "Group Management" on the left sidebar</p></div>'
},
createGroup: {
title: ' Create New Group',
@@ -3534,8 +3534,8 @@ export default {
// User tour steps
user: {
welcome: {
title: '👋 Welcome to Sub2API',
description: '<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">Hello! Welcome to the Sub2API AI service platform.</p><p style="margin-bottom: 12px;"><b>🎯 Quick Start:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>🔑 Create API Key</li><li>📋 Copy key to your application</li><li>🚀 Start using AI services</li></ul><p style="color: #10b981; font-weight: 600;">Just 1 minute, let\'s get started →</p></div>',
title: '👋 Welcome to FireflyAPI',
description: '<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">Hello! Welcome to the FireflyAPI AI service platform.</p><p style="margin-bottom: 12px;"><b>🎯 Quick Start:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>🔑 Create API Key</li><li>📋 Copy key to your application</li><li>🚀 Start using AI services</li></ul><p style="color: #10b981; font-weight: 600;">Just 1 minute, let\'s get started →</p></div>',
nextBtn: 'Start 🚀',
prevBtn: 'Skip'
},

View File

@@ -40,8 +40,8 @@ export default {
// Setup Wizard
setup: {
title: 'Sub2API 安装向导',
description: '配置您的 Sub2API 实例',
title: 'FireflyAPI 安装向导',
description: '配置您的 FireflyAPI 实例',
database: {
title: '数据库配置',
description: '连接到您的 PostgreSQL 数据库',
@@ -3237,7 +3237,7 @@ export default {
},
linuxdo: {
title: 'LinuxDo Connect 登录',
description: '配置 LinuxDo Connect OAuth用于 Sub2API 用户登录',
description: '配置 LinuxDo Connect OAuth用于 FireflyAPI 用户登录',
enable: '启用 LinuxDo 登录',
enableHint: '在登录/注册页面显示 LinuxDo 登录入口',
clientId: 'Client ID',
@@ -3267,7 +3267,7 @@ export default {
description: '自定义站点品牌',
siteName: '站点名称',
siteNameHint: '显示在邮件和页面标题中',
siteNamePlaceholder: 'Sub2API',
siteNamePlaceholder: 'FireflyAPI',
siteSubtitle: '站点副标题',
siteSubtitleHint: '显示在登录和注册页面',
siteSubtitlePlaceholder: '订阅转 API 转换平台',
@@ -3327,7 +3327,7 @@ export default {
fromEmail: '发件人邮箱',
fromEmailPlaceholder: "noreply{'@'}example.com",
fromName: '发件人名称',
fromNamePlaceholder: 'Sub2API',
fromNamePlaceholder: 'FireflyAPI',
useTls: '使用 TLS',
useTlsHint: '为 SMTP 连接启用 TLS 加密'
},
@@ -3603,16 +3603,16 @@ export default {
// Admin tour steps
admin: {
welcome: {
title: '👋 欢迎使用 Sub2API',
title: '👋 欢迎使用 FireflyAPI',
description:
'<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">Sub2API 是一个强大的 AI 服务中转平台,让您轻松管理和分发 AI 服务。</p><p style="margin-bottom: 12px;"><b>🎯 核心功能:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>📦 <b>分组管理</b> - 创建不同的服务套餐VIP、免费试用等</li><li>🔗 <b>账号池</b> - 连接多个上游 AI 服务商账号</li><li>🔑 <b>密钥分发</b> - 为用户生成独立的 API Key</li><li>💰 <b>计费管理</b> - 灵活的费率和配额控制</li></ul><p style="color: #10b981; font-weight: 600;">接下来,我们将用 3 分钟带您完成首次配置 →</p></div>',
'<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">FireflyAPI 是一个强大的 AI 服务中转平台,让您轻松管理和分发 AI 服务。</p><p style="margin-bottom: 12px;"><b>🎯 核心功能:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>📦 <b>分组管理</b> - 创建不同的服务套餐VIP、免费试用等</li><li>🔗 <b>账号池</b> - 连接多个上游 AI 服务商账号</li><li>🔑 <b>密钥分发</b> - 为用户生成独立的 API Key</li><li>💰 <b>计费管理</b> - 灵活的费率和配额控制</li></ul><p style="color: #10b981; font-weight: 600;">接下来,我们将用 3 分钟带您完成首次配置 →</p></div>',
nextBtn: '开始配置 🚀',
prevBtn: '跳过'
},
groupManage: {
title: '📦 第一步:分组管理',
description:
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>什么是分组?</b></p><p style="margin-bottom: 12px;">分组是 Sub2API 的核心概念,它就像一个"服务套餐"</p><ul style="margin-left: 20px; margin-bottom: 12px; font-size: 13px;"><li>🎯 每个分组可以包含多个上游账号</li><li>💰 每个分组有独立的计费倍率</li><li>👥 可以设置为公开或专属分组</li></ul><p style="margin-top: 12px; padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 示例:</b>您可以创建"VIP专线"(高倍率)和"免费试用"(低倍率)两个分组</p><p style="margin-top: 16px; color: #10b981; font-weight: 600;">👉 点击左侧的"分组管理"开始</p></div>'
'<div style="line-height: 1.7;"><p style="margin-bottom: 12px;"><b>什么是分组?</b></p><p style="margin-bottom: 12px;">分组是 FireflyAPI 的核心概念,它就像一个"服务套餐"</p><ul style="margin-left: 20px; margin-bottom: 12px; font-size: 13px;"><li>🎯 每个分组可以包含多个上游账号</li><li>💰 每个分组有独立的计费倍率</li><li>👥 可以设置为公开或专属分组</li></ul><p style="margin-top: 12px; padding: 8px 12px; background: #f0fdf4; border-left: 3px solid #10b981; border-radius: 4px; font-size: 13px;"><b>💡 示例:</b>您可以创建"VIP专线"(高倍率)和"免费试用"(低倍率)两个分组</p><p style="margin-top: 16px; color: #10b981; font-weight: 600;">👉 点击左侧的"分组管理"开始</p></div>'
},
createGroup: {
title: ' 创建新分组',
@@ -3724,9 +3724,9 @@ export default {
// User tour steps
user: {
welcome: {
title: '👋 欢迎使用 Sub2API',
title: '👋 欢迎使用 FireflyAPI',
description:
'<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">您好!欢迎来到 Sub2API AI 服务平台。</p><p style="margin-bottom: 12px;"><b>🎯 快速开始:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>🔑 创建 API 密钥</li><li>📋 复制密钥到您的应用</li><li>🚀 开始使用 AI 服务</li></ul><p style="color: #10b981; font-weight: 600;">只需 1 分钟,让我们开始吧 →</p></div>',
'<div style="line-height: 1.8;"><p style="margin-bottom: 16px;">您好!欢迎来到 FireflyAPI AI 服务平台。</p><p style="margin-bottom: 12px;"><b>🎯 快速开始:</b></p><ul style="margin-left: 20px; margin-bottom: 16px;"><li>🔑 创建 API 密钥</li><li>📋 复制密钥到您的应用</li><li>🚀 开始使用 AI 服务</li></ul><p style="color: #10b981; font-weight: 600;">只需 1 分钟,让我们开始吧 →</p></div>',
nextBtn: '开始 🚀',
prevBtn: '跳过'
},

View File

@@ -16,7 +16,7 @@ const appStore = useAppStore()
appStore.initFromInjectedConfig()
// Set document title immediately after config is loaded
if (appStore.siteName && appStore.siteName !== 'Sub2API') {
if (appStore.siteName && appStore.siteName !== 'FireflyAPI') {
document.title = `${appStore.siteName} - AI API Gateway`
}

View File

@@ -389,7 +389,7 @@ router.beforeEach((to, _from, next) => {
// Set page title
const appStore = useAppStore()
const siteName = appStore.siteName || 'Sub2API'
const siteName = appStore.siteName || 'FireflyAPI'
if (to.meta.title) {
document.title = `${to.meta.title} - ${siteName}`
} else {

View File

@@ -24,7 +24,7 @@ export const useAppStore = defineStore('app', () => {
// Public settings cache state
const publicSettingsLoaded = ref<boolean>(false)
const publicSettingsLoading = ref<boolean>(false)
const siteName = ref<string>('Sub2API')
const siteName = ref<string>('FireflyAPI')
const siteLogo = ref<string>('')
const siteVersion = ref<string>('')
const contactInfo = ref<string>('')
@@ -284,7 +284,7 @@ export const useAppStore = defineStore('app', () => {
*/
function applySettings(config: PublicSettings): void {
cachedPublicSettings.value = config
siteName.value = config.site_name || 'Sub2API'
siteName.value = config.site_name || 'FireflyAPI'
siteLogo.value = config.site_logo || ''
siteVersion.value = config.version || ''
contactInfo.value = config.contact_info || ''

View File

@@ -204,7 +204,7 @@ export function formatReasoningEffort(effort: string | null | undefined): string
}
/**
* 格式化时间(显示时分)
* 格式化时间(显示时分
* @param date 日期字符串或 Date 对象
* @returns 格式化后的时间字符串
*/
@@ -212,6 +212,7 @@ export function formatTime(date: string | Date | null | undefined): string {
return formatDate(date, {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
})
}

View File

@@ -390,14 +390,6 @@
>
{{ t('home.docs') }}
</a>
<a
:href="githubUrl"
target="_blank"
rel="noopener noreferrer"
class="text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-dark-400 dark:hover:text-white"
>
GitHub
</a>
</div>
</div>
</footer>
@@ -417,7 +409,7 @@ const authStore = useAuthStore()
const appStore = useAppStore()
// Site settings - directly from appStore (already initialized from injected config)
const siteName = computed(() => appStore.cachedPublicSettings?.site_name || appStore.siteName || 'Sub2API')
const siteName = computed(() => appStore.cachedPublicSettings?.site_name || appStore.siteName || 'FireflyAPI')
const siteLogo = computed(() => appStore.cachedPublicSettings?.site_logo || appStore.siteLogo || '')
const siteSubtitle = computed(() => appStore.cachedPublicSettings?.site_subtitle || 'AI API Gateway Platform')
const docUrl = computed(() => appStore.cachedPublicSettings?.doc_url || appStore.docUrl || '')
@@ -432,9 +424,6 @@ const isHomeContentUrl = computed(() => {
// Theme
const isDark = ref(document.documentElement.classList.contains('dark'))
// GitHub URL
const githubUrl = 'https://github.com/Wei-Shaw/sub2api'
// Auth state
const isAuthenticated = computed(() => authStore.isAuthenticated)
const isAdmin = computed(() => authStore.isAdmin)

View File

@@ -1135,7 +1135,7 @@ const form = reactive<SettingsForm>({
totp_encryption_key_configured: false,
default_balance: 0,
default_concurrency: 1,
site_name: 'Sub2API',
site_name: 'FireflyAPI',
site_logo: '',
site_subtitle: 'Subscription to API Conversion Platform',
api_base_url: '',

View File

@@ -207,7 +207,7 @@ const hasRegisterData = ref<boolean>(false)
// Public settings
const turnstileEnabled = ref<boolean>(false)
const turnstileSiteKey = ref<string>('')
const siteName = ref<string>('Sub2API')
const siteName = ref<string>('FireflyAPI')
// Turnstile for resend
const turnstileRef = ref<InstanceType<typeof TurnstileWidget> | null>(null)
@@ -243,7 +243,7 @@ onMounted(async () => {
const settings = await getPublicSettings()
turnstileEnabled.value = settings.turnstile_enabled
turnstileSiteKey.value = settings.turnstile_site_key || ''
siteName.value = settings.site_name || 'Sub2API'
siteName.value = settings.site_name || 'FireflyAPI'
} catch (error) {
console.error('Failed to load public settings:', error)
}

View File

@@ -317,7 +317,7 @@ const promoCodeEnabled = ref<boolean>(true)
const invitationCodeEnabled = ref<boolean>(false)
const turnstileEnabled = ref<boolean>(false)
const turnstileSiteKey = ref<string>('')
const siteName = ref<string>('Sub2API')
const siteName = ref<string>('FireflyAPI')
const linuxdoOAuthEnabled = ref<boolean>(false)
// Turnstile
@@ -368,7 +368,7 @@ onMounted(async () => {
invitationCodeEnabled.value = settings.invitation_code_enabled
turnstileEnabled.value = settings.turnstile_enabled
turnstileSiteKey.value = settings.turnstile_site_key || ''
siteName.value = settings.site_name || 'Sub2API'
siteName.value = settings.site_name || 'FireflyAPI'
linuxdoOAuthEnabled.value = settings.linuxdo_oauth_enabled
// Read promo code from URL parameter only if promo code is enabled

View File

@@ -131,7 +131,7 @@
v-model="formData.database.dbname"
type="text"
class="input"
placeholder="sub2api"
placeholder="fireflyapi"
/>
</div>
<div>
@@ -535,7 +535,7 @@ const formData = reactive<InstallRequest>({
port: 5432,
user: 'postgres',
password: '',
dbname: 'sub2api',
dbname: 'fireflyapi',
sslmode: 'disable'
},
redis: {