Merge pull request #3 from hkxiaoyao/main

feat(account): add trial quota tracking and display
This commit is contained in:
hkxiaoyao
2026-02-06 13:16:24 +08:00
committed by GitHub
parent d89ff02fce
commit d05bd00207
4 changed files with 92 additions and 10 deletions

View File

@@ -65,6 +65,13 @@ type Account struct {
NextResetDate string `json:"nextResetDate,omitempty"` // Date when usage resets (YYYY-MM-DD)
LastRefresh int64 `json:"lastRefresh,omitempty"` // Last info refresh timestamp
// Trial usage tracking
TrialUsageCurrent float64 `json:"trialUsageCurrent,omitempty"` // Trial quota current usage
TrialUsageLimit float64 `json:"trialUsageLimit,omitempty"` // Trial quota total limit
TrialUsagePercent float64 `json:"trialUsagePercent,omitempty"` // Trial quota usage percentage (0.0-1.0)
TrialStatus string `json:"trialStatus,omitempty"` // Trial status: ACTIVE, EXPIRED, NONE
TrialExpiresAt int64 `json:"trialExpiresAt,omitempty"` // Trial expiration timestamp (Unix seconds)
// Runtime statistics (updated during operation)
RequestCount int `json:"requestCount,omitempty"` // Total requests processed
ErrorCount int `json:"errorCount,omitempty"` // Total errors encountered
@@ -109,6 +116,11 @@ type AccountInfo struct {
UsagePercent float64
NextResetDate string
LastRefresh int64
TrialUsageCurrent float64
TrialUsageLimit float64
TrialUsagePercent float64
TrialStatus string
TrialExpiresAt int64
}
var (
@@ -345,6 +357,11 @@ func UpdateAccountInfo(id string, info AccountInfo) error {
cfg.Accounts[i].UsagePercent = info.UsagePercent
cfg.Accounts[i].NextResetDate = info.NextResetDate
cfg.Accounts[i].LastRefresh = info.LastRefresh
cfg.Accounts[i].TrialUsageCurrent = info.TrialUsageCurrent
cfg.Accounts[i].TrialUsageLimit = info.TrialUsageLimit
cfg.Accounts[i].TrialUsagePercent = info.TrialUsagePercent
cfg.Accounts[i].TrialStatus = info.TrialStatus
cfg.Accounts[i].TrialExpiresAt = info.TrialExpiresAt
return Save()
}
}

View File

@@ -1267,15 +1267,20 @@ func (h *Handler) apiGetAccounts(w http.ResponseWriter, r *http.Request) {
"expiresAt": a.ExpiresAt,
"hasToken": a.AccessToken != "",
"machineId": a.MachineId,
"subscriptionType": a.SubscriptionType,
"subscriptionTitle": a.SubscriptionTitle,
"daysRemaining": a.DaysRemaining,
"usageCurrent": a.UsageCurrent,
"usageLimit": a.UsageLimit,
"usagePercent": a.UsagePercent,
"nextResetDate": a.NextResetDate,
"lastRefresh": a.LastRefresh,
"requestCount": stats.RequestCount,
"subscriptionType": a.SubscriptionType,
"subscriptionTitle": a.SubscriptionTitle,
"daysRemaining": a.DaysRemaining,
"usageCurrent": a.UsageCurrent,
"usageLimit": a.UsageLimit,
"usagePercent": a.UsagePercent,
"nextResetDate": a.NextResetDate,
"lastRefresh": a.LastRefresh,
"trialUsageCurrent": a.TrialUsageCurrent,
"trialUsageLimit": a.TrialUsageLimit,
"trialUsagePercent": a.TrialUsagePercent,
"trialStatus": a.TrialStatus,
"trialExpiresAt": a.TrialExpiresAt,
"requestCount": stats.RequestCount,
"errorCount": stats.ErrorCount,
"totalTokens": stats.TotalTokens,
"totalCredits": stats.TotalCredits,

View File

@@ -186,6 +186,28 @@ func RefreshAccountInfo(account *config.Account) (*config.AccountInfo, error) {
}
}
// 解析试用配额信息
if len(usage.UsageBreakdownList) > 0 {
breakdown := usage.UsageBreakdownList[0]
if breakdown.FreeTrialInfo != nil {
info.TrialUsageCurrent = breakdown.FreeTrialInfo.CurrentUsage
info.TrialUsageLimit = breakdown.FreeTrialInfo.UsageLimit
if info.TrialUsageLimit > 0 {
info.TrialUsagePercent = info.TrialUsageCurrent / info.TrialUsageLimit
}
info.TrialStatus = breakdown.FreeTrialInfo.FreeTrialStatus
// 解析试用到期时间
if breakdown.FreeTrialInfo.FreeTrialExpiry != "" {
if ts, err := breakdown.FreeTrialInfo.FreeTrialExpiry.Int64(); err == nil && ts > 0 {
info.TrialExpiresAt = ts
} else if f, err := breakdown.FreeTrialInfo.FreeTrialExpiry.Float64(); err == nil && f > 0 {
info.TrialExpiresAt = int64(f)
}
}
}
}
return info, nil
}

View File

@@ -74,6 +74,9 @@
.usage-fill.high { background: #f59e0b; }
.usage-fill.critical { background: #ef4444; }
.usage-text { display: flex; justify-content: space-between; font-size: 11px; color: #64748b; margin-top: 4px; }
.usage-label { font-size: 12px; color: #64748b; margin-bottom: 4px; font-weight: 500; }
.account-usage + .account-usage { margin-top: 8px; }
.badge-trial { background: #10b981; color: white; }
.account-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin-top: 10px; padding-top: 10px; border-top: 1px solid #e2e8f0; }
.account-stat { text-align: center; }
.account-stat-value { font-weight: 600; font-size: 13px; }
@@ -336,12 +339,15 @@
container.innerHTML = accountsData.map(a => {
const usagePercent = (a.usagePercent || 0) * 100;
const usageClass = usagePercent > 90 ? 'critical' : usagePercent > 70 ? 'high' : '';
const trialUsagePercent = (a.trialUsagePercent || 0) * 100;
const trialUsageClass = trialUsagePercent > 90 ? 'critical' : trialUsagePercent > 70 ? 'high' : '';
return `<div class="account-card">
<div class="account-header">
<div class="account-info">
<div class="account-email">${a.email || a.id.substring(0,12)+'...'}</div>
<div class="account-meta">
${getSubBadge(a.subscriptionType)}
${getTrialBadge(a)}
<span class="badge badge-info">${formatAuthMethod(a.provider || a.authMethod)}</span>
${getStatusBadge(a)}
</div>
@@ -354,12 +360,21 @@
</div>
</div>
${a.usageLimit > 0 ? `<div class="account-usage">
<div class="usage-label">主配额</div>
<div class="usage-bar"><div class="usage-fill ${usageClass}" style="width:${usagePercent}%"></div></div>
<div class="usage-text">
<span>${a.usageCurrent?.toFixed(1) || 0} / ${a.usageLimit?.toFixed(0) || 0}</span>
<span>${usagePercent.toFixed(1)}%</span>
</div>
</div>` : ''}
${a.trialUsageLimit > 0 ? `<div class="account-usage">
<div class="usage-label">试用配额 ${formatTrialExpiry(a.trialExpiresAt)}</div>
<div class="usage-bar"><div class="usage-fill ${trialUsageClass}" style="width:${trialUsagePercent}%"></div></div>
<div class="usage-text">
<span>${a.trialUsageCurrent?.toFixed(1) || 0} / ${a.trialUsageLimit?.toFixed(0) || 0}</span>
<span>${trialUsagePercent.toFixed(1)}%</span>
</div>
</div>` : ''}
<div class="account-stats">
<div class="account-stat"><div class="account-stat-value">${a.requestCount || 0}</div><div class="account-stat-label">请求</div></div>
<div class="account-stat"><div class="account-stat-value">${formatNum(a.totalTokens || 0)}</div><div class="account-stat-label">Tokens</div></div>
@@ -378,6 +393,24 @@
return '<span class="badge badge-free">FREE</span>';
}
function getTrialBadge(account) {
if (account.trialStatus === 'ACTIVE' && account.trialUsageLimit > 0) {
return '<span class="badge badge-trial">试用中</span>';
}
return '';
}
function formatTrialExpiry(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp * 1000);
const now = new Date();
const diffDays = Math.ceil((date - now) / (1000 * 60 * 60 * 24));
if (diffDays < 0) return '(已过期)';
if (diffDays === 0) return '(今天到期)';
if (diffDays <= 7) return `(${diffDays}天后到期)`;
return '';
}
function formatAuthMethod(method) {
if (!method) return '-';
if (method === 'idc') return 'Enterprise';
@@ -438,8 +471,13 @@
<div class="detail-grid">
<div class="detail-item"><div class="detail-label">订阅类型</div><div class="detail-value">${a.subscriptionTitle || a.subscriptionType || '-'}</div></div>
<div class="detail-item"><div class="detail-label">Token到期</div><div class="detail-value">${a.expiresAt ? new Date(a.expiresAt*1000).toLocaleString() : '-'}</div></div>
<div class="detail-item"><div class="detail-label">用量</div><div class="detail-value">${a.usageCurrent?.toFixed(1) || 0} / ${a.usageLimit?.toFixed(0) || 0}</div></div>
<div class="detail-item"><div class="detail-label">主配额</div><div class="detail-value">${a.usageCurrent?.toFixed(1) || 0} / ${a.usageLimit?.toFixed(0) || 0}</div></div>
<div class="detail-item"><div class="detail-label">重置日期</div><div class="detail-value">${a.nextResetDate || '-'}</div></div>
${a.trialUsageLimit > 0 ? `
<div class="detail-item"><div class="detail-label">试用配额</div><div class="detail-value">${a.trialUsageCurrent?.toFixed(1) || 0} / ${a.trialUsageLimit?.toFixed(0) || 0}</div></div>
<div class="detail-item"><div class="detail-label">试用状态</div><div class="detail-value">${a.trialStatus || '-'}</div></div>
<div class="detail-item"><div class="detail-label">试用到期</div><div class="detail-value">${a.trialExpiresAt ? new Date(a.trialExpiresAt*1000).toLocaleString() : '-'}</div></div>
` : ''}
</div>
</div>
<div class="detail-section">