From 22ce5a8ece5e282c7dbd4a310256e14968bff7a4 Mon Sep 17 00:00:00 2001 From: quantal Date: Mon, 18 May 2026 00:35:36 +0800 Subject: [PATCH 01/20] fix: update airgate-sdk checksum --- backend/go.sum | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/go.sum b/backend/go.sum index e8dac71..64d2c95 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,4 +1,4 @@ -github.com/DouDOU-start/airgate-sdk v0.2.0 h1:69NmSLic7kgiffhc9EdVsPTJTZV2jlXgVYNw3+wYDrY= +github.com/DouDOU-start/airgate-sdk v0.2.0 h1:ajVxsfQywevfoJSQWZ6zf1zASvLePWKRnSCIyHHEN70= github.com/DouDOU-start/airgate-sdk v0.2.0/go.mod h1:784vC4lIfCnUdOroWuvn55Dv/b9TucVMpFwuu40gc0Q= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= From d96be84d546de789bf88020d34ee22bc4e090a60 Mon Sep 17 00:00:00 2001 From: quantal Date: Mon, 18 May 2026 12:19:04 +0800 Subject: [PATCH 02/20] feat: extend account usage window structure with additional fields and update related functions --- backend/internal/gateway/account_usage.go | 30 ++++++++++++++--------- backend/internal/gateway/gateway.go | 12 ++++----- web/src/components/UsageWindow.tsx | 16 +++++++----- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/backend/internal/gateway/account_usage.go b/backend/internal/gateway/account_usage.go index 6285904..4b69539 100644 --- a/backend/internal/gateway/account_usage.go +++ b/backend/internal/gateway/account_usage.go @@ -10,12 +10,15 @@ type quotaInfo struct { // accountUsageWindow 是 Claude 插件私有的账号用量窗口。 type accountUsageWindow struct { - Key string `json:"key"` - Label string `json:"label"` - UsedPercent float64 `json:"used_percent"` - ResetAt string `json:"reset_at,omitempty"` - ResetAfterSeconds int `json:"reset_after_seconds,omitempty"` - UpdatedAt string `json:"updated_at,omitempty"` + Key string `json:"key"` + Label string `json:"label"` + DisplayLabel string `json:"display_label,omitempty"` + Slot string `json:"slot,omitempty"` + Group string `json:"group,omitempty"` + UsedPercent float64 `json:"used_percent"` + ResetAt string `json:"reset_at,omitempty"` + ResetSeconds int `json:"reset_seconds,omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` } type accountUsageInfo struct { @@ -33,17 +36,20 @@ type accountUsageAccountsResponse struct { Errors []accountUsageError `json:"errors,omitempty"` } -func newAccountUsageWindow(key, label string, usedPercent float64, resetAt *time.Time, now time.Time) accountUsageWindow { +func newAccountUsageWindow(key, label, displayLabel, slot, group string, usedPercent float64, resetAt *time.Time, now time.Time) accountUsageWindow { window := accountUsageWindow{ - Key: key, - Label: label, - UsedPercent: usedPercent, - UpdatedAt: now.UTC().Format(time.RFC3339), + Key: key, + Label: label, + DisplayLabel: displayLabel, + Slot: slot, + Group: group, + UsedPercent: usedPercent, + UpdatedAt: now.UTC().Format(time.RFC3339), } if resetAt != nil { window.ResetAt = resetAt.UTC().Format(time.RFC3339) if resetAt.After(now) { - window.ResetAfterSeconds = int(resetAt.Sub(now).Seconds()) + window.ResetSeconds = int(resetAt.Sub(now).Seconds()) } } return window diff --git a/backend/internal/gateway/gateway.go b/backend/internal/gateway/gateway.go index e16cb88..b7e34cd 100644 --- a/backend/internal/gateway/gateway.go +++ b/backend/internal/gateway/gateway.go @@ -351,24 +351,24 @@ func (g *AnthropicGateway) HandleRequest(ctx context.Context, _, path, _ string, } // 5h 窗口(Current session) - appendUsageWindow := func(key, label string, utilization float64, resetsAt string) { + appendUsageWindow := func(key, label, displayLabel, slot, group string, utilization float64, resetsAt string) { if resetsAt != "" { if resetAt, err := time.Parse(time.RFC3339, resetsAt); err == nil { - info.Windows = append(info.Windows, newAccountUsageWindow(key, label, utilization, &resetAt, now)) + info.Windows = append(info.Windows, newAccountUsageWindow(key, label, displayLabel, slot, group, utilization, &resetAt, now)) return } } - info.Windows = append(info.Windows, newAccountUsageWindow(key, label, utilization, nil, now)) + info.Windows = append(info.Windows, newAccountUsageWindow(key, label, displayLabel, slot, group, utilization, nil, now)) } - appendUsageWindow("5h", "5h", usageResp.FiveHour.Utilization, usageResp.FiveHour.ResetsAt) + appendUsageWindow("5h", "5h", "5h", "5h", "base", usageResp.FiveHour.Utilization, usageResp.FiveHour.ResetsAt) // 7d 窗口(全部模型) - appendUsageWindow("7d", "7d", usageResp.SevenDay.Utilization, usageResp.SevenDay.ResetsAt) + appendUsageWindow("7d", "7d", "7d", "7d", "base", usageResp.SevenDay.Utilization, usageResp.SevenDay.ResetsAt) // 7d Sonnet 窗口(仅当 API 返回了该窗口数据) if usageResp.SevenDaySonnet.ResetsAt != "" || usageResp.SevenDaySonnet.Utilization > 0 { - appendUsageWindow("7d_sonnet", "7d Sonnet", usageResp.SevenDaySonnet.Utilization, usageResp.SevenDaySonnet.ResetsAt) + appendUsageWindow("model:7d:sonnet", "7d Sonnet", "7d", "7d", "model:sonnet", usageResp.SevenDaySonnet.Utilization, usageResp.SevenDaySonnet.ResetsAt) } resp.Accounts[strconv.FormatInt(a.ID, 10)] = info diff --git a/web/src/components/UsageWindow.tsx b/web/src/components/UsageWindow.tsx index 0699087..075cd80 100644 --- a/web/src/components/UsageWindow.tsx +++ b/web/src/components/UsageWindow.tsx @@ -4,9 +4,11 @@ import type { AccountSurfaceProps } from '@doudou-start/airgate-theme/plugin'; interface UsageWindowItem { key?: string; label: string; + display_label?: string; + slot?: string; + group?: string; used_percent: number; reset_seconds?: number; - reset_after_seconds?: number; reset_at?: string; } @@ -24,7 +26,6 @@ function getUsageWindows(context: AccountSurfaceProps['context']): UsageWindowIt function resolveResetSeconds(w: UsageWindowItem) { if (typeof w.reset_seconds === 'number') return w.reset_seconds; - if (typeof w.reset_after_seconds === 'number') return w.reset_after_seconds; if (w.reset_at) { const delta = Date.parse(w.reset_at) - Date.now(); if (Number.isFinite(delta) && delta > 0) return Math.floor(delta / 1000); @@ -49,11 +50,13 @@ function usageColor(pct: number) { } function windowOrder(w: UsageWindowItem) { + const slot = (w.slot || '').toLowerCase(); + const group = (w.group || '').toLowerCase(); const key = (w.key || '').toLowerCase(); const label = w.label.toLowerCase(); - if (key === '5h' || label.startsWith('5h')) return 0; - if (key === '7d' || label === '7d') return 1; - if (key.includes('sonnet') || label.includes('sonnet')) return 2; + if (slot === '5h' || key === '5h' || label.startsWith('5h')) return 0; + if ((slot === '7d' && group === 'base') || key === '7d' || label === '7d') return 1; + if (group.includes('sonnet') || key.includes('sonnet') || label.includes('sonnet')) return 2; return 3; } @@ -133,9 +136,10 @@ export function UsageWindow({ context }: AccountSurfaceProps) { const barPercent = Math.max(0, Math.min(100, percent)); const color = usageColor(w.used_percent); const resetText = formatReset(resolveResetSeconds(w)); + const displayLabel = w.display_label?.trim() || w.slot?.trim() || w.label; return (
- {w.label} + {displayLabel}
Date: Mon, 18 May 2026 12:32:41 +0800 Subject: [PATCH 03/20] chore: ignore local git attributes --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 33f67a3..29bc34c 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ backend/go.work.sum .idea/ .vscode/ *.swp + +# Local git attributes +.gitattributes From a61f3729f99a80a974a729f0746d741155b2c946 Mon Sep 17 00:00:00 2001 From: quantal Date: Mon, 18 May 2026 12:47:20 +0800 Subject: [PATCH 04/20] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20useResetTick?= =?UTF-8?q?=20=E9=92=A9=E5=AD=90=E4=BB=A5=E5=A4=84=E7=90=86=E9=87=8D?= =?UTF-8?q?=E7=BD=AE=E7=A7=92=E6=95=B0=E7=9A=84=E6=9B=B4=E6=96=B0=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/UsageWindow.tsx | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/web/src/components/UsageWindow.tsx b/web/src/components/UsageWindow.tsx index 075cd80..618e514 100644 --- a/web/src/components/UsageWindow.tsx +++ b/web/src/components/UsageWindow.tsx @@ -1,4 +1,4 @@ -import type { CSSProperties } from 'react'; +import { useEffect, useState, type CSSProperties } from 'react'; import type { AccountSurfaceProps } from '@doudou-start/airgate-theme/plugin'; interface UsageWindowItem { @@ -24,12 +24,24 @@ function getUsageWindows(context: AccountSurfaceProps['context']): UsageWindowIt return windows.filter(isUsageWindowItem); } -function resolveResetSeconds(w: UsageWindowItem) { - if (typeof w.reset_seconds === 'number') return w.reset_seconds; +function useResetTick(enabled: boolean) { + const [now, setNow] = useState(() => Date.now()); + + useEffect(() => { + if (!enabled) return undefined; + const timer = window.setInterval(() => setNow(Date.now()), 30_000); + return () => window.clearInterval(timer); + }, [enabled]); + + return now; +} + +function resolveResetSeconds(w: UsageWindowItem, now: number) { if (w.reset_at) { - const delta = Date.parse(w.reset_at) - Date.now(); - if (Number.isFinite(delta) && delta > 0) return Math.floor(delta / 1000); + const delta = Date.parse(w.reset_at) - now; + if (Number.isFinite(delta)) return Math.max(0, Math.floor(delta / 1000)); } + if (typeof w.reset_seconds === 'number') return w.reset_seconds; return 0; } @@ -127,6 +139,7 @@ const resetStyle: CSSProperties = { export function UsageWindow({ context }: AccountSurfaceProps) { const windows = [...getUsageWindows(context)].sort((a, b) => windowOrder(a) - windowOrder(b)); + const resetNow = useResetTick(windows.length > 0); if (windows.length === 0) return null; return ( @@ -135,7 +148,7 @@ export function UsageWindow({ context }: AccountSurfaceProps) { const percent = Math.round(w.used_percent); const barPercent = Math.max(0, Math.min(100, percent)); const color = usageColor(w.used_percent); - const resetText = formatReset(resolveResetSeconds(w)); + const resetText = formatReset(resolveResetSeconds(w, resetNow)); const displayLabel = w.display_label?.trim() || w.slot?.trim() || w.label; return (
From 251d1d0819e72077712985305633cad1ed427644 Mon Sep 17 00:00:00 2001 From: quantal Date: Mon, 25 May 2026 12:40:35 +0800 Subject: [PATCH 05/20] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96token=E8=AE=A1?= =?UTF-8?q?=E9=87=8F=E8=BE=93=E5=87=BA=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/UsageMetricDetail.tsx | 35 ++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/web/src/components/UsageMetricDetail.tsx b/web/src/components/UsageMetricDetail.tsx index 7271d64..c7ca619 100644 --- a/web/src/components/UsageMetricDetail.tsx +++ b/web/src/components/UsageMetricDetail.tsx @@ -73,6 +73,27 @@ const labelStyle: CSSProperties = { whiteSpace: 'nowrap', }; +const inlineValueStyle: CSSProperties = { + display: 'inline-flex', + minWidth: 0, + maxWidth: '100%', + alignItems: 'baseline', + justifyContent: 'flex-end', + gap: '0.25rem', +}; + +const inlineValueMetaStyle: CSSProperties = { + minWidth: 0, + overflow: 'hidden', + color: 'var(--ag-text-tertiary)', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', +}; + +const inlineValueNumberStyle: CSSProperties = { + flexShrink: 0, +}; + const valueStyle: CSSProperties = { minWidth: 0, maxWidth: '12rem', @@ -124,6 +145,17 @@ function Row({ label, tone, value }: { label: ReactNode; tone?: string; value: R ); } +function outputTokenValue(reasoningTokens: number, outputTokens: number) { + return ( + + {reasoningTokens > 0 ? ( + (推理 {formatNumber(reasoningTokens)}) + ) : null} + {formatNumber(outputTokens)} + + ); +} + export function UsageMetricDetail({ context }: UsageRecordSurfaceProps) { const record = recordFromContext(context); const metrics = contextArray(context, 'usageMetrics', 'usage_metrics'); @@ -144,12 +176,11 @@ export function UsageMetricDetail({ context }: UsageRecordSurfaceProps) {
- + {cacheReadTokens > 0 ? : null} {cacheCreationTokens > 0 ? : null} {cacheCreation5mTokens > 0 ? : null} {cacheCreation1hTokens > 0 ? : null} - {reasoningTokens > 0 ? : null}
From 300a742bd1777fe3021d93aa9960c9e71fe219ae Mon Sep 17 00:00:00 2001 From: quantal Date: Wed, 27 May 2026 21:29:48 +0800 Subject: [PATCH 06/20] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E7=94=A8?= =?UTF-8?q?=E9=87=8F=E6=A8=A1=E5=9E=8B=E5=92=8C=E8=AE=A1=E8=B4=B9=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/gateway/models.go | 63 +++++++++++++++--------- web/src/components/UsageCostDetail.tsx | 46 ++++++++--------- web/src/components/UsageMetricDetail.tsx | 43 +++------------- 3 files changed, 70 insertions(+), 82 deletions(-) diff --git a/backend/internal/gateway/models.go b/backend/internal/gateway/models.go index 6154049..8e25876 100644 --- a/backend/internal/gateway/models.go +++ b/backend/internal/gateway/models.go @@ -194,21 +194,21 @@ func newTokenUsage(modelID string, tokens tokenUsage, firstTokenMs int64) *sdk.U } func setUsageModelAttribute(usage *sdk.Usage, modelID string) { - if usage == nil || modelID == "" { - return - } - setUsageAttribute(usage, sdk.UsageAttribute{ - Key: usageAttrModel, - Label: "模型", - Kind: "model", - Value: modelID, - }) + _ = usage + _ = modelID } func setUsageTokens(usage *sdk.Usage, tokens tokenUsage) { if usage == nil { return } + usage.InputTokens = tokens.inputTokens + usage.OutputTokens = tokens.outputTokens + usage.CachedInputTokens = tokens.cachedInputTokens + usage.CacheCreationTokens = tokens.cacheCreationTokens + usage.CacheCreation5mTokens = tokens.cacheCreation5mTokens + usage.CacheCreation1hTokens = tokens.cacheCreation1hTokens + usage.ReasoningOutputTokens = tokens.reasoningOutputTokens setUsageMetric(usage, sdk.UsageMetric{ Key: usageMetricInputTokens, Label: "输入 Token", @@ -275,22 +275,30 @@ func usageMetricValue(usage *sdk.Usage, key string) float64 { if usage == nil { return 0 } - for _, metric := range usage.Metrics { - if metric.Key == key { - return metric.Value - } + switch key { + case usageMetricInputTokens: + return float64(usage.InputTokens) + case usageMetricCachedInputTokens: + return float64(usage.CachedInputTokens) + case usageMetricCacheCreationTokens: + return float64(usage.CacheCreationTokens) + case usageMetricCacheCreation5mTokens: + return float64(usage.CacheCreation5mTokens) + case usageMetricCacheCreation1hTokens: + return float64(usage.CacheCreation1hTokens) + case usageMetricOutputTokens: + return float64(usage.OutputTokens) + case usageMetricReasoningOutputTokens: + return float64(usage.ReasoningOutputTokens) + case usageMetricTotalTokens: + return float64(usage.InputTokens + usage.CachedInputTokens + usage.CacheCreationTokens + usage.OutputTokens) } return 0 } func setUsageAttribute(usage *sdk.Usage, attr sdk.UsageAttribute) { - for i := range usage.Attributes { - if usage.Attributes[i].Key == attr.Key { - usage.Attributes[i] = attr - return - } - } - usage.Attributes = append(usage.Attributes, attr) + _ = usage + _ = attr } func setUsageMetric(usage *sdk.Usage, metric sdk.UsageMetric) { @@ -336,10 +344,7 @@ func recomputeUsageAccountCost(usage *sdk.Usage) { if usage == nil { return } - var total float64 - for _, detail := range usage.CostDetails { - total += detail.AccountCost - } + total := usage.InputCost + usage.OutputCost + usage.CachedInputCost + usage.CacheCreationCost usage.AccountCost = total if usage.Currency == "" { usage.Currency = usageCurrencyUSD @@ -390,6 +395,16 @@ func fillUsageCost(usage *sdk.Usage) { cacheCreation5mCost := tokenCost(billableCacheCreation5mTokens, spec.CacheCreationPrice) cacheCreation1hCost := tokenCost(cacheCreation1hTokens, spec.CacheCreation1hPrice) outputCost := tokenCost(outputTokens, spec.OutputPrice) + usage.InputPrice = spec.InputPrice + usage.CachedInputPrice = spec.CachedPrice + usage.CacheCreationPrice = spec.CacheCreationPrice + usage.CacheCreation1hPrice = spec.CacheCreation1hPrice + usage.OutputPrice = spec.OutputPrice + usage.InputCost = inputCost + usage.CachedInputCost = cachedCost + usage.CacheCreationCost = cacheCreation5mCost + cacheCreation1hCost + usage.OutputCost = outputCost + recomputeUsageAccountCost(usage) setUsageMetric(usage, sdk.UsageMetric{ Key: usageMetricInputTokens, diff --git a/web/src/components/UsageCostDetail.tsx b/web/src/components/UsageCostDetail.tsx index 152d1a8..4232572 100644 --- a/web/src/components/UsageCostDetail.tsx +++ b/web/src/components/UsageCostDetail.tsx @@ -6,9 +6,6 @@ interface UsageCostDetailItem { label?: string; account_cost?: number; user_cost?: number; - billing_multiplier?: number; - currency?: string; - metadata?: Record; } interface UsageRecordLike { @@ -27,6 +24,7 @@ interface UsageRecordLike { service_tier?: string; input_price?: number; output_price?: number; + usage_metadata?: Record; } const panelStyle: CSSProperties = { @@ -100,16 +98,24 @@ const dividerStyle: CSSProperties = { borderTop: '1px solid var(--ag-border)', }; -function contextArray(context: UsageRecordSurfaceProps['context'], camel: string, snake: string): T[] { - const value = context?.[camel] ?? context?.[snake]; - return Array.isArray(value) ? value as T[] : []; -} - function recordFromContext(context: UsageRecordSurfaceProps['context']): UsageRecordLike { const record = context?.record; return record && typeof record === 'object' ? record as UsageRecordLike : {}; } +function metadataFromContext(context: UsageRecordSurfaceProps['context'], record: UsageRecordLike): Record { + const fromContext = context?.usageMetadata ?? context?.usage_metadata; + if (fromContext && typeof fromContext === 'object' && !Array.isArray(fromContext)) { + return fromContext as Record; + } + return record.usage_metadata ?? {}; +} + +function metadataText(metadata: Record, key: string) { + const value = metadata[key]?.trim(); + return value || ''; +} + function money(value: unknown) { const amount = typeof value === 'number' && Number.isFinite(value) ? value : 0; return `$${amount.toFixed(6)}`; @@ -134,12 +140,6 @@ function toCostLabel(raw: string): string { return stripTokenSuffix(s) + '成本'; } -function toUnitLabel(raw: string): string { - const s = raw.trim(); - if (s.includes('单价') || s.toLowerCase().includes('price')) return s; - return stripTokenSuffix(s) + '单价'; -} - function fallbackDetails(record: UsageRecordLike): UsageCostDetailItem[] { return [ { key: 'input_tokens', label: '输入', account_cost: record.input_cost }, @@ -151,19 +151,19 @@ function fallbackDetails(record: UsageRecordLike): UsageCostDetailItem[] { export function UsageCostDetail({ context }: UsageRecordSurfaceProps) { const record = recordFromContext(context); + const metadata = metadataFromContext(context, record); const isAdmin = context?.adminView !== false; - const details = contextArray(context, 'usageCostDetails', 'usage_cost_details'); - const rows = details.length > 0 ? details : fallbackDetails(record); + const rows = fallbackDetails(record); const unitPrices: { label: string; value: string }[] = []; - for (const item of rows) { - if (item.metadata?.unit_price && item.metadata?.unit) { - unitPrices.push({ - label: toUnitLabel(item.label || item.key || ''), - value: `$${Number(item.metadata.unit_price).toFixed(4)} / ${item.metadata.unit.replace(/^USD\//, '')}`, - }); - } + const imageUnitPrice = Number(metadataText(metadata, 'image_unit_price')); + const imageUnit = metadataText(metadata, 'image_unit') || 'USD/image'; + if (Number.isFinite(imageUnitPrice) && imageUnitPrice > 0) { + unitPrices.push({ + label: '图片单价', + value: `$${imageUnitPrice.toFixed(4)} / ${imageUnit.replace(/^USD\//, '')}`, + }); } if (unitPrices.length === 0) { if (record.input_price && record.input_price > 0) diff --git a/web/src/components/UsageMetricDetail.tsx b/web/src/components/UsageMetricDetail.tsx index c7ca619..a262086 100644 --- a/web/src/components/UsageMetricDetail.tsx +++ b/web/src/components/UsageMetricDetail.tsx @@ -1,14 +1,6 @@ import type { CSSProperties, ReactNode } from 'react'; import type { UsageRecordSurfaceProps } from '@doudou-start/airgate-theme/plugin'; -interface UsageMetric { - key?: string; - label?: string; - kind?: string; - unit?: string; - value?: number; -} - interface UsageRecordLike { model?: string; input_tokens?: number; @@ -107,35 +99,17 @@ const valueStyle: CSSProperties = { whiteSpace: 'nowrap', }; -function contextArray(context: UsageRecordSurfaceProps['context'], camel: string, snake: string): T[] { - const value = context?.[camel] ?? context?.[snake]; - return Array.isArray(value) ? value as T[] : []; -} - function recordFromContext(context: UsageRecordSurfaceProps['context']): UsageRecordLike { const record = context?.record; return record && typeof record === 'object' ? record as UsageRecordLike : {}; } -function norm(value?: string) { - return (value || '').trim().toLowerCase().replace(/[\s-]+/g, '_'); -} - -function numberValue(value: unknown) { - return typeof value === 'number' && Number.isFinite(value) ? value : 0; -} - function formatNumber(value: number) { return Number.isInteger(value) ? value.toLocaleString() : value.toLocaleString(undefined, { maximumFractionDigits: 4 }); } -function metricValue(metrics: UsageMetric[], keys: string[]) { - const metric = metrics.find((item) => keys.includes(norm(item.key || item.kind || item.label))); - return metric ? numberValue(metric.value) : 0; -} - function Row({ label, tone, value }: { label: ReactNode; tone?: string; value: ReactNode }) { return (
@@ -158,15 +132,14 @@ function outputTokenValue(reasoningTokens: number, outputTokens: number) { export function UsageMetricDetail({ context }: UsageRecordSurfaceProps) { const record = recordFromContext(context); - const metrics = contextArray(context, 'usageMetrics', 'usage_metrics'); - const inputTokens = metricValue(metrics, ['input_tokens', 'input_token', 'prompt_tokens', 'prompt_token']) || record.input_tokens || 0; - const outputTokens = metricValue(metrics, ['output_tokens', 'output_token', 'completion_tokens', 'completion_token']) || record.output_tokens || 0; - const cacheReadTokens = metricValue(metrics, ['cached_input_tokens', 'cached_input_token', 'cache_read_tokens', 'cache_read_token']) || record.cached_input_tokens || 0; - const cacheCreationTokens = metricValue(metrics, ['cache_creation_tokens', 'cache_creation_input_tokens', 'cache_creation_token']) || record.cache_creation_tokens || 0; - const cacheCreation5mTokens = metricValue(metrics, ['cache_creation_5m_tokens', 'cache_creation_5m_input_tokens']) || record.cache_creation_5m_tokens || 0; - const cacheCreation1hTokens = metricValue(metrics, ['cache_creation_1h_tokens', 'cache_creation_1h_input_tokens']) || record.cache_creation_1h_tokens || 0; - const reasoningTokens = metricValue(metrics, ['reasoning_output_tokens', 'reasoning_tokens', 'reasoning_token']) || record.reasoning_output_tokens || 0; - const totalTokens = metricValue(metrics, ['total_tokens', 'total_token']) || inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens; + const inputTokens = record.input_tokens || 0; + const outputTokens = record.output_tokens || 0; + const cacheReadTokens = record.cached_input_tokens || 0; + const cacheCreationTokens = record.cache_creation_tokens || 0; + const cacheCreation5mTokens = record.cache_creation_5m_tokens || 0; + const cacheCreation1hTokens = record.cache_creation_1h_tokens || 0; + const reasoningTokens = record.reasoning_output_tokens || 0; + const totalTokens = inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens; return (
From 35770dd8cb9eb036d4679616cc77bb1f89fa3234 Mon Sep 17 00:00:00 2001 From: quantal Date: Thu, 28 May 2026 00:39:51 +0800 Subject: [PATCH 07/20] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E7=94=A8?= =?UTF-8?q?=E9=87=8F=E6=A8=A1=E5=9E=8B=EF=BC=8C=E4=BC=98=E5=8C=96=E8=AE=A1?= =?UTF-8?q?=E8=B4=B9=E9=80=BB=E8=BE=91=E5=92=8C=E5=85=83=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- backend/internal/gateway/models.go | 239 ++++------------------- backend/internal/gateway/stream.go | 1 - backend/internal/gateway/stream_test.go | 48 ++--- web/src/components/UsageCostDetail.tsx | 9 +- web/src/components/UsageMetricDetail.tsx | 19 +- 6 files changed, 85 insertions(+), 233 deletions(-) diff --git a/README.md b/README.md index 393568c..34d9a30 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ AirGate Anthropic 是 [airgate-core](https://github.com/DouDOU-start/airgate-cor - **进程内 Token 刷新锁** — 基于 sync.Mutex 的 per-account 并发保护,double-check 防止重复刷新,不可重试错误自动分类 + 60 秒冷却窗口 - **TLS 指纹伪装** — OAuth/session_key 账号使用 uTLS 模拟 Claude CLI 2.x 的 JA3 指纹,API Key 账号使用标准 TLS - **连接池复用** — 标准 TLS 和 uTLS 指纹各自独立连接池,按 proxyURL 分桶缓存 Transport,避免每次请求创建新连接 -- **精确成本计算** — 插件内按 Anthropic 定价填充 `UsageMetric` / `UsageCostDetail`,支持 standard/priority/flex 计费层级 +- **精确成本计算** — 插件内按 Anthropic 定价填充标准 Usage 成本字段,Claude 专属维度写入 metadata - **流式 Usage 提取** — SSE 流中实时提取 input_tokens、output_tokens、cache_read_input_tokens、reasoning_output_tokens,记录 FirstTokenMs - **Console 批量导入** — 支持通过 Session Key 批量一键创建 OAuth/Setup Token 账号 - **完整前端 Widget** — 四种账号类型的表单面板,OAuth 授权引导、Session Key 一键获取、状态展示 diff --git a/backend/internal/gateway/models.go b/backend/internal/gateway/models.go index 8e25876..8fbf5ee 100644 --- a/backend/internal/gateway/models.go +++ b/backend/internal/gateway/models.go @@ -2,8 +2,8 @@ package gateway import ( "encoding/json" - "fmt" "sort" + "strconv" "strings" sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" @@ -55,11 +55,9 @@ const ( usageMetricReasoningOutputTokens = "reasoning_output_tokens" usageMetricTotalTokens = "total_tokens" - usageCostInput = "input_tokens" - usageCostCachedInput = "cached_input_tokens" - usageCostCacheCreation5m = "cache_creation_5m_input_tokens" - usageCostCacheCreation1h = "cache_creation_1h_input_tokens" - usageCostOutput = "output_tokens" + usageMetaCacheCreation5mTokens = "claude.cache_creation_5m_tokens" + usageMetaCacheCreation1hTokens = "claude.cache_creation_1h_tokens" + usageMetaCacheCreation1hPrice = "claude.cache_creation_1h_price" ) // ModelIDOverrides 短名到长名的映射 @@ -188,16 +186,10 @@ func newTokenUsage(modelID string, tokens tokenUsage, firstTokenMs int64) *sdk.U Currency: usageCurrencyUSD, FirstTokenMs: firstTokenMs, } - setUsageModelAttribute(usage, modelID) setUsageTokens(usage, tokens) return usage } -func setUsageModelAttribute(usage *sdk.Usage, modelID string) { - _ = usage - _ = modelID -} - func setUsageTokens(usage *sdk.Usage, tokens tokenUsage) { if usage == nil { return @@ -206,65 +198,9 @@ func setUsageTokens(usage *sdk.Usage, tokens tokenUsage) { usage.OutputTokens = tokens.outputTokens usage.CachedInputTokens = tokens.cachedInputTokens usage.CacheCreationTokens = tokens.cacheCreationTokens - usage.CacheCreation5mTokens = tokens.cacheCreation5mTokens - usage.CacheCreation1hTokens = tokens.cacheCreation1hTokens usage.ReasoningOutputTokens = tokens.reasoningOutputTokens - setUsageMetric(usage, sdk.UsageMetric{ - Key: usageMetricInputTokens, - Label: "输入 Token", - Kind: "token", - Unit: "token", - Value: float64(tokens.inputTokens), - }) - setUsageMetric(usage, sdk.UsageMetric{ - Key: usageMetricCachedInputTokens, - Label: "缓存输入 Token", - Kind: "token", - Unit: "token", - Value: float64(tokens.cachedInputTokens), - }) - setUsageMetric(usage, sdk.UsageMetric{ - Key: usageMetricCacheCreationTokens, - Label: "缓存写入 Token", - Kind: "token", - Unit: "token", - Value: float64(tokens.cacheCreationTokens), - }) - setUsageMetric(usage, sdk.UsageMetric{ - Key: usageMetricCacheCreation5mTokens, - Label: "缓存写入 Token 5m", - Kind: "token", - Unit: "token", - Value: float64(tokens.cacheCreation5mTokens), - }) - setUsageMetric(usage, sdk.UsageMetric{ - Key: usageMetricCacheCreation1hTokens, - Label: "缓存写入 Token 1h", - Kind: "token", - Unit: "token", - Value: float64(tokens.cacheCreation1hTokens), - }) - setUsageMetric(usage, sdk.UsageMetric{ - Key: usageMetricOutputTokens, - Label: "输出 Token", - Kind: "token", - Unit: "token", - Value: float64(tokens.outputTokens), - }) - setUsageMetric(usage, sdk.UsageMetric{ - Key: usageMetricReasoningOutputTokens, - Label: "推理 Token", - Kind: "token", - Unit: "token", - Value: float64(tokens.reasoningOutputTokens), - }) - setUsageMetric(usage, sdk.UsageMetric{ - Key: usageMetricTotalTokens, - Label: "总 Token", - Kind: "token", - Unit: "token", - Value: float64(tokens.inputTokens + tokens.cachedInputTokens + tokens.cacheCreationTokens + tokens.outputTokens), - }) + setUsageMetadataInt(usage, usageMetaCacheCreation5mTokens, tokens.cacheCreation5mTokens) + setUsageMetadataInt(usage, usageMetaCacheCreation1hTokens, tokens.cacheCreation1hTokens) } func usageMetricInt(usage *sdk.Usage, key string) int { @@ -283,9 +219,9 @@ func usageMetricValue(usage *sdk.Usage, key string) float64 { case usageMetricCacheCreationTokens: return float64(usage.CacheCreationTokens) case usageMetricCacheCreation5mTokens: - return float64(usage.CacheCreation5mTokens) + return usageMetadataFloat(usage, usageMetaCacheCreation5mTokens) case usageMetricCacheCreation1hTokens: - return float64(usage.CacheCreation1hTokens) + return usageMetadataFloat(usage, usageMetaCacheCreation1hTokens) case usageMetricOutputTokens: return float64(usage.OutputTokens) case usageMetricReasoningOutputTokens: @@ -296,48 +232,47 @@ func usageMetricValue(usage *sdk.Usage, key string) float64 { return 0 } -func setUsageAttribute(usage *sdk.Usage, attr sdk.UsageAttribute) { - _ = usage - _ = attr +func setUsageMetadata(usage *sdk.Usage, key, value string) { + if usage == nil { + return + } + value = strings.TrimSpace(value) + if value == "" { + return + } + if usage.Metadata == nil { + usage.Metadata = map[string]string{} + } + usage.Metadata[key] = value } -func setUsageMetric(usage *sdk.Usage, metric sdk.UsageMetric) { - for i := range usage.Metrics { - if usage.Metrics[i].Key == metric.Key { - usage.Metrics[i] = metric - return - } +func setUsageMetadataInt(usage *sdk.Usage, key string, value int) { + if value <= 0 { + return } - usage.Metrics = append(usage.Metrics, metric) + setUsageMetadata(usage, key, strconv.Itoa(value)) } -func setUsageCostDetail(usage *sdk.Usage, detail sdk.UsageCostDetail) { - if detail.AccountCost <= 0 { - removeUsageCostDetail(usage, detail.Key) +func setUsageMetadataFloat(usage *sdk.Usage, key string, value float64) { + if value <= 0 { return } - for i := range usage.CostDetails { - if usage.CostDetails[i].Key == detail.Key { - usage.CostDetails[i] = detail - recomputeUsageAccountCost(usage) - return - } - } - usage.CostDetails = append(usage.CostDetails, detail) - recomputeUsageAccountCost(usage) + setUsageMetadata(usage, key, strconv.FormatFloat(value, 'f', -1, 64)) } -func removeUsageCostDetail(usage *sdk.Usage, key string) { +func usageMetadataFloat(usage *sdk.Usage, key string) float64 { if usage == nil { - return + return 0 } - for i := range usage.CostDetails { - if usage.CostDetails[i].Key == key { - usage.CostDetails = append(usage.CostDetails[:i], usage.CostDetails[i+1:]...) - recomputeUsageAccountCost(usage) - return - } + raw := strings.TrimSpace(usage.Metadata[key]) + if raw == "" { + return 0 + } + value, err := strconv.ParseFloat(raw, 64) + if err != nil { + return 0 } + return value } func recomputeUsageAccountCost(usage *sdk.Usage) { @@ -358,17 +293,6 @@ func tokenCost(tokens int, pricePerMillion float64) float64 { return float64(tokens) * pricePerMillion / 1_000_000 } -func priceMetadata(price float64, pricingModel string) map[string]string { - metadata := map[string]string{ - "unit_price": fmt.Sprintf("%.10g", price), - "unit": "USD/1M tokens", - } - if pricingModel != "" { - metadata["pricing_model"] = pricingModel - } - return metadata -} - // fillUsageCost 根据 Usage 中的 token 计量和插件私有价格表填充费用。 // 未知 model 只在计费规格上兜底,Usage.Model 仍保持上游实际返回值。 func fillUsageCost(usage *sdk.Usage) { @@ -376,7 +300,7 @@ func fillUsageCost(usage *sdk.Usage) { return } - pricingModel, spec := LookupModelSpec(usage.Model) + _, spec := LookupModelSpec(usage.Model) inputTokens := usageMetricInt(usage, usageMetricInputTokens) outputTokens := usageMetricInt(usage, usageMetricOutputTokens) cachedInputTokens := usageMetricInt(usage, usageMetricCachedInputTokens) @@ -398,99 +322,14 @@ func fillUsageCost(usage *sdk.Usage) { usage.InputPrice = spec.InputPrice usage.CachedInputPrice = spec.CachedPrice usage.CacheCreationPrice = spec.CacheCreationPrice - usage.CacheCreation1hPrice = spec.CacheCreation1hPrice usage.OutputPrice = spec.OutputPrice usage.InputCost = inputCost usage.CachedInputCost = cachedCost usage.CacheCreationCost = cacheCreation5mCost + cacheCreation1hCost usage.OutputCost = outputCost + setUsageMetadataFloat(usage, usageMetaCacheCreation1hPrice, spec.CacheCreation1hPrice) recomputeUsageAccountCost(usage) - setUsageMetric(usage, sdk.UsageMetric{ - Key: usageMetricInputTokens, - Label: "输入 Token", - Kind: "token", - Unit: "token", - Value: float64(inputTokens), - AccountCost: inputCost, - Currency: usageCurrencyUSD, - Metadata: priceMetadata(spec.InputPrice, pricingModel), - }) - setUsageMetric(usage, sdk.UsageMetric{ - Key: usageMetricCachedInputTokens, - Label: "缓存输入 Token", - Kind: "token", - Unit: "token", - Value: float64(cachedInputTokens), - AccountCost: cachedCost, - Currency: usageCurrencyUSD, - Metadata: priceMetadata(spec.CachedPrice, pricingModel), - }) - setUsageMetric(usage, sdk.UsageMetric{ - Key: usageMetricCacheCreation5mTokens, - Label: "缓存写入 Token 5m", - Kind: "token", - Unit: "token", - Value: float64(billableCacheCreation5mTokens), - AccountCost: cacheCreation5mCost, - Currency: usageCurrencyUSD, - Metadata: priceMetadata(spec.CacheCreationPrice, pricingModel), - }) - setUsageMetric(usage, sdk.UsageMetric{ - Key: usageMetricCacheCreation1hTokens, - Label: "缓存写入 Token 1h", - Kind: "token", - Unit: "token", - Value: float64(cacheCreation1hTokens), - AccountCost: cacheCreation1hCost, - Currency: usageCurrencyUSD, - Metadata: priceMetadata(spec.CacheCreation1hPrice, pricingModel), - }) - setUsageMetric(usage, sdk.UsageMetric{ - Key: usageMetricOutputTokens, - Label: "输出 Token", - Kind: "token", - Unit: "token", - Value: float64(outputTokens), - AccountCost: outputCost, - Currency: usageCurrencyUSD, - Metadata: priceMetadata(spec.OutputPrice, pricingModel), - }) - setUsageCostDetail(usage, sdk.UsageCostDetail{ - Key: usageCostInput, - Label: "输入 Token", - AccountCost: inputCost, - Currency: usageCurrencyUSD, - Metadata: priceMetadata(spec.InputPrice, pricingModel), - }) - setUsageCostDetail(usage, sdk.UsageCostDetail{ - Key: usageCostCachedInput, - Label: "缓存输入 Token", - AccountCost: cachedCost, - Currency: usageCurrencyUSD, - Metadata: priceMetadata(spec.CachedPrice, pricingModel), - }) - setUsageCostDetail(usage, sdk.UsageCostDetail{ - Key: usageCostCacheCreation5m, - Label: "缓存写入 Token 5m", - AccountCost: cacheCreation5mCost, - Currency: usageCurrencyUSD, - Metadata: priceMetadata(spec.CacheCreationPrice, pricingModel), - }) - setUsageCostDetail(usage, sdk.UsageCostDetail{ - Key: usageCostCacheCreation1h, - Label: "缓存写入 Token 1h", - AccountCost: cacheCreation1hCost, - Currency: usageCurrencyUSD, - Metadata: priceMetadata(spec.CacheCreation1hPrice, pricingModel), - }) - setUsageCostDetail(usage, sdk.UsageCostDetail{ - Key: usageCostOutput, - Label: "输出 Token", - AccountCost: outputCost, - Currency: usageCurrencyUSD, - Metadata: priceMetadata(spec.OutputPrice, pricingModel), - }) } // claudeModelListEntry Anthropic /v1/models 接口返回的单个模型 diff --git a/backend/internal/gateway/stream.go b/backend/internal/gateway/stream.go index baaf715..3951400 100644 --- a/backend/internal/gateway/stream.go +++ b/backend/internal/gateway/stream.go @@ -118,7 +118,6 @@ func extractAnthropicUsage(data string, eventType string, usage *sdk.Usage, toke switch eventType { case "message_start": usage.Model = gjson.Get(data, "message.model").String() - setUsageModelAttribute(usage, usage.Model) if applyAnthropicUsageNode(gjson.Get(data, "message.usage"), tokens, true) { setUsageTokens(usage, *tokens) } diff --git a/backend/internal/gateway/stream_test.go b/backend/internal/gateway/stream_test.go index 7d755e7..22cea4a 100644 --- a/backend/internal/gateway/stream_test.go +++ b/backend/internal/gateway/stream_test.go @@ -22,12 +22,12 @@ func TestExtractAnthropicUsage_MessageStartFallsBackToCachedTokens(t *testing.T) if usage.Model != "claude-opus-4-7" { t.Fatalf("model = %q, want claude-opus-4-7", usage.Model) } - requireUsageMetric(t, usage, usageMetricInputTokens, 12) - requireUsageMetric(t, usage, usageMetricCachedInputTokens, 9) - requireUsageMetric(t, usage, usageMetricCacheCreationTokens, 7) - requireUsageMetric(t, usage, usageMetricCacheCreation5mTokens, 3) - requireUsageMetric(t, usage, usageMetricCacheCreation1hTokens, 4) - requireUsageMetric(t, usage, usageMetricTotalTokens, 28) + requireUsageValue(t, usage, usageMetricInputTokens, 12) + requireUsageValue(t, usage, usageMetricCachedInputTokens, 9) + requireUsageValue(t, usage, usageMetricCacheCreationTokens, 7) + requireUsageValue(t, usage, usageMetricCacheCreation5mTokens, 3) + requireUsageValue(t, usage, usageMetricCacheCreation1hTokens, 4) + requireUsageValue(t, usage, usageMetricTotalTokens, 28) } func TestExtractAnthropicUsage_MessageDeltaKeepsStartValues(t *testing.T) { @@ -40,14 +40,14 @@ func TestExtractAnthropicUsage_MessageDeltaKeepsStartValues(t *testing.T) { delta := `{"type":"message_delta","usage":{"input_tokens":0,"output_tokens":1046,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cached_tokens":11,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"reasoning_output_tokens":128}}` extractAnthropicUsage(delta, "message_delta", usage, &tokens) - requireUsageMetric(t, usage, usageMetricInputTokens, 10) - requireUsageMetric(t, usage, usageMetricCachedInputTokens, 6) - requireUsageMetric(t, usage, usageMetricCacheCreationTokens, 7) - requireUsageMetric(t, usage, usageMetricCacheCreation5mTokens, 3) - requireUsageMetric(t, usage, usageMetricCacheCreation1hTokens, 4) - requireUsageMetric(t, usage, usageMetricOutputTokens, 1046) - requireUsageMetric(t, usage, usageMetricReasoningOutputTokens, 128) - requireUsageMetric(t, usage, usageMetricTotalTokens, 1069) + requireUsageValue(t, usage, usageMetricInputTokens, 10) + requireUsageValue(t, usage, usageMetricCachedInputTokens, 6) + requireUsageValue(t, usage, usageMetricCacheCreationTokens, 7) + requireUsageValue(t, usage, usageMetricCacheCreation5mTokens, 3) + requireUsageValue(t, usage, usageMetricCacheCreation1hTokens, 4) + requireUsageValue(t, usage, usageMetricOutputTokens, 1046) + requireUsageValue(t, usage, usageMetricReasoningOutputTokens, 128) + requireUsageValue(t, usage, usageMetricTotalTokens, 1069) } func TestExtractAnthropicUsage_MessageDeltaFallsBackToCachedTokens(t *testing.T) { @@ -60,9 +60,9 @@ func TestExtractAnthropicUsage_MessageDeltaFallsBackToCachedTokens(t *testing.T) delta := `{"type":"message_delta","usage":{"output_tokens":42,"cache_read_input_tokens":0,"cached_tokens":11}}` extractAnthropicUsage(delta, "message_delta", usage, &tokens) - requireUsageMetric(t, usage, usageMetricCachedInputTokens, 11) - requireUsageMetric(t, usage, usageMetricOutputTokens, 42) - requireUsageMetric(t, usage, usageMetricTotalTokens, 70) + requireUsageValue(t, usage, usageMetricCachedInputTokens, 11) + requireUsageValue(t, usage, usageMetricOutputTokens, 42) + requireUsageValue(t, usage, usageMetricTotalTokens, 70) } func TestHandleNonStreamResponseParsesAnthropicUsageFallbacks(t *testing.T) { @@ -92,18 +92,18 @@ func TestHandleNonStreamResponseParsesAnthropicUsageFallbacks(t *testing.T) { if usage.Model != "claude-opus-4-7" { t.Fatalf("model = %q, want claude-opus-4-7", usage.Model) } - requireUsageMetric(t, usage, usageMetricInputTokens, 123) - requireUsageMetric(t, usage, usageMetricCachedInputTokens, 7) - requireUsageMetric(t, usage, usageMetricCacheCreationTokens, 7) - requireUsageMetric(t, usage, usageMetricCacheCreation5mTokens, 5) - requireUsageMetric(t, usage, usageMetricCacheCreation1hTokens, 2) - requireUsageMetric(t, usage, usageMetricOutputTokens, 456) + requireUsageValue(t, usage, usageMetricInputTokens, 123) + requireUsageValue(t, usage, usageMetricCachedInputTokens, 7) + requireUsageValue(t, usage, usageMetricCacheCreationTokens, 7) + requireUsageValue(t, usage, usageMetricCacheCreation5mTokens, 5) + requireUsageValue(t, usage, usageMetricCacheCreation1hTokens, 2) + requireUsageValue(t, usage, usageMetricOutputTokens, 456) if diff := usage.AccountCost - 0.01206975; diff < -1e-9 || diff > 1e-9 { t.Fatalf("account cost = %.12f, want %.12f", usage.AccountCost, 0.01206975) } } -func requireUsageMetric(t *testing.T, usage *sdk.Usage, key string, want int) { +func requireUsageValue(t *testing.T, usage *sdk.Usage, key string, want int) { t.Helper() if got := usageMetricInt(usage, key); got != want { t.Fatalf("%s = %d, want %d", key, got, want) diff --git a/web/src/components/UsageCostDetail.tsx b/web/src/components/UsageCostDetail.tsx index 4232572..7960dd9 100644 --- a/web/src/components/UsageCostDetail.tsx +++ b/web/src/components/UsageCostDetail.tsx @@ -24,6 +24,7 @@ interface UsageRecordLike { service_tier?: string; input_price?: number; output_price?: number; + cache_creation_price?: number; usage_metadata?: Record; } @@ -104,7 +105,7 @@ function recordFromContext(context: UsageRecordSurfaceProps['context']): UsageRe } function metadataFromContext(context: UsageRecordSurfaceProps['context'], record: UsageRecordLike): Record { - const fromContext = context?.usageMetadata ?? context?.usage_metadata; + const fromContext = context?.usage_metadata; if (fromContext && typeof fromContext === 'object' && !Array.isArray(fromContext)) { return fromContext as Record; } @@ -157,8 +158,8 @@ export function UsageCostDetail({ context }: UsageRecordSurfaceProps) { const rows = fallbackDetails(record); const unitPrices: { label: string; value: string }[] = []; - const imageUnitPrice = Number(metadataText(metadata, 'image_unit_price')); - const imageUnit = metadataText(metadata, 'image_unit') || 'USD/image'; + const imageUnitPrice = Number(metadataText(metadata, 'openai.image.unit_price')); + const imageUnit = metadataText(metadata, 'openai.image.unit') || 'USD/image'; if (Number.isFinite(imageUnitPrice) && imageUnitPrice > 0) { unitPrices.push({ label: '图片单价', @@ -170,6 +171,8 @@ export function UsageCostDetail({ context }: UsageRecordSurfaceProps) { unitPrices.push({ label: '输入单价', value: `$${record.input_price.toFixed(4)} / 1M Token` }); if (record.output_price && record.output_price > 0) unitPrices.push({ label: '输出单价', value: `$${record.output_price.toFixed(4)} / 1M Token` }); + if (record.cache_creation_price && record.cache_creation_price > 0) + unitPrices.push({ label: '缓存写入单价', value: `$${record.cache_creation_price.toFixed(4)} / 1M Token` }); } const hasRateInfo = !!record.service_tier diff --git a/web/src/components/UsageMetricDetail.tsx b/web/src/components/UsageMetricDetail.tsx index a262086..f65f822 100644 --- a/web/src/components/UsageMetricDetail.tsx +++ b/web/src/components/UsageMetricDetail.tsx @@ -7,9 +7,8 @@ interface UsageRecordLike { output_tokens?: number; cached_input_tokens?: number; cache_creation_tokens?: number; - cache_creation_5m_tokens?: number; - cache_creation_1h_tokens?: number; reasoning_output_tokens?: number; + usage_metadata?: Record; } const panelStyle: CSSProperties = { @@ -104,6 +103,17 @@ function recordFromContext(context: UsageRecordSurfaceProps['context']): UsageRe return record && typeof record === 'object' ? record as UsageRecordLike : {}; } +function metadataFromContext(context: UsageRecordSurfaceProps['context'], record: UsageRecordLike): Record { + const direct = context?.usage_metadata; + if (direct && typeof direct === 'object') return direct as Record; + return record.usage_metadata ?? {}; +} + +function metadataNumber(metadata: Record, key: string) { + const value = Number(metadata[key]); + return Number.isFinite(value) ? value : 0; +} + function formatNumber(value: number) { return Number.isInteger(value) ? value.toLocaleString() @@ -132,12 +142,13 @@ function outputTokenValue(reasoningTokens: number, outputTokens: number) { export function UsageMetricDetail({ context }: UsageRecordSurfaceProps) { const record = recordFromContext(context); + const metadata = metadataFromContext(context, record); const inputTokens = record.input_tokens || 0; const outputTokens = record.output_tokens || 0; const cacheReadTokens = record.cached_input_tokens || 0; const cacheCreationTokens = record.cache_creation_tokens || 0; - const cacheCreation5mTokens = record.cache_creation_5m_tokens || 0; - const cacheCreation1hTokens = record.cache_creation_1h_tokens || 0; + const cacheCreation5mTokens = metadataNumber(metadata, 'claude.cache_creation_5m_tokens'); + const cacheCreation1hTokens = metadataNumber(metadata, 'claude.cache_creation_1h_tokens'); const reasoningTokens = record.reasoning_output_tokens || 0; const totalTokens = inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens; From 817fabe816c1e1695d71c3f0c395f546521b20e8 Mon Sep 17 00:00:00 2001 From: quantal Date: Thu, 28 May 2026 00:57:07 +0800 Subject: [PATCH 08/20] =?UTF-8?q?feat:=20=E9=87=8D=E6=9E=84=E7=94=A8?= =?UTF-8?q?=E9=87=8F=E6=A8=A1=E5=9E=8B=EF=BC=8C=E7=AE=80=E5=8C=96=E8=AE=A1?= =?UTF-8?q?=E8=B4=B9=E9=80=BB=E8=BE=91=E5=92=8C=E6=B5=8B=E8=AF=95=E7=94=A8?= =?UTF-8?q?=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/gateway/models.go | 53 +++++++------------------ backend/internal/gateway/stream_test.go | 52 ++++++++++++------------ 2 files changed, 40 insertions(+), 65 deletions(-) diff --git a/backend/internal/gateway/models.go b/backend/internal/gateway/models.go index 8fbf5ee..68498db 100644 --- a/backend/internal/gateway/models.go +++ b/backend/internal/gateway/models.go @@ -44,17 +44,6 @@ var modelRegistry = map[string]Spec{ const ( usageCurrencyUSD = "USD" - usageAttrModel = "model" - - usageMetricInputTokens = "input_tokens" - usageMetricCachedInputTokens = "cached_input_tokens" - usageMetricCacheCreationTokens = "cache_creation_input_tokens" - usageMetricCacheCreation5mTokens = "cache_creation_5m_input_tokens" - usageMetricCacheCreation1hTokens = "cache_creation_1h_input_tokens" - usageMetricOutputTokens = "output_tokens" - usageMetricReasoningOutputTokens = "reasoning_output_tokens" - usageMetricTotalTokens = "total_tokens" - usageMetaCacheCreation5mTokens = "claude.cache_creation_5m_tokens" usageMetaCacheCreation1hTokens = "claude.cache_creation_1h_tokens" usageMetaCacheCreation1hPrice = "claude.cache_creation_1h_price" @@ -203,33 +192,19 @@ func setUsageTokens(usage *sdk.Usage, tokens tokenUsage) { setUsageMetadataInt(usage, usageMetaCacheCreation1hTokens, tokens.cacheCreation1hTokens) } -func usageMetricInt(usage *sdk.Usage, key string) int { - return int(usageMetricValue(usage, key)) +func usageCacheCreation5mTokens(usage *sdk.Usage) int { + return int(usageMetadataFloat(usage, usageMetaCacheCreation5mTokens)) } -func usageMetricValue(usage *sdk.Usage, key string) float64 { +func usageCacheCreation1hTokens(usage *sdk.Usage) int { + return int(usageMetadataFloat(usage, usageMetaCacheCreation1hTokens)) +} + +func usageTotalTokens(usage *sdk.Usage) int { if usage == nil { return 0 } - switch key { - case usageMetricInputTokens: - return float64(usage.InputTokens) - case usageMetricCachedInputTokens: - return float64(usage.CachedInputTokens) - case usageMetricCacheCreationTokens: - return float64(usage.CacheCreationTokens) - case usageMetricCacheCreation5mTokens: - return usageMetadataFloat(usage, usageMetaCacheCreation5mTokens) - case usageMetricCacheCreation1hTokens: - return usageMetadataFloat(usage, usageMetaCacheCreation1hTokens) - case usageMetricOutputTokens: - return float64(usage.OutputTokens) - case usageMetricReasoningOutputTokens: - return float64(usage.ReasoningOutputTokens) - case usageMetricTotalTokens: - return float64(usage.InputTokens + usage.CachedInputTokens + usage.CacheCreationTokens + usage.OutputTokens) - } - return 0 + return usage.InputTokens + usage.CachedInputTokens + usage.CacheCreationTokens + usage.OutputTokens } func setUsageMetadata(usage *sdk.Usage, key, value string) { @@ -301,12 +276,12 @@ func fillUsageCost(usage *sdk.Usage) { } _, spec := LookupModelSpec(usage.Model) - inputTokens := usageMetricInt(usage, usageMetricInputTokens) - outputTokens := usageMetricInt(usage, usageMetricOutputTokens) - cachedInputTokens := usageMetricInt(usage, usageMetricCachedInputTokens) - cacheCreationTokens := usageMetricInt(usage, usageMetricCacheCreationTokens) - cacheCreation5mTokens := usageMetricInt(usage, usageMetricCacheCreation5mTokens) - cacheCreation1hTokens := usageMetricInt(usage, usageMetricCacheCreation1hTokens) + inputTokens := usage.InputTokens + outputTokens := usage.OutputTokens + cachedInputTokens := usage.CachedInputTokens + cacheCreationTokens := usage.CacheCreationTokens + cacheCreation5mTokens := usageCacheCreation5mTokens(usage) + cacheCreation1hTokens := usageCacheCreation1hTokens(usage) genericCacheCreationTokens := cacheCreationTokens - cacheCreation5mTokens - cacheCreation1hTokens if genericCacheCreationTokens < 0 { diff --git a/backend/internal/gateway/stream_test.go b/backend/internal/gateway/stream_test.go index 22cea4a..071603b 100644 --- a/backend/internal/gateway/stream_test.go +++ b/backend/internal/gateway/stream_test.go @@ -22,12 +22,12 @@ func TestExtractAnthropicUsage_MessageStartFallsBackToCachedTokens(t *testing.T) if usage.Model != "claude-opus-4-7" { t.Fatalf("model = %q, want claude-opus-4-7", usage.Model) } - requireUsageValue(t, usage, usageMetricInputTokens, 12) - requireUsageValue(t, usage, usageMetricCachedInputTokens, 9) - requireUsageValue(t, usage, usageMetricCacheCreationTokens, 7) - requireUsageValue(t, usage, usageMetricCacheCreation5mTokens, 3) - requireUsageValue(t, usage, usageMetricCacheCreation1hTokens, 4) - requireUsageValue(t, usage, usageMetricTotalTokens, 28) + requireUsageInt(t, "input tokens", usage.InputTokens, 12) + requireUsageInt(t, "cached input tokens", usage.CachedInputTokens, 9) + requireUsageInt(t, "cache creation tokens", usage.CacheCreationTokens, 7) + requireUsageInt(t, "cache creation 5m tokens", usageCacheCreation5mTokens(usage), 3) + requireUsageInt(t, "cache creation 1h tokens", usageCacheCreation1hTokens(usage), 4) + requireUsageInt(t, "total tokens", usageTotalTokens(usage), 28) } func TestExtractAnthropicUsage_MessageDeltaKeepsStartValues(t *testing.T) { @@ -40,14 +40,14 @@ func TestExtractAnthropicUsage_MessageDeltaKeepsStartValues(t *testing.T) { delta := `{"type":"message_delta","usage":{"input_tokens":0,"output_tokens":1046,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cached_tokens":11,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"reasoning_output_tokens":128}}` extractAnthropicUsage(delta, "message_delta", usage, &tokens) - requireUsageValue(t, usage, usageMetricInputTokens, 10) - requireUsageValue(t, usage, usageMetricCachedInputTokens, 6) - requireUsageValue(t, usage, usageMetricCacheCreationTokens, 7) - requireUsageValue(t, usage, usageMetricCacheCreation5mTokens, 3) - requireUsageValue(t, usage, usageMetricCacheCreation1hTokens, 4) - requireUsageValue(t, usage, usageMetricOutputTokens, 1046) - requireUsageValue(t, usage, usageMetricReasoningOutputTokens, 128) - requireUsageValue(t, usage, usageMetricTotalTokens, 1069) + requireUsageInt(t, "input tokens", usage.InputTokens, 10) + requireUsageInt(t, "cached input tokens", usage.CachedInputTokens, 6) + requireUsageInt(t, "cache creation tokens", usage.CacheCreationTokens, 7) + requireUsageInt(t, "cache creation 5m tokens", usageCacheCreation5mTokens(usage), 3) + requireUsageInt(t, "cache creation 1h tokens", usageCacheCreation1hTokens(usage), 4) + requireUsageInt(t, "output tokens", usage.OutputTokens, 1046) + requireUsageInt(t, "reasoning output tokens", usage.ReasoningOutputTokens, 128) + requireUsageInt(t, "total tokens", usageTotalTokens(usage), 1069) } func TestExtractAnthropicUsage_MessageDeltaFallsBackToCachedTokens(t *testing.T) { @@ -60,9 +60,9 @@ func TestExtractAnthropicUsage_MessageDeltaFallsBackToCachedTokens(t *testing.T) delta := `{"type":"message_delta","usage":{"output_tokens":42,"cache_read_input_tokens":0,"cached_tokens":11}}` extractAnthropicUsage(delta, "message_delta", usage, &tokens) - requireUsageValue(t, usage, usageMetricCachedInputTokens, 11) - requireUsageValue(t, usage, usageMetricOutputTokens, 42) - requireUsageValue(t, usage, usageMetricTotalTokens, 70) + requireUsageInt(t, "cached input tokens", usage.CachedInputTokens, 11) + requireUsageInt(t, "output tokens", usage.OutputTokens, 42) + requireUsageInt(t, "total tokens", usageTotalTokens(usage), 70) } func TestHandleNonStreamResponseParsesAnthropicUsageFallbacks(t *testing.T) { @@ -92,20 +92,20 @@ func TestHandleNonStreamResponseParsesAnthropicUsageFallbacks(t *testing.T) { if usage.Model != "claude-opus-4-7" { t.Fatalf("model = %q, want claude-opus-4-7", usage.Model) } - requireUsageValue(t, usage, usageMetricInputTokens, 123) - requireUsageValue(t, usage, usageMetricCachedInputTokens, 7) - requireUsageValue(t, usage, usageMetricCacheCreationTokens, 7) - requireUsageValue(t, usage, usageMetricCacheCreation5mTokens, 5) - requireUsageValue(t, usage, usageMetricCacheCreation1hTokens, 2) - requireUsageValue(t, usage, usageMetricOutputTokens, 456) + requireUsageInt(t, "input tokens", usage.InputTokens, 123) + requireUsageInt(t, "cached input tokens", usage.CachedInputTokens, 7) + requireUsageInt(t, "cache creation tokens", usage.CacheCreationTokens, 7) + requireUsageInt(t, "cache creation 5m tokens", usageCacheCreation5mTokens(usage), 5) + requireUsageInt(t, "cache creation 1h tokens", usageCacheCreation1hTokens(usage), 2) + requireUsageInt(t, "output tokens", usage.OutputTokens, 456) if diff := usage.AccountCost - 0.01206975; diff < -1e-9 || diff > 1e-9 { t.Fatalf("account cost = %.12f, want %.12f", usage.AccountCost, 0.01206975) } } -func requireUsageValue(t *testing.T, usage *sdk.Usage, key string, want int) { +func requireUsageInt(t *testing.T, label string, got, want int) { t.Helper() - if got := usageMetricInt(usage, key); got != want { - t.Fatalf("%s = %d, want %d", key, got, want) + if got != want { + t.Fatalf("%s = %d, want %d", label, got, want) } } From 30c97f2669640d976542eae47e096a22361cd9be Mon Sep 17 00:00:00 2001 From: quantal Date: Fri, 29 May 2026 20:39:14 +0800 Subject: [PATCH 09/20] =?UTF-8?q?feat:=20=E7=8B=AC=E7=AB=8B=E5=8F=91?= =?UTF-8?q?=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 4 ++-- Makefile | 2 +- README.md | 14 +++++++------- backend/cmd/devserver/main.go | 4 ++-- backend/cmd/fp/main.go | 2 +- backend/cmd/genmanifest/main.go | 4 ++-- backend/go.mod | 4 ++-- backend/go.sum | 2 -- backend/internal/gateway/errors.go | 2 +- backend/internal/gateway/forward.go | 2 +- backend/internal/gateway/gateway.go | 2 +- backend/internal/gateway/headers.go | 2 +- backend/internal/gateway/metadata.go | 2 +- backend/internal/gateway/models.go | 2 +- backend/internal/gateway/oauth_handler.go | 4 ++-- backend/internal/gateway/outcome.go | 2 +- backend/internal/gateway/sidecar.go | 2 +- backend/internal/gateway/stream.go | 2 +- backend/internal/gateway/stream_test.go | 2 +- backend/internal/gateway/token_manager.go | 2 +- backend/main.go | 4 ++-- web/package.json | 2 +- web/pnpm-lock.yaml | 12 ++++++------ web/src/components/AccountForm.tsx | 4 ++-- web/src/components/AccountIdentity.tsx | 2 +- web/src/components/UsageCostDetail.tsx | 2 +- web/src/components/UsageMetricDetail.tsx | 2 +- web/src/components/UsageModelMeta.tsx | 2 +- web/src/components/UsageWindow.tsx | 2 +- web/src/index.ts | 2 +- 31 files changed, 48 insertions(+), 50 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c8e599..1deffa9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: branches: [master] env: - GOPRIVATE: github.com/DouDOU-start/airgate-sdk + GOPRIVATE: github.com/DevilGenius/airgate-sdk jobs: ci: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1822940..a9a3334 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ permissions: contents: write env: - GOPRIVATE: github.com/DouDOU-start/airgate-sdk + GOPRIVATE: github.com/DevilGenius/airgate-sdk jobs: build: @@ -63,7 +63,7 @@ jobs: echo "Injecting PluginVersion=${VERSION}" mkdir -p bin cd backend && go build -buildvcs=false -trimpath \ - -ldflags "-X 'github.com/DouDOU-start/airgate-claude/backend/internal/gateway.PluginVersion=${VERSION}'" \ + -ldflags "-X 'github.com/DevilGenius/airgate-claude/backend/internal/gateway.PluginVersion=${VERSION}'" \ -o ../bin/gateway-claude-${{ matrix.goos }}-${{ matrix.goarch }} . - name: Generate SHA256 diff --git a/Makefile b/Makefile index 8593691..6acde81 100644 --- a/Makefile +++ b/Makefile @@ -74,7 +74,7 @@ lint: ## 代码检查(需要安装 golangci-lint) fmt: ## 格式化代码 @cd backend && \ if command -v goimports > /dev/null 2>&1; then \ - goimports -w -local github.com/DouDOU-start .; \ + goimports -w -local github.com/DevilGenius .; \ else \ $(GO) fmt ./...; \ fi diff --git a/README.md b/README.md index 34d9a30..4987839 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ --- -AirGate Anthropic 是 [airgate-core](https://github.com/DouDOU-start/airgate-core) 的 Claude 网关插件,基于 [airgate-sdk](https://github.com/DouDOU-start/airgate-sdk) 构建。它以 gRPC 子进程方式运行,负责将 Claude Messages API 请求转发到 Anthropic 上游,支持多种认证方式和连接池复用。 +AirGate Anthropic 是 [airgate-core](https://github.com/DevilGenius/airgate-core) 的 Claude 网关插件,基于 [airgate-sdk](https://github.com/DevilGenius/airgate-sdk) 构建。它以 gRPC 子进程方式运行,负责将 Claude Messages API 请求转发到 Anthropic 上游,支持多种认证方式和连接池复用。 ## 核心特性 @@ -155,9 +155,9 @@ AirGate Anthropic 是 [airgate-core](https://github.com/DouDOU-start/airgate-cor 需要 Go 1.25+、Node 22+,以及兄弟目录 `airgate-sdk` 与 `airgate-core`: ```bash -git clone https://github.com/DouDOU-start/airgate-sdk.git -git clone https://github.com/DouDOU-start/airgate-core.git -git clone https://github.com/DouDOU-start/airgate-claude.git +git clone https://github.com/DevilGenius/airgate-sdk.git +git clone https://github.com/DevilGenius/airgate-core.git +git clone https://github.com/DevilGenius/airgate-claude.git cd airgate-claude ``` @@ -221,6 +221,6 @@ airgate-claude/ ## 相关项目 -- [airgate-core](https://github.com/DouDOU-start/airgate-core) — 核心网关(账号调度、计费、管理后台) -- [airgate-sdk](https://github.com/DouDOU-start/airgate-sdk) — 插件 SDK -- [airgate-openai](https://github.com/DouDOU-start/airgate-openai) — OpenAI 网关插件(参考实现) +- [airgate-core](https://github.com/DevilGenius/airgate-core) — 核心网关(账号调度、计费、管理后台) +- [airgate-sdk](https://github.com/DevilGenius/airgate-sdk) — 插件 SDK +- [airgate-openai](https://github.com/DevilGenius/airgate-openai) — OpenAI 网关插件(参考实现) diff --git a/backend/cmd/devserver/main.go b/backend/cmd/devserver/main.go index d51dc67..7dc2d4f 100644 --- a/backend/cmd/devserver/main.go +++ b/backend/cmd/devserver/main.go @@ -6,9 +6,9 @@ import ( "log" "net/http" - "github.com/DouDOU-start/airgate-sdk/devkit/devserver" + "github.com/DevilGenius/airgate-sdk/devkit/devserver" - "github.com/DouDOU-start/airgate-claude/backend/internal/gateway" + "github.com/DevilGenius/airgate-claude/backend/internal/gateway" ) func main() { diff --git a/backend/cmd/fp/main.go b/backend/cmd/fp/main.go index 45c440f..4d130e9 100644 --- a/backend/cmd/fp/main.go +++ b/backend/cmd/fp/main.go @@ -34,7 +34,7 @@ import ( utls "github.com/refraction-networking/utls" - "github.com/DouDOU-start/airgate-claude/backend/internal/gateway" + "github.com/DevilGenius/airgate-claude/backend/internal/gateway" ) // snapshot 即将写盘的指纹快照 diff --git a/backend/cmd/genmanifest/main.go b/backend/cmd/genmanifest/main.go index ef4baba..1916aea 100644 --- a/backend/cmd/genmanifest/main.go +++ b/backend/cmd/genmanifest/main.go @@ -9,8 +9,8 @@ import ( "gopkg.in/yaml.v3" - "github.com/DouDOU-start/airgate-claude/backend/internal/gateway" - sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" + "github.com/DevilGenius/airgate-claude/backend/internal/gateway" + sdk "github.com/DevilGenius/airgate-sdk/sdkgo" ) const generatedComment = "# 本文件由 backend/cmd/genmanifest 自动生成,请勿手工修改。\n\n" diff --git a/backend/go.mod b/backend/go.mod index 0190710..d09810a 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,9 +1,9 @@ -module github.com/DouDOU-start/airgate-claude/backend +module github.com/DevilGenius/airgate-claude/backend go 1.25.7 require ( - github.com/DouDOU-start/airgate-sdk v0.2.1 + github.com/DevilGenius/airgate-sdk v0.2.1 github.com/imroc/req/v3 v3.57.0 github.com/refraction-networking/utls v1.8.2 github.com/tidwall/gjson v1.18.0 diff --git a/backend/go.sum b/backend/go.sum index fdb2f4f..4e7b9f5 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,5 +1,3 @@ -github.com/DouDOU-start/airgate-sdk v0.2.1 h1:MylrFMifIJy5mT1wSFdXcSvAXjIcB6itp4dw8pP4pes= -github.com/DouDOU-start/airgate-sdk v0.2.1/go.mod h1:784vC4lIfCnUdOroWuvn55Dv/b9TucVMpFwuu40gc0Q= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= diff --git a/backend/internal/gateway/errors.go b/backend/internal/gateway/errors.go index c39468a..415fea7 100644 --- a/backend/internal/gateway/errors.go +++ b/backend/internal/gateway/errors.go @@ -8,7 +8,7 @@ import ( "strings" "time" - sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" + sdk "github.com/DevilGenius/airgate-sdk/sdkgo" "github.com/tidwall/gjson" ) diff --git a/backend/internal/gateway/forward.go b/backend/internal/gateway/forward.go index 27ae65c..3d1fade 100644 --- a/backend/internal/gateway/forward.go +++ b/backend/internal/gateway/forward.go @@ -14,7 +14,7 @@ import ( "github.com/tidwall/gjson" "github.com/tidwall/sjson" - sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" + sdk "github.com/DevilGenius/airgate-sdk/sdkgo" ) const ( diff --git a/backend/internal/gateway/gateway.go b/backend/internal/gateway/gateway.go index b7e34cd..734422c 100644 --- a/backend/internal/gateway/gateway.go +++ b/backend/internal/gateway/gateway.go @@ -13,7 +13,7 @@ import ( "strings" "time" - sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" + sdk "github.com/DevilGenius/airgate-sdk/sdkgo" ) // AnthropicGateway Claude 网关插件 diff --git a/backend/internal/gateway/headers.go b/backend/internal/gateway/headers.go index eb3fd6a..ad106fa 100644 --- a/backend/internal/gateway/headers.go +++ b/backend/internal/gateway/headers.go @@ -5,7 +5,7 @@ import ( "net/http" "strings" - sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" + sdk "github.com/DevilGenius/airgate-sdk/sdkgo" ) // ────────────────────────────────────────────────────── diff --git a/backend/internal/gateway/metadata.go b/backend/internal/gateway/metadata.go index 6ae3d42..b0cb548 100644 --- a/backend/internal/gateway/metadata.go +++ b/backend/internal/gateway/metadata.go @@ -1,6 +1,6 @@ package gateway -import sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" +import sdk "github.com/DevilGenius/airgate-sdk/sdkgo" //go:generate go run ../../cmd/genmanifest diff --git a/backend/internal/gateway/models.go b/backend/internal/gateway/models.go index 68498db..e86c8d3 100644 --- a/backend/internal/gateway/models.go +++ b/backend/internal/gateway/models.go @@ -6,7 +6,7 @@ import ( "strconv" "strings" - sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" + sdk "github.com/DevilGenius/airgate-sdk/sdkgo" ) // Spec 单个模型的完整元数据 diff --git a/backend/internal/gateway/oauth_handler.go b/backend/internal/gateway/oauth_handler.go index 63784cd..c9dcdc0 100644 --- a/backend/internal/gateway/oauth_handler.go +++ b/backend/internal/gateway/oauth_handler.go @@ -10,8 +10,8 @@ import ( "strings" "time" - "github.com/DouDOU-start/airgate-sdk/devkit/devserver" - sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" + "github.com/DevilGenius/airgate-sdk/devkit/devserver" + sdk "github.com/DevilGenius/airgate-sdk/sdkgo" ) // OAuthDevHandler devserver 的 OAuth HTTP handler diff --git a/backend/internal/gateway/outcome.go b/backend/internal/gateway/outcome.go index c27bceb..ed0ad56 100644 --- a/backend/internal/gateway/outcome.go +++ b/backend/internal/gateway/outcome.go @@ -5,7 +5,7 @@ import ( "net/http" "time" - sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" + sdk "github.com/DevilGenius/airgate-sdk/sdkgo" ) // 构造 ForwardOutcome 的 helper,让各转发路径不重复写 struct literal。 diff --git a/backend/internal/gateway/sidecar.go b/backend/internal/gateway/sidecar.go index 5d4a2cf..cf78edc 100644 --- a/backend/internal/gateway/sidecar.go +++ b/backend/internal/gateway/sidecar.go @@ -10,7 +10,7 @@ import ( "sync" "time" - sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" + sdk "github.com/DevilGenius/airgate-sdk/sdkgo" ) // ────────────────────────────────────────────────────── diff --git a/backend/internal/gateway/stream.go b/backend/internal/gateway/stream.go index 3951400..e3b8761 100644 --- a/backend/internal/gateway/stream.go +++ b/backend/internal/gateway/stream.go @@ -11,7 +11,7 @@ import ( "github.com/tidwall/gjson" - sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" + sdk "github.com/DevilGenius/airgate-sdk/sdkgo" ) // SSE 流式响应透传 + usage 提取。调用者保证 resp.StatusCode 是 2xx(4xx/5xx 由 handleErrorResponse 处理)。 diff --git a/backend/internal/gateway/stream_test.go b/backend/internal/gateway/stream_test.go index 071603b..3383a5c 100644 --- a/backend/internal/gateway/stream_test.go +++ b/backend/internal/gateway/stream_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" + sdk "github.com/DevilGenius/airgate-sdk/sdkgo" ) func TestExtractAnthropicUsage_MessageStartFallsBackToCachedTokens(t *testing.T) { diff --git a/backend/internal/gateway/token_manager.go b/backend/internal/gateway/token_manager.go index 668cd77..d95ed4b 100644 --- a/backend/internal/gateway/token_manager.go +++ b/backend/internal/gateway/token_manager.go @@ -8,7 +8,7 @@ import ( "sync" "time" - sdk "github.com/DouDOU-start/airgate-sdk/sdkgo" + sdk "github.com/DevilGenius/airgate-sdk/sdkgo" ) // ────────────────────────────────────────────────────── diff --git a/backend/main.go b/backend/main.go index caf2bba..56d8fcb 100644 --- a/backend/main.go +++ b/backend/main.go @@ -1,8 +1,8 @@ package main import ( - "github.com/DouDOU-start/airgate-claude/backend/internal/gateway" - sdkgrpc "github.com/DouDOU-start/airgate-sdk/runtimego/grpc" + "github.com/DevilGenius/airgate-claude/backend/internal/gateway" + sdkgrpc "github.com/DevilGenius/airgate-sdk/runtimego/grpc" ) func main() { diff --git a/web/package.json b/web/package.json index fe4c223..c4db823 100644 --- a/web/package.json +++ b/web/package.json @@ -9,7 +9,7 @@ "dev": "vite build --watch" }, "dependencies": { - "@doudou-start/airgate-theme": "https://github.com/DouDOU-start/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz", + "@devilgenius/airgate-theme": "https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 54a920c..e65e101 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -8,9 +8,9 @@ importers: .: dependencies: - '@doudou-start/airgate-theme': - specifier: https://github.com/DouDOU-start/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz - version: https://github.com/DouDOU-start/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz(react@19.2.5) + '@devilgenius/airgate-theme': + specifier: https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz + version: https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz(react@19.2.5) react: specifier: ^19.0.0 version: 19.2.5 @@ -119,8 +119,8 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@doudou-start/airgate-theme@https://github.com/DouDOU-start/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz': - resolution: {tarball: https://github.com/DouDOU-start/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz} + '@devilgenius/airgate-theme@https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz': + resolution: {tarball: https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz} version: 0.2.1 peerDependencies: react: ^19.0.0 @@ -764,7 +764,7 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@doudou-start/airgate-theme@https://github.com/DouDOU-start/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz(react@19.2.5)': + '@devilgenius/airgate-theme@https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz(react@19.2.5)': dependencies: react: 19.2.5 diff --git a/web/src/components/AccountForm.tsx b/web/src/components/AccountForm.tsx index bf6b9b2..7763b7b 100644 --- a/web/src/components/AccountForm.tsx +++ b/web/src/components/AccountForm.tsx @@ -1,10 +1,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { cssVar } from '@doudou-start/airgate-theme'; +import { cssVar } from '@devilgenius/airgate-theme'; import type { AccountFormProps, PluginBatchAccountInput, PluginOAuthBatchExchangeResult, -} from '@doudou-start/airgate-theme/plugin'; +} from '@devilgenius/airgate-theme/plugin'; type BatchExchangeResult = PluginOAuthBatchExchangeResult; type BatchAccountInput = PluginBatchAccountInput; diff --git a/web/src/components/AccountIdentity.tsx b/web/src/components/AccountIdentity.tsx index 657ae47..3ebf3ed 100644 --- a/web/src/components/AccountIdentity.tsx +++ b/web/src/components/AccountIdentity.tsx @@ -1,5 +1,5 @@ import type { CSSProperties } from 'react'; -import type { AccountSurfaceProps } from '@doudou-start/airgate-theme/plugin'; +import type { AccountSurfaceProps } from '@devilgenius/airgate-theme/plugin'; type AccountLike = { type?: string; diff --git a/web/src/components/UsageCostDetail.tsx b/web/src/components/UsageCostDetail.tsx index 7960dd9..6794bae 100644 --- a/web/src/components/UsageCostDetail.tsx +++ b/web/src/components/UsageCostDetail.tsx @@ -1,5 +1,5 @@ import type { CSSProperties, ReactNode } from 'react'; -import type { UsageRecordSurfaceProps } from '@doudou-start/airgate-theme/plugin'; +import type { UsageRecordSurfaceProps } from '@devilgenius/airgate-theme/plugin'; interface UsageCostDetailItem { key?: string; diff --git a/web/src/components/UsageMetricDetail.tsx b/web/src/components/UsageMetricDetail.tsx index f65f822..ea0b94d 100644 --- a/web/src/components/UsageMetricDetail.tsx +++ b/web/src/components/UsageMetricDetail.tsx @@ -1,5 +1,5 @@ import type { CSSProperties, ReactNode } from 'react'; -import type { UsageRecordSurfaceProps } from '@doudou-start/airgate-theme/plugin'; +import type { UsageRecordSurfaceProps } from '@devilgenius/airgate-theme/plugin'; interface UsageRecordLike { model?: string; diff --git a/web/src/components/UsageModelMeta.tsx b/web/src/components/UsageModelMeta.tsx index 443c4bc..e22b201 100644 --- a/web/src/components/UsageModelMeta.tsx +++ b/web/src/components/UsageModelMeta.tsx @@ -1,4 +1,4 @@ -import type { UsageRecordSurfaceProps } from '@doudou-start/airgate-theme/plugin'; +import type { UsageRecordSurfaceProps } from '@devilgenius/airgate-theme/plugin'; import type { CSSProperties } from 'react'; type UsageContext = { diff --git a/web/src/components/UsageWindow.tsx b/web/src/components/UsageWindow.tsx index 618e514..4088e14 100644 --- a/web/src/components/UsageWindow.tsx +++ b/web/src/components/UsageWindow.tsx @@ -1,5 +1,5 @@ import { useEffect, useState, type CSSProperties } from 'react'; -import type { AccountSurfaceProps } from '@doudou-start/airgate-theme/plugin'; +import type { AccountSurfaceProps } from '@devilgenius/airgate-theme/plugin'; interface UsageWindowItem { key?: string; diff --git a/web/src/index.ts b/web/src/index.ts index 8adede5..e9c8a61 100644 --- a/web/src/index.ts +++ b/web/src/index.ts @@ -1,5 +1,5 @@ import { AccountForm } from './components/AccountForm'; -import type { PluginFrontendModule } from '@doudou-start/airgate-theme/plugin'; +import type { PluginFrontendModule } from '@devilgenius/airgate-theme/plugin'; import { ClaudeIcon } from './components/ClaudeIcon'; import { AccountIdentity } from './components/AccountIdentity'; import { UsageCostDetail } from './components/UsageCostDetail'; From e8adf5e06dde1d62b94b13675a07fe4799c51460 Mon Sep 17 00:00:00 2001 From: quantal Date: Fri, 29 May 2026 21:34:44 +0800 Subject: [PATCH 10/20] chore: refresh sdk v0.2.1 checksums --- backend/go.sum | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/go.sum b/backend/go.sum index 4e7b9f5..b4105b7 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,3 +1,5 @@ +github.com/DevilGenius/airgate-sdk v0.2.1 h1:043M0Qom56yg99/noLyMKx/Tbvj4MFFyCG6Q9kzy8qM= +github.com/DevilGenius/airgate-sdk v0.2.1/go.mod h1:G+TeuDfouZz0Z/pxXeCYyto0c8RbVhBZcRyislIzcKc= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= From b31f21376e781f0aa2fc0dd6845cd1a9f6db7100 Mon Sep 17 00:00:00 2001 From: quantal Date: Fri, 29 May 2026 21:52:03 +0800 Subject: [PATCH 11/20] chore: require go 1.26.3 --- .github/workflows/ci.yml | 1 + README.md | 4 ++-- backend/go.mod | 2 +- backend/go.sum | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1deffa9..ee1656e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,7 @@ name: CI on: + workflow_dispatch: push: branches: [master] pull_request: diff --git a/README.md b/README.md index 799fa75..60bf4b1 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ AirGate Anthropic 是 [airgate-core](https://github.com/DevilGenius/airgate-core | 层 | 技术 | |---|---| -| 后端 | Go 1.25 · gRPC · gjson · uTLS(JA3 指纹) | +| 后端 | Go 1.26.3 · gRPC · gjson · uTLS(JA3 指纹) | | 前端 | React 19 · Vite · TypeScript(账号表单 Widget) | | 插件协议 | hashicorp/go-plugin (gRPC) | | 上游协议 | Anthropic Messages API · Anthropic OAuth | @@ -156,7 +156,7 @@ AirGate Anthropic 是 [airgate-core](https://github.com/DevilGenius/airgate-core ### 方式 2:源码运行(开发) -需要 Go 1.25+、Node 22+,以及兄弟目录 `airgate-sdk` 与 `airgate-core`: +需要 Go 1.26.3+、Node 22+,以及兄弟目录 `airgate-sdk` 与 `airgate-core`: ```bash git clone https://github.com/DevilGenius/airgate-sdk.git diff --git a/backend/go.mod b/backend/go.mod index d09810a..7ea35a8 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,6 +1,6 @@ module github.com/DevilGenius/airgate-claude/backend -go 1.25.7 +go 1.26.3 require ( github.com/DevilGenius/airgate-sdk v0.2.1 diff --git a/backend/go.sum b/backend/go.sum index b4105b7..5e38272 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,5 +1,5 @@ -github.com/DevilGenius/airgate-sdk v0.2.1 h1:043M0Qom56yg99/noLyMKx/Tbvj4MFFyCG6Q9kzy8qM= -github.com/DevilGenius/airgate-sdk v0.2.1/go.mod h1:G+TeuDfouZz0Z/pxXeCYyto0c8RbVhBZcRyislIzcKc= +github.com/DevilGenius/airgate-sdk v0.2.1 h1:83zhKTHJU8G+oFBxHScAMe4UwIfuXxNmaeZ8p1ytY+0= +github.com/DevilGenius/airgate-sdk v0.2.1/go.mod h1:EnjPPVBiONFv0gcQqiBPqs86HRhcVKICdFaB03yJcI8= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= From ba6d6f5e1b316e65b17255e75dae38d0e5f64e64 Mon Sep 17 00:00:00 2001 From: quantal Date: Fri, 29 May 2026 21:55:27 +0800 Subject: [PATCH 12/20] ci: verify master push trigger From 78866581a22d8bb425a3f0594a6facb64ed04aa4 Mon Sep 17 00:00:00 2001 From: quantal Date: Fri, 29 May 2026 21:57:54 +0800 Subject: [PATCH 13/20] ci: recheck master push trigger From 7ddd168cb08d02f6152451ff40c9f2c2710792b6 Mon Sep 17 00:00:00 2001 From: quantal Date: Fri, 29 May 2026 21:59:50 +0800 Subject: [PATCH 14/20] ci: refresh workflow trigger registration --- .github/workflows/{ci.yml => continuous-integration.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{ci.yml => continuous-integration.yml} (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/continuous-integration.yml similarity index 100% rename from .github/workflows/ci.yml rename to .github/workflows/continuous-integration.yml From 6f4b726ef02116d0a9c6a380da1f999ded412b47 Mon Sep 17 00:00:00 2001 From: quantal Date: Fri, 29 May 2026 22:01:22 +0800 Subject: [PATCH 15/20] ci: verify registered workflow push From a31844b68928a24c3080bb52d586338af1b59e01 Mon Sep 17 00:00:00 2001 From: quantal Date: Fri, 29 May 2026 22:13:04 +0800 Subject: [PATCH 16/20] ci: update actions to node 24 runtime --- .github/workflows/continuous-integration.yml | 8 ++++---- .github/workflows/release.yml | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index ee1656e..b369c6a 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -15,13 +15,13 @@ jobs: name: Lint & Test & Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v6 with: version: 11.1.1 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 22 cache: pnpm @@ -30,7 +30,7 @@ jobs: - name: Build frontend run: cd web && pnpm install --frozen-lockfile && pnpm run build - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: backend/go.mod cache-dependency-path: backend/go.sum diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a9a3334..98d5b89 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,13 +27,13 @@ jobs: - goos: darwin goarch: arm64 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@v6 with: version: 11.1.1 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 22 cache: pnpm @@ -47,7 +47,7 @@ jobs: rm -rf backend/internal/gateway/webdist cp -r web/dist backend/internal/gateway/webdist - - uses: actions/setup-go@v5 + - uses: actions/setup-go@v6 with: go-version-file: backend/go.mod cache-dependency-path: backend/go.sum @@ -85,7 +85,7 @@ jobs: needs: build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Download all artifacts uses: actions/download-artifact@v4 @@ -97,7 +97,7 @@ jobs: run: ls -la dist/ - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: tag_name: ${{ github.ref_name }} name: ${{ github.ref_name }} From daeff34c62f1d7c21b24a1ec42a4fb2a59803886 Mon Sep 17 00:00:00 2001 From: quantal Date: Sat, 30 May 2026 06:53:13 +0800 Subject: [PATCH 17/20] =?UTF-8?q?fix:=20=E7=A8=B3=E5=AE=9A=E6=80=A7?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/go.sum | 2 +- backend/internal/gateway/errors.go | 7 + backend/internal/gateway/forward.go | 5 +- backend/internal/gateway/gateway.go | 47 ++++-- backend/internal/gateway/models.go | 13 +- backend/internal/gateway/oauth.go | 154 ++++++++++++++++-- backend/internal/gateway/oauth_handler.go | 3 +- backend/internal/gateway/session.go | 61 +++++-- backend/internal/gateway/sidecar.go | 106 +++++++++++- backend/internal/gateway/tlsfingerprint.go | 50 +++--- backend/internal/gateway/token_manager.go | 72 ++++++++- backend/internal/gateway/transport_pool.go | 180 ++++++++++++++++++--- 12 files changed, 591 insertions(+), 109 deletions(-) diff --git a/backend/go.sum b/backend/go.sum index 5e38272..b04c0c4 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,4 +1,4 @@ -github.com/DevilGenius/airgate-sdk v0.2.1 h1:83zhKTHJU8G+oFBxHScAMe4UwIfuXxNmaeZ8p1ytY+0= +github.com/DevilGenius/airgate-sdk v0.2.1 h1:6qIJX3NNK0yOaj+OkF3HxZCLNY2k4OfEc6vOGti8+io= github.com/DevilGenius/airgate-sdk v0.2.1/go.mod h1:EnjPPVBiONFv0gcQqiBPqs86HRhcVKICdFaB03yJcI8= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= diff --git a/backend/internal/gateway/errors.go b/backend/internal/gateway/errors.go index 415fea7..6eddf2a 100644 --- a/backend/internal/gateway/errors.go +++ b/backend/internal/gateway/errors.go @@ -2,6 +2,7 @@ package gateway import ( "encoding/json" + "io" "net/http" "regexp" "strconv" @@ -12,6 +13,8 @@ import ( "github.com/tidwall/gjson" ) +const maxErrorResponseBodyBytes = 1 << 20 + // classifyHTTPFailure 根据 HTTP 状态码 + 错误文本归一化为 OutcomeKind。 // // 429 → AccountRateLimited @@ -95,6 +98,10 @@ func truncate(s string, maxLen int) string { return s[:maxLen] + "..." } +func readLimitedErrorBody(r io.Reader) ([]byte, error) { + return io.ReadAll(io.LimitReader(r, maxErrorResponseBodyBytes+1)) +} + func jsonError(msg string) []byte { b, _ := json.Marshal(map[string]string{"error": msg}) return b diff --git a/backend/internal/gateway/forward.go b/backend/internal/gateway/forward.go index 3d1fade..03db5b9 100644 --- a/backend/internal/gateway/forward.go +++ b/backend/internal/gateway/forward.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "fmt" - "io" "net/http" "net/url" "strings" @@ -402,7 +401,7 @@ func preprocessOAuthBody(body []byte, account *sdk.Account) []byte { } // 2. 注入 metadata.user_id(如果缺失) - // CLI >= 2.1.78 使用 JSON 格式;使用 sticky session 30 min 内复用同一 session_id + // CLI >= 2.1.78 使用 JSON 格式;使用 sticky session 3600s 内复用同一 session_id if !gjson.GetBytes(body, "metadata.user_id").Exists() { accountUUID := account.Credentials["account_uuid"] if accountUUID == "" { @@ -493,7 +492,7 @@ func normalizeRequestBody(body []byte) []byte { // unwritten 让 Core canFailover 不被短路;Core 会 failover + 触发状态机 // - UpstreamTransient(5xx):不写 w,让 Core failover func handleErrorResponse(resp *http.Response, _ http.ResponseWriter, start time.Time) sdk.ForwardOutcome { - respBody, _ := io.ReadAll(resp.Body) + respBody, _ := readLimitedErrorBody(resp.Body) msg := extractErrorMessage(respBody) if msg == "" { msg = truncate(string(respBody), 200) diff --git a/backend/internal/gateway/gateway.go b/backend/internal/gateway/gateway.go index 734422c..91cf1a3 100644 --- a/backend/internal/gateway/gateway.go +++ b/backend/internal/gateway/gateway.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "fmt" - "io" "log/slog" "net/http" "net/url" @@ -18,13 +17,14 @@ import ( // AnthropicGateway Claude 网关插件 type AnthropicGateway struct { - logger *slog.Logger - ctx sdk.PluginContext - tokenMgr *tokenManager - stdPool *StandardTransportPool - fpPool *FingerprintTransportPool - registry *accountRegistry - sidecar *sidecarRunner + logger *slog.Logger + ctx sdk.PluginContext + tokenMgr *tokenManager + stdPool *StandardTransportPool + fpPool *FingerprintTransportPool + oauthReqPool *OAuthReqClientPool + registry *accountRegistry + sidecar *sidecarRunner } func (g *AnthropicGateway) Info() sdk.PluginInfo { @@ -43,6 +43,7 @@ func (g *AnthropicGateway) Init(ctx sdk.PluginContext) error { // 初始化连接池 g.stdPool = NewStandardTransportPool() g.fpPool = NewFingerprintTransportPool() + g.oauthReqPool = NewOAuthReqClientPool() // 账号注册表 + sidecar 运行器 g.registry = newAccountRegistry() @@ -74,6 +75,9 @@ func (g *AnthropicGateway) Stop(_ context.Context) error { if g.fpPool != nil { g.fpPool.Close() } + if g.oauthReqPool != nil { + g.oauthReqPool.Close() + } return nil } @@ -138,8 +142,12 @@ func (g *AnthropicGateway) ValidateAccount(ctx context.Context, credentials map[ client := &http.Client{Timeout: 30 * time.Second} if proxyURL := credentials["proxy_url"]; proxyURL != "" { - if u, err := url.Parse(proxyURL); err == nil { - client.Transport = &http.Transport{Proxy: http.ProxyURL(u)} + if g != nil && g.stdPool != nil { + client.Transport = g.stdPool.Get(proxyURL) + } else if u, err := url.Parse(proxyURL); err == nil { + transport := &http.Transport{Proxy: http.ProxyURL(u)} + client.Transport = transport + defer transport.CloseIdleConnections() } } resp, err := client.Do(req) @@ -169,7 +177,7 @@ func (g *AnthropicGateway) QueryQuota(ctx context.Context, credentials map[strin } // 调用 Anthropic usage API - usageResp, err := g.fetchUsage(ctx, accessToken, credentials["proxy_url"]) + usageResp, err := g.fetchUsage(ctx, 0, accessToken, credentials["proxy_url"], credentials["tls_profile"]) if err != nil { return nil, fmt.Errorf("查询用量失败: %w", err) } @@ -334,7 +342,7 @@ func (g *AnthropicGateway) HandleRequest(ctx context.Context, _, path, _ string, continue } - usageResp, err := g.fetchUsage(ctx, accessToken, a.Credentials["proxy_url"]) + usageResp, err := g.fetchUsage(ctx, a.ID, accessToken, a.Credentials["proxy_url"], a.Credentials["tls_profile"]) if err != nil { resp.Errors = append(resp.Errors, accountUsageError{ ID: a.ID, @@ -492,7 +500,7 @@ type UsageResponse struct { } // fetchUsage 从 Anthropic API 获取 OAuth 账号用量 -func (g *AnthropicGateway) fetchUsage(ctx context.Context, accessToken, proxyURL string) (*UsageResponse, error) { +func (g *AnthropicGateway) fetchUsage(ctx context.Context, accountID int64, accessToken, proxyURL, tlsProfile string) (*UsageResponse, error) { httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, usageAPIURL, nil) if err != nil { return nil, err @@ -509,7 +517,7 @@ func (g *AnthropicGateway) fetchUsage(ctx context.Context, accessToken, proxyURL // 使用 TLS 指纹客户端(api.anthropic.com 用 Claude CLI 指纹) client := &http.Client{ Timeout: 30 * time.Second, - Transport: buildFingerprintTransport(proxyURL), + Transport: g.usageTransport(accountID, proxyURL, tlsProfile), } resp, err := client.Do(httpReq) @@ -519,7 +527,7 @@ func (g *AnthropicGateway) fetchUsage(ctx context.Context, accessToken, proxyURL defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) + body, _ := readLimitedErrorBody(resp.Body) return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, truncate(string(body), 200)) } @@ -530,6 +538,13 @@ func (g *AnthropicGateway) fetchUsage(ctx context.Context, accessToken, proxyURL return &usageResp, nil } +func (g *AnthropicGateway) usageTransport(accountID int64, proxyURL, tlsProfile string) http.RoundTripper { + if g != nil && g.fpPool != nil { + return g.fpPool.Get(accountID, proxyURL, tlsProfile) + } + return buildFingerprintTransportWithProfile(proxyURL, tlsProfile) +} + // ────────────────────────────────────────────────────── // 工具函数 // ────────────────────────────────────────────────────── @@ -603,7 +618,7 @@ func (g *AnthropicGateway) forwardCountTokens(ctx context.Context, req *sdk.Forw } defer func() { _ = resp.Body.Close() }() - respBody, err := io.ReadAll(resp.Body) + respBody, err := readLimitedErrorBody(resp.Body) if err != nil { reason := fmt.Sprintf("读取上游响应失败: %v", err) logger.Warn("upstream_response_read_failed", diff --git a/backend/internal/gateway/models.go b/backend/internal/gateway/models.go index e85cc33..707d032 100644 --- a/backend/internal/gateway/models.go +++ b/backend/internal/gateway/models.go @@ -5,6 +5,7 @@ import ( "sort" "strconv" "strings" + "sync" sdk "github.com/DevilGenius/airgate-sdk/sdkgo" ) @@ -42,6 +43,8 @@ var modelRegistry = map[string]Spec{ "claude-haiku-4-5-20251001": {"Claude Haiku 4.5", 200000, 64000, 1.0, 0.1, 1.25, 2.0, 5.0}, } +var modelRegistryMu sync.RWMutex + const ( usageCurrencyUSD = "USD" @@ -76,8 +79,9 @@ func NormalizeModelID(id string) string { // AllModelSpecs 返回所有注册模型的 SDK ModelInfo 列表 func AllModelSpecs() []sdk.ModelInfo { - models := make([]sdk.ModelInfo, 0, len(modelRegistry)) - for _, item := range AllPricingSpecs() { + pricing := AllPricingSpecs() + models := make([]sdk.ModelInfo, 0, len(pricing)) + for _, item := range pricing { models = append(models, specToModelInfo(item.ID, item.Spec)) } return models @@ -91,6 +95,8 @@ type NamedSpec struct { // AllPricingSpecs 返回带价格的插件私有模型规格,用于 manifest 生成和计费。 func AllPricingSpecs() []NamedSpec { + modelRegistryMu.RLock() + defer modelRegistryMu.RUnlock() items := make([]NamedSpec, 0, len(modelRegistry)) for id, spec := range modelRegistry { items = append(items, NamedSpec{ID: id, Spec: spec}) @@ -115,6 +121,9 @@ var fallbackSpec = Spec{ // LookupModelSpec 查找模型计费规格,未知模型返回兜底规格。 func LookupModelSpec(modelID string) (string, Spec) { + modelRegistryMu.RLock() + defer modelRegistryMu.RUnlock() + // 精确匹配 if spec, ok := modelRegistry[modelID]; ok { return modelID, spec diff --git a/backend/internal/gateway/oauth.go b/backend/internal/gateway/oauth.go index d0865bd..1bb6fab 100644 --- a/backend/internal/gateway/oauth.go +++ b/backend/internal/gateway/oauth.go @@ -8,7 +8,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "io" "net/http" "net/url" "strings" @@ -27,8 +26,8 @@ const ( OAuthAuthorizeURL = "https://claude.ai/oauth/authorize" OAuthTokenURL = "https://platform.claude.com/v1/oauth/token" OAuthRedirectURI = "https://platform.claude.com/oauth/code/callback" - OAuthScopeBrowser = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload" - OAuthScopeAPI = "user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload" + OAuthScopeBrowser = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload" + OAuthScopeAPI = "user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload" // Session Key 通过 claude.ai 获取 org 和 authorization code 的端点 claudeAIBaseURL = "https://claude.ai" @@ -36,8 +35,11 @@ const ( // PKCE 字符集 codeVerifierCharset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" - // OAuth Session TTL - oauthSessionTTL = 30 * time.Minute + // OAuth Session TTL(3600s) + oauthSessionTTL = 3600 * time.Second + + // oauthSessionMaxEntries 是异常流量下的高水位安全阀;正常回收主要靠 TTL。 + oauthSessionMaxEntries = 1000000 ) // ────────────────────────────────────────────────────── @@ -64,17 +66,26 @@ var sessionStore = &oauthSessionStore{ func (s *oauthSessionStore) Set(state string, session *OAuthSession) { s.mu.Lock() defer s.mu.Unlock() + now := time.Now() + if session.CreatedAt.IsZero() { + session.CreatedAt = now + } + s.cleanExpiredLocked(now) + if _, exists := s.sessions[state]; !exists && len(s.sessions) >= oauthSessionMaxEntries { + s.deleteOldestLocked() + } s.sessions[state] = session } func (s *oauthSessionStore) Get(state string) (*OAuthSession, bool) { - s.mu.RLock() - defer s.mu.RUnlock() + s.mu.Lock() + defer s.mu.Unlock() session, ok := s.sessions[state] if !ok { return nil, false } if time.Since(session.CreatedAt) > oauthSessionTTL { + delete(s.sessions, state) return nil, false } return session, true @@ -166,13 +177,31 @@ func (g *AnthropicGateway) HandleOAuthCallback(ctx context.Context, code, state, func (s *oauthSessionStore) CleanExpired() { s.mu.Lock() defer s.mu.Unlock() + s.cleanExpiredLocked(time.Now()) +} + +func (s *oauthSessionStore) cleanExpiredLocked(now time.Time) { for state, session := range s.sessions { - if time.Since(session.CreatedAt) > oauthSessionTTL { + if now.Sub(session.CreatedAt) > oauthSessionTTL { delete(s.sessions, state) } } } +func (s *oauthSessionStore) deleteOldestLocked() { + var oldestState string + var oldestAt time.Time + for state, session := range s.sessions { + if oldestState == "" || session.CreatedAt.Before(oldestAt) { + oldestState = state + oldestAt = session.CreatedAt + } + } + if oldestState != "" { + delete(s.sessions, oldestState) + } +} + // ────────────────────────────────────────────────────── // Session Key → OAuth Token 完整流程 // ────────────────────────────────────────────────────── @@ -208,6 +237,100 @@ func (g *AnthropicGateway) ExchangeSessionKeyForToken(ctx context.Context, sessi return g.exchangeSessionKeyWithScope(ctx, sessionKey, proxyURL, OAuthScopeAPI) } +const ( + oauthReqClientMaxEntries = 100000 + oauthReqClientIdleTTL = 2 * time.Hour + oauthReqClientCleanupInterval = 10 * time.Minute +) + +type oauthReqClientEntry struct { + client *req.Client + lastUsedAt time.Time +} + +// OAuthReqClientPool 缓存 claude.ai 请求使用的 req.Client,按 proxyURL 隔离。 +type OAuthReqClientPool struct { + mu sync.Mutex + clients map[string]*oauthReqClientEntry + lastCleanupTime time.Time +} + +func NewOAuthReqClientPool() *OAuthReqClientPool { + return &OAuthReqClientPool{ + clients: make(map[string]*oauthReqClientEntry), + lastCleanupTime: time.Now(), + } +} + +func (p *OAuthReqClientPool) Get(proxyURL string) *req.Client { + if p == nil { + return buildOAuthReqClient(proxyURL) + } + now := time.Now() + p.mu.Lock() + defer p.mu.Unlock() + if entry, ok := p.clients[proxyURL]; ok { + entry.lastUsedAt = now + p.cleanupIdleLocked(now) + return entry.client + } + p.cleanupIdleLocked(now) + if len(p.clients) >= oauthReqClientMaxEntries { + p.deleteOldestLocked() + } + client := buildOAuthReqClient(proxyURL) + p.clients[proxyURL] = &oauthReqClientEntry{client: client, lastUsedAt: now} + return client +} + +func (p *OAuthReqClientPool) cleanupIdleLocked(now time.Time) { + if now.Sub(p.lastCleanupTime) < oauthReqClientCleanupInterval && len(p.clients) < oauthReqClientMaxEntries { + return + } + for proxyURL, entry := range p.clients { + if now.Sub(entry.lastUsedAt) > oauthReqClientIdleTTL { + closeOAuthReqClient(entry.client) + delete(p.clients, proxyURL) + } + } + p.lastCleanupTime = now +} + +func (p *OAuthReqClientPool) deleteOldestLocked() { + var oldestKey string + var oldestAt time.Time + for proxyURL, entry := range p.clients { + if oldestKey == "" || entry.lastUsedAt.Before(oldestAt) { + oldestKey = proxyURL + oldestAt = entry.lastUsedAt + } + } + if oldestKey != "" { + closeOAuthReqClient(p.clients[oldestKey].client) + delete(p.clients, oldestKey) + } +} + +func (p *OAuthReqClientPool) Close() { + if p == nil { + return + } + p.mu.Lock() + defer p.mu.Unlock() + for _, entry := range p.clients { + closeOAuthReqClient(entry.client) + } + p.clients = make(map[string]*oauthReqClientEntry) + p.lastCleanupTime = time.Now() +} + +func closeOAuthReqClient(client *req.Client) { + if client == nil || client.GetTransport() == nil { + return + } + client.GetTransport().CloseIdleConnections() +} + // buildOAuthReqClient 构建用于 claude.ai 请求的 req 客户端(Chrome TLS 指纹 + 绕过 Cloudflare) func buildOAuthReqClient(proxyURL string) *req.Client { client := req.C(). @@ -222,17 +345,21 @@ func buildOAuthReqClient(proxyURL string) *req.Client { // buildOAuthClient 构建用于 token 交换的标准 HTTP 客户端(platform.claude.com 无 Cloudflare) func (g *AnthropicGateway) buildOAuthClient(proxyURL string) *http.Client { - transport := buildFingerprintTransport(proxyURL) return &http.Client{ Timeout: 60 * time.Second, - Transport: transport, + Transport: g.usageTransport(0, proxyURL, ""), } } // exchangeSessionKeyWithScope 通用的 Session Key → OAuth Token 流程 func (g *AnthropicGateway) exchangeSessionKeyWithScope(ctx context.Context, sessionKey, proxyURL, scope string) (*TokenResponse, error) { // claude.ai 请求使用 Chrome 指纹客户端(绕过 Cloudflare) - reqClient := buildOAuthReqClient(proxyURL) + var reqClient *req.Client + if g != nil && g.oauthReqPool != nil { + reqClient = g.oauthReqPool.Get(proxyURL) + } else { + reqClient = buildOAuthReqClient(proxyURL) + } // Step 1: 获取组织 UUID orgUUID, err := g.getOrganizationUUID(ctx, reqClient, sessionKey) @@ -407,7 +534,7 @@ func (g *AnthropicGateway) exchangeCodeForToken(ctx context.Context, client *htt defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) + body, _ := readLimitedErrorBody(resp.Body) return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, truncate(string(body), 200)) } @@ -448,7 +575,7 @@ func (g *AnthropicGateway) RefreshToken(ctx context.Context, refreshToken, proxy defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) + body, _ := readLimitedErrorBody(resp.Body) return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, truncate(string(body), 200)) } @@ -505,4 +632,3 @@ func base64URLEncode(data []byte) string { encoded := base64.URLEncoding.EncodeToString(data) return strings.TrimRight(encoded, "=") } - diff --git a/backend/internal/gateway/oauth_handler.go b/backend/internal/gateway/oauth_handler.go index c9dcdc0..d5e59e8 100644 --- a/backend/internal/gateway/oauth_handler.go +++ b/backend/internal/gateway/oauth_handler.go @@ -1,7 +1,6 @@ package gateway import ( - "context" "encoding/json" "fmt" "net/http" @@ -273,7 +272,7 @@ func (h *OAuthDevHandler) handleUsage(w http.ResponseWriter, r *http.Request) { return } - usageResp, err := h.Gateway.fetchUsage(context.Background(), accessToken, account.Credentials["proxy_url"]) + usageResp, err := h.Gateway.fetchUsage(r.Context(), id, accessToken, account.Credentials["proxy_url"], account.Credentials["tls_profile"]) if err != nil { http.Error(w, fmt.Sprintf(`{"error":"%s"}`, err.Error()), http.StatusInternalServerError) return diff --git a/backend/internal/gateway/session.go b/backend/internal/gateway/session.go index 3d22038..81d7d44 100644 --- a/backend/internal/gateway/session.go +++ b/backend/internal/gateway/session.go @@ -12,14 +12,20 @@ import ( ) // ────────────────────────────────────────────────────── -// Session 身份:UUIDv4 + 30 min sticky 复用 +// Session 身份:UUIDv4 + 3600s sticky 复用 // ────────────────────────────────────────────────────── // // 真实 Claude CLI 在同一会话内多次请求会复用同一 metadata.user_id; // 每次都重新生成会被 Anthropic 识别为非 CLI 流量。 -// 这里以 accountID + messages 前缀哈希为 key,TTL 30 min。 +// 这里以 accountID + messages 前缀哈希为 key,TTL 3600s。 -const sessionTTL = 30 * time.Minute +const ( + sessionTTL = 3600 * time.Second + + // sessionCacheMaxEntries 只是异常流量下的高水位安全阀;正常回收主要靠 TTL。 + sessionCacheMaxEntries = 1000000 + sessionCleanupInterval = time.Minute +) // newDeviceID 生成 64 字符十六进制 device_id(per-account 确定性) // 真实 CLI 使用 64-char hex device identifier @@ -47,11 +53,15 @@ type sessionEntry struct { // sessionCache accountID+conversation 指纹 → 固定 user_id type sessionCache struct { - mu sync.Mutex - entries map[string]sessionEntry + mu sync.Mutex + entries map[string]sessionEntry + lastCleanupTime time.Time } -var defaultSessionCache = &sessionCache{entries: make(map[string]sessionEntry)} +var defaultSessionCache = &sessionCache{ + entries: make(map[string]sessionEntry), + lastCleanupTime: time.Now(), +} // conversationFingerprint 以 messages 数组的稳定切片计算指纹 // 使用首条消息 + 倒数第二条消息(保持多轮对话一致,对末尾问句不敏感) @@ -86,20 +96,43 @@ func (c *sessionCache) stickyUserID(accountID int64, fingerprint string) string c.mu.Lock() defer c.mu.Unlock() - // 清理过期项(低频访问时顺带 GC) - if len(c.entries) > 1024 { - for k, e := range c.entries { - if now.After(e.expiresAt) { - delete(c.entries, k) - } - } - } + c.cleanupExpiredLocked(now) if e, ok := c.entries[key]; ok && now.Before(e.expiresAt) { return e.userID + } else if ok { + delete(c.entries, key) } + if len(c.entries) >= sessionCacheMaxEntries { + c.deleteOneExpiredOrArbitraryLocked(now) + } id := newUUIDv4() c.entries[key] = sessionEntry{userID: id, expiresAt: now.Add(sessionTTL)} return id } + +func (c *sessionCache) cleanupExpiredLocked(now time.Time) { + if now.Sub(c.lastCleanupTime) < sessionCleanupInterval && len(c.entries) < sessionCacheMaxEntries { + return + } + for k, e := range c.entries { + if now.After(e.expiresAt) { + delete(c.entries, k) + } + } + c.lastCleanupTime = now +} + +func (c *sessionCache) deleteOneExpiredOrArbitraryLocked(now time.Time) { + for k, e := range c.entries { + if now.After(e.expiresAt) { + delete(c.entries, k) + return + } + } + for k := range c.entries { + delete(c.entries, k) + return + } +} diff --git a/backend/internal/gateway/sidecar.go b/backend/internal/gateway/sidecar.go index cf78edc..0c65ac5 100644 --- a/backend/internal/gateway/sidecar.go +++ b/backend/internal/gateway/sidecar.go @@ -29,7 +29,12 @@ const ( countTokensChanBuffer = 128 countTokensRequestTimeout = 20 * time.Second probeRequestTimeout = 15 * time.Second + probeMaxConcurrency = 64 probeModel = "claude-haiku-4-5-20251001" + + accountRegistryMaxEntries = 100000 + accountRegistryIdleTTL = 24 * time.Hour + accountRegistryCleanupInterval = 30 * time.Minute ) // ────────────────────────────────────────────────────── @@ -48,12 +53,16 @@ type accountSnapshot struct { // accountRegistry 记录被 forward 路径见到过的 oauth/session_key 账号 // sidecar 只对 registry 里的账号发起流量,避免并发冲击所有账号 type accountRegistry struct { - mu sync.RWMutex - accounts map[int64]*accountSnapshot + mu sync.RWMutex + accounts map[int64]*accountSnapshot + lastCleanupTime time.Time } func newAccountRegistry() *accountRegistry { - return &accountRegistry{accounts: make(map[int64]*accountSnapshot)} + return &accountRegistry{ + accounts: make(map[int64]*accountSnapshot), + lastCleanupTime: time.Now(), + } } // register 登记/更新账号快照(凭证按值拷贝,避免 race) @@ -75,14 +84,20 @@ func (r *accountRegistry) register(account *sdk.Account) { snap.credentials[k] = v } r.mu.Lock() + r.cleanupLocked(snap.lastSeenAt) + if _, exists := r.accounts[account.ID]; !exists && len(r.accounts) >= accountRegistryMaxEntries { + r.deleteOldestLocked() + } r.accounts[account.ID] = snap r.mu.Unlock() } // snapshot 返回当前所有账号快照的切片拷贝 func (r *accountRegistry) snapshot() []*accountSnapshot { - r.mu.RLock() - defer r.mu.RUnlock() + now := time.Now() + r.mu.Lock() + defer r.mu.Unlock() + r.cleanupLocked(now) out := make([]*accountSnapshot, 0, len(r.accounts)) for _, a := range r.accounts { out = append(out, a) @@ -90,6 +105,34 @@ func (r *accountRegistry) snapshot() []*accountSnapshot { return out } +func (r *accountRegistry) cleanupLocked(now time.Time) { + if now.Sub(r.lastCleanupTime) < accountRegistryCleanupInterval && len(r.accounts) < accountRegistryMaxEntries { + return + } + for accountID, snap := range r.accounts { + if now.Sub(snap.lastSeenAt) > accountRegistryIdleTTL { + delete(r.accounts, accountID) + } + } + r.lastCleanupTime = now +} + +func (r *accountRegistry) deleteOldestLocked() { + var oldestID int64 + var oldestAt time.Time + found := false + for accountID, snap := range r.accounts { + if !found || snap.lastSeenAt.Before(oldestAt) { + oldestID = accountID + oldestAt = snap.lastSeenAt + found = true + } + } + if found { + delete(r.accounts, oldestID) + } +} + // ────────────────────────────────────────────────────── // sidecarRunner // ────────────────────────────────────────────────────── @@ -104,14 +147,21 @@ type countTokensJob struct { type sidecarRunner struct { gateway *AnthropicGateway jobs chan countTokensJob + ctx context.Context cancel context.CancelFunc wg sync.WaitGroup + probes sync.WaitGroup + probeCh chan struct{} + mu sync.Mutex + stopped bool } func newSidecarRunner(g *AnthropicGateway) *sidecarRunner { return &sidecarRunner{ gateway: g, jobs: make(chan countTokensJob, countTokensChanBuffer), + probeCh: make(chan struct{}, probeMaxConcurrency), + stopped: true, } } @@ -119,7 +169,17 @@ func newSidecarRunner(g *AnthropicGateway) *sidecarRunner { // postRefreshProbe 是事件驱动(由 tokenManager 成功刷新后触发),不占用常驻 goroutine func (s *sidecarRunner) start() { ctx, cancel := context.WithCancel(context.Background()) + s.mu.Lock() + if !s.stopped { + s.mu.Unlock() + cancel() + return + } + s.jobs = make(chan countTokensJob, countTokensChanBuffer) + s.ctx = ctx s.cancel = cancel + s.stopped = false + s.mu.Unlock() s.wg.Add(2) go s.usagePollerLoop(ctx) @@ -128,11 +188,19 @@ func (s *sidecarRunner) start() { // stop 停止所有 sidecar func (s *sidecarRunner) stop() { + s.mu.Lock() + if s.stopped { + s.mu.Unlock() + return + } + s.stopped = true if s.cancel != nil { s.cancel() } close(s.jobs) + s.mu.Unlock() s.wg.Wait() + s.probes.Wait() } // scheduleCountTokens 非阻塞投递 count_tokens 任务;队列满则丢弃(sidecar 流量不应反压主链路) @@ -142,11 +210,17 @@ func (s *sidecarRunner) scheduleCountTokens(accountID int64, body []byte) { } bodyCopy := make([]byte, len(body)) copy(bodyCopy, body) + s.mu.Lock() + if s.stopped { + s.mu.Unlock() + return + } select { case s.jobs <- countTokensJob{accountID: accountID, body: bodyCopy}: default: // 队列满,安静丢弃 } + s.mu.Unlock() } // ────────────────────────────────────────────────────── @@ -197,7 +271,7 @@ func (s *sidecarRunner) pollOne(ctx context.Context, snap *accountSnapshot, logg pollCtx, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() - _, err := s.gateway.fetchUsage(pollCtx, token, snap.credentials["proxy_url"]) + _, err := s.gateway.fetchUsage(pollCtx, snap.id, token, snap.credentials["proxy_url"], snap.credentials["tls_profile"]) if err != nil { logger.Warn("sidecar usage poll failed", "account_id", snap.id, "error", err) return @@ -281,8 +355,26 @@ func (s *sidecarRunner) fireRefreshProbe(account *sdk.Account) { snap.credentials[k] = v } + s.mu.Lock() + if s.stopped || s.ctx == nil { + s.mu.Unlock() + return + } + parentCtx := s.ctx + select { + case s.probeCh <- struct{}{}: + default: + s.mu.Unlock() + s.gateway.logger.Warn("sidecar probe skipped: concurrency limit reached", "account_id", account.ID) + return + } + s.probes.Add(1) + s.mu.Unlock() + go func() { - ctx, cancel := context.WithTimeout(context.Background(), probeRequestTimeout) + defer s.probes.Done() + defer func() { <-s.probeCh }() + ctx, cancel := context.WithTimeout(parentCtx, probeRequestTimeout) defer cancel() s.runProbe(ctx, snap) }() diff --git a/backend/internal/gateway/tlsfingerprint.go b/backend/internal/gateway/tlsfingerprint.go index 4ae1ed6..9dd247e 100644 --- a/backend/internal/gateway/tlsfingerprint.go +++ b/backend/internal/gateway/tlsfingerprint.go @@ -82,20 +82,20 @@ var defaultSignatureAlgorithms = []utls.SignatureScheme{ // 不应通过人为加 GREASE 打乱 JA3。 func buildBunClientHelloSpec() *utls.ClientHelloSpec { extensions := []utls.TLSExtension{ - &utls.SNIExtension{}, // 0: server_name - &utls.GREASEEncryptedClientHelloExtension{}, // 65037: ECH (boringssl GREASE) - &utls.ExtendedMasterSecretExtension{}, // 23 - &utls.RenegotiationInfoExtension{Renegotiation: utls.RenegotiateOnceAsClient}, // 65281 - &utls.SupportedCurvesExtension{Curves: defaultCurves}, // 10 - &utls.SupportedPointsExtension{SupportedPoints: []uint8{0}}, // 11 (uncompressed) - &utls.SessionTicketExtension{}, // 35 - &utls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}}, // 16 - &utls.StatusRequestExtension{}, // 5 - &utls.SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: defaultSignatureAlgorithms}, // 13 - &utls.SCTExtension{}, // 18 - &utls.KeyShareExtension{KeyShares: []utls.KeyShare{{Group: utls.X25519}}}, // 51 - &utls.PSKKeyExchangeModesExtension{Modes: []uint8{uint8(utls.PskModeDHE)}}, // 45 - &utls.SupportedVersionsExtension{Versions: []uint16{utls.VersionTLS13, utls.VersionTLS12}}, // 43 + &utls.SNIExtension{}, // 0: server_name + &utls.GREASEEncryptedClientHelloExtension{}, // 65037: ECH (boringssl GREASE) + &utls.ExtendedMasterSecretExtension{}, // 23 + &utls.RenegotiationInfoExtension{Renegotiation: utls.RenegotiateOnceAsClient}, // 65281 + &utls.SupportedCurvesExtension{Curves: defaultCurves}, // 10 + &utls.SupportedPointsExtension{SupportedPoints: []uint8{0}}, // 11 (uncompressed) + &utls.SessionTicketExtension{}, // 35 + &utls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}}, // 16 + &utls.StatusRequestExtension{}, // 5 + &utls.SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: defaultSignatureAlgorithms}, // 13 + &utls.SCTExtension{}, // 18 + &utls.KeyShareExtension{KeyShares: []utls.KeyShare{{Group: utls.X25519}}}, // 51 + &utls.PSKKeyExchangeModesExtension{Modes: []uint8{uint8(utls.PskModeDHE)}}, // 45 + &utls.SupportedVersionsExtension{Versions: []uint16{utls.VersionTLS13, utls.VersionTLS12}}, // 43 } return &utls.ClientHelloSpec{ @@ -130,18 +130,13 @@ func selectClientHelloSpec(profile string) *utls.ClientHelloSpec { // TLS 指纹 Transport 构建 // ────────────────────────────────────────────────────── -// buildFingerprintTransport 构建带 Bun 1.3.x TLS 指纹的 Transport +// buildFingerprintTransportWithProfile 允许账号指定 tls_profile 精确选择 ClientHelloSpec // // Bun 默认 fetch 只协商 http/1.1(ground truth),因此: // - ALPN 只带 http/1.1 // - ForceAttemptHTTP2=false,不启 http2.ConfigureTransport // // 不强制 h2 是刻意的:Anthropic 侧有行为模型对比,配置与真实 CLI 不一致会拉高识别分数。 -func buildFingerprintTransport(proxyURL string) *http.Transport { - return buildFingerprintTransportWithProfile(proxyURL, "") -} - -// buildFingerprintTransportWithProfile 允许账号指定 tls_profile 精确选择 ClientHelloSpec func buildFingerprintTransportWithProfile(proxyURL, profile string) *http.Transport { dialer := &net.Dialer{ Timeout: httpDialTimeout, @@ -281,9 +276,24 @@ func dialThroughProxy(ctx context.Context, proxyURL string, targetAddr string, d return nil, fmt.Errorf("proxy CONNECT failed: %s", resp.Status) } + if br.Buffered() > 0 { + return &bufferedProxyConn{Conn: conn, reader: br}, nil + } return conn, nil } +type bufferedProxyConn struct { + net.Conn + reader *bufio.Reader +} + +func (c *bufferedProxyConn) Read(b []byte) (int, error) { + if c.reader != nil && c.reader.Buffered() > 0 { + return c.reader.Read(b) + } + return c.Conn.Read(b) +} + func hasPort(addr string) bool { _, _, err := net.SplitHostPort(addr) return err == nil diff --git a/backend/internal/gateway/token_manager.go b/backend/internal/gateway/token_manager.go index d95ed4b..3202ccb 100644 --- a/backend/internal/gateway/token_manager.go +++ b/backend/internal/gateway/token_manager.go @@ -21,13 +21,19 @@ const ( refreshCooldown = 60 * time.Second // 已知失败的冷却窗口 maxRefreshRetries = 2 // 最大重试次数 refreshRetryBackoff = 1 * time.Second // 重试退避间隔 + + tokenStateMaxEntries = 100000 + tokenStateIdleTTL = 24 * time.Hour + tokenStateCleanupInterval = 30 * time.Minute ) // tokenManager 管理 OAuth token 的并发安全刷新 type tokenManager struct { - gateway *AnthropicGateway - logger *slog.Logger - locks sync.Map // accountID (int64) -> *accountRefreshState + gateway *AnthropicGateway + logger *slog.Logger + mu sync.Mutex + locks map[int64]*accountRefreshState + lastCleanupTime time.Time } // accountRefreshState 单个账号的刷新状态 @@ -37,20 +43,63 @@ type accountRefreshState struct { lastToken string // 上次刷新后的 access_token(用于 double-check) lastError error lastErrorAt time.Time + lastSeenAt time.Time } // newTokenManager 创建 token 管理器 func newTokenManager(gw *AnthropicGateway, logger *slog.Logger) *tokenManager { return &tokenManager{ - gateway: gw, - logger: logger, + gateway: gw, + logger: logger, + locks: make(map[int64]*accountRefreshState), + lastCleanupTime: time.Now(), } } // getState 获取或创建账号的刷新状态 func (m *tokenManager) getState(accountID int64) *accountRefreshState { - val, _ := m.locks.LoadOrStore(accountID, &accountRefreshState{}) - return val.(*accountRefreshState) + now := time.Now() + m.mu.Lock() + defer m.mu.Unlock() + m.cleanupLocked(now) + if state, ok := m.locks[accountID]; ok { + state.lastSeenAt = now + return state + } + if len(m.locks) >= tokenStateMaxEntries { + m.deleteOldestLocked() + } + state := &accountRefreshState{lastSeenAt: now} + m.locks[accountID] = state + return state +} + +func (m *tokenManager) cleanupLocked(now time.Time) { + if now.Sub(m.lastCleanupTime) < tokenStateCleanupInterval && len(m.locks) < tokenStateMaxEntries { + return + } + for accountID, state := range m.locks { + if now.Sub(state.lastSeenAt) > tokenStateIdleTTL { + delete(m.locks, accountID) + } + } + m.lastCleanupTime = now +} + +func (m *tokenManager) deleteOldestLocked() { + var oldestID int64 + var oldestAt time.Time + found := false + for accountID, state := range m.locks { + if !found || state.lastSeenAt.Before(oldestAt) { + oldestID = accountID + oldestAt = state.lastSeenAt + found = true + } + } + if found { + delete(m.locks, oldestID) + } } // ensureValidToken 检查 token 过期状态,必要时自动刷新 @@ -195,10 +244,17 @@ func (m *tokenManager) doRefresh(ctx context.Context, account *sdk.Account) (map var lastErr error for attempt := 0; attempt <= maxRefreshRetries; attempt++ { if attempt > 0 { + timer := time.NewTimer(refreshRetryBackoff * time.Duration(attempt)) select { case <-ctx.Done(): + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } return nil, ctx.Err() - case <-time.After(refreshRetryBackoff * time.Duration(attempt)): + case <-timer.C: } } diff --git a/backend/internal/gateway/transport_pool.go b/backend/internal/gateway/transport_pool.go index 62bda6a..7dc8a43 100644 --- a/backend/internal/gateway/transport_pool.go +++ b/backend/internal/gateway/transport_pool.go @@ -6,6 +6,8 @@ import ( "net" "net/http" "net/url" + "strconv" + "strings" "sync" "time" ) @@ -14,18 +16,31 @@ import ( // 传输连接池:复用 HTTP Transport,避免每次请求创建新连接 // ────────────────────────────────────────────────────── +const ( + standardTransportMaxEntries = 100000 + standardTransportIdleTTL = 2 * time.Hour + standardTransportCleanupInterval = 10 * time.Minute +) + +type standardTransportEntry struct { + transport *http.Transport + lastUsedAt time.Time +} + // StandardTransportPool API Key 账号的标准 TLS 连接池 // 按 proxyURL 分组缓存 Transport type StandardTransportPool struct { - mu sync.RWMutex - pool map[string]*http.Transport // key = proxyURL (空字符串表示直连) - dialer *net.Dialer + mu sync.RWMutex + pool map[string]*standardTransportEntry // key = proxyURL (空字符串表示直连) + dialer *net.Dialer + lastCleanupTime time.Time } // NewStandardTransportPool 创建标准 Transport 连接池 func NewStandardTransportPool() *StandardTransportPool { return &StandardTransportPool{ - pool: make(map[string]*http.Transport), + pool: make(map[string]*standardTransportEntry), + lastCleanupTime: time.Now(), dialer: &net.Dialer{ Timeout: httpDialTimeout, KeepAlive: 30 * time.Second, @@ -35,9 +50,12 @@ func NewStandardTransportPool() *StandardTransportPool { // Get 获取或创建 Transport func (p *StandardTransportPool) Get(proxyURL string) *http.Transport { + now := time.Now() p.mu.RLock() - if t, ok := p.pool[proxyURL]; ok { + if entry, ok := p.pool[proxyURL]; ok { + t := entry.transport p.mu.RUnlock() + p.touch(proxyURL, now) return t } p.mu.RUnlock() @@ -46,13 +64,18 @@ func (p *StandardTransportPool) Get(proxyURL string) *http.Transport { defer p.mu.Unlock() // Double-check - if t, ok := p.pool[proxyURL]; ok { - return t + if entry, ok := p.pool[proxyURL]; ok { + entry.lastUsedAt = now + return entry.transport } + p.cleanupIdleLocked(now) + if len(p.pool) >= standardTransportMaxEntries { + p.deleteOldestLocked() + } t := &http.Transport{ DialContext: p.dialer.DialContext, - TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12}, + TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12}, TLSHandshakeTimeout: httpTLSTimeout, MaxIdleConns: httpMaxIdleConns, MaxIdleConnsPerHost: httpIdleConnsPerHost, @@ -66,32 +89,83 @@ func (p *StandardTransportPool) Get(proxyURL string) *http.Transport { } } - p.pool[proxyURL] = t + p.pool[proxyURL] = &standardTransportEntry{transport: t, lastUsedAt: now} return t } +func (p *StandardTransportPool) touch(proxyURL string, now time.Time) { + p.mu.Lock() + if entry, ok := p.pool[proxyURL]; ok { + entry.lastUsedAt = now + } + p.cleanupIdleLocked(now) + p.mu.Unlock() +} + +func (p *StandardTransportPool) cleanupIdleLocked(now time.Time) { + if now.Sub(p.lastCleanupTime) < standardTransportCleanupInterval && len(p.pool) < standardTransportMaxEntries { + return + } + for proxyURL, entry := range p.pool { + if now.Sub(entry.lastUsedAt) > standardTransportIdleTTL { + entry.transport.CloseIdleConnections() + delete(p.pool, proxyURL) + } + } + p.lastCleanupTime = now +} + +func (p *StandardTransportPool) deleteOldestLocked() { + var oldestKey string + var oldestAt time.Time + for proxyURL, entry := range p.pool { + if oldestKey == "" || entry.lastUsedAt.Before(oldestAt) { + oldestKey = proxyURL + oldestAt = entry.lastUsedAt + } + } + if oldestKey != "" { + p.pool[oldestKey].transport.CloseIdleConnections() + delete(p.pool, oldestKey) + } +} + // Close 关闭所有 Transport func (p *StandardTransportPool) Close() { p.mu.Lock() defer p.mu.Unlock() - for _, t := range p.pool { - t.CloseIdleConnections() + for _, entry := range p.pool { + entry.transport.CloseIdleConnections() } - p.pool = make(map[string]*http.Transport) + p.pool = make(map[string]*standardTransportEntry) + p.lastCleanupTime = time.Now() } // FingerprintTransportPool OAuth/session_key 的 uTLS 指纹连接池 // 按 (accountID, proxyURL) 分桶:不同账号持有独立 Transport, // 互不共享连接 / session ticket / PSK,使得多账号在上游看来是独立 CLI 实例。 +const ( + fingerprintTransportMaxEntries = 100000 + fingerprintTransportIdleTTL = 2 * time.Hour + fingerprintTransportCleanupInterval = 10 * time.Minute +) + +type fingerprintTransportEntry struct { + transport *http.Transport + lastUsedAt time.Time +} + type FingerprintTransportPool struct { - mu sync.RWMutex - pool map[string]*http.Transport // key = "accountID|proxyURL" + mu sync.RWMutex + pool map[string]*fingerprintTransportEntry // key = "accountID|proxyURL|profile" + lastCleanupTime time.Time } // NewFingerprintTransportPool 创建 TLS 指纹 Transport 连接池 func NewFingerprintTransportPool() *FingerprintTransportPool { return &FingerprintTransportPool{ - pool: make(map[string]*http.Transport), + pool: make(map[string]*fingerprintTransportEntry), + lastCleanupTime: time.Now(), } } @@ -103,10 +177,13 @@ func fpKey(accountID int64, proxyURL, profile string) string { // Get 获取或创建指纹化 Transport(按账号 + profile 隔离) func (p *FingerprintTransportPool) Get(accountID int64, proxyURL, profile string) *http.Transport { key := fpKey(accountID, proxyURL, profile) + now := time.Now() p.mu.RLock() - if t, ok := p.pool[key]; ok { + if entry, ok := p.pool[key]; ok { + t := entry.transport p.mu.RUnlock() + p.touch(key, now) return t } p.mu.RUnlock() @@ -114,23 +191,82 @@ func (p *FingerprintTransportPool) Get(accountID int64, proxyURL, profile string p.mu.Lock() defer p.mu.Unlock() - if t, ok := p.pool[key]; ok { - return t + if entry, ok := p.pool[key]; ok { + entry.lastUsedAt = now + return entry.transport } + p.cleanupIdleLocked(now) + if len(p.pool) >= fingerprintTransportMaxEntries { + p.deleteOldestLocked() + } t := buildFingerprintTransportWithProfile(proxyURL, profile) - p.pool[key] = t + p.pool[key] = &fingerprintTransportEntry{transport: t, lastUsedAt: now} return t } +func (p *FingerprintTransportPool) touch(key string, now time.Time) { + p.mu.Lock() + if entry, ok := p.pool[key]; ok { + entry.lastUsedAt = now + } + p.cleanupIdleLocked(now) + p.mu.Unlock() +} + +func (p *FingerprintTransportPool) cleanupIdleLocked(now time.Time) { + if now.Sub(p.lastCleanupTime) < fingerprintTransportCleanupInterval && len(p.pool) < fingerprintTransportMaxEntries { + return + } + for key, entry := range p.pool { + if now.Sub(entry.lastUsedAt) > fingerprintTransportIdleTTL { + entry.transport.CloseIdleConnections() + delete(p.pool, key) + } + } + p.lastCleanupTime = now +} + +func (p *FingerprintTransportPool) deleteOldestLocked() { + var oldestKey string + var oldestAt time.Time + for key, entry := range p.pool { + if oldestKey == "" || entry.lastUsedAt.Before(oldestAt) { + oldestKey = key + oldestAt = entry.lastUsedAt + } + } + if oldestKey != "" { + p.pool[oldestKey].transport.CloseIdleConnections() + delete(p.pool, oldestKey) + } +} + +// RemoveAccount 移除指定账号的指纹 Transport,供账号删除/禁用事件调用。 +func (p *FingerprintTransportPool) RemoveAccount(accountID int64) { + if p == nil { + return + } + prefix := strconv.FormatInt(accountID, 10) + "|" + p.mu.Lock() + defer p.mu.Unlock() + for key, entry := range p.pool { + if strings.HasPrefix(key, prefix) { + entry.transport.CloseIdleConnections() + delete(p.pool, key) + } + } +} + // Close 关闭所有 Transport func (p *FingerprintTransportPool) Close() { p.mu.Lock() defer p.mu.Unlock() - for _, t := range p.pool { - t.CloseIdleConnections() + for _, entry := range p.pool { + entry.transport.CloseIdleConnections() } - p.pool = make(map[string]*http.Transport) + p.pool = make(map[string]*fingerprintTransportEntry) + p.lastCleanupTime = time.Now() } // getHTTPClient 根据账号类型从连接池获取 HTTP Client From daf293604743891928fac5837e85d47eb28e8877 Mon Sep 17 00:00:00 2001 From: quantal Date: Sun, 31 May 2026 21:08:28 +0800 Subject: [PATCH 18/20] =?UTF-8?q?chore(deps):=20=E6=9B=B4=E6=96=B0=20pnpm?= =?UTF-8?q?=20=E7=89=88=E6=9C=AC=E8=87=B3=2011.5.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/continuous-integration.yml | 2 +- .github/workflows/release.yml | 2 +- backend/go.sum | 2 +- web/package.json | 2 +- web/pnpm-lock.yaml | 507 ++++++++++--------- web/pnpm-workspace.yaml | 2 + web/src/components/UsageCostDetail.tsx | 4 +- web/src/components/UsageMetricDetail.tsx | 33 +- 8 files changed, 285 insertions(+), 269 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index b369c6a..e02289b 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -19,7 +19,7 @@ jobs: - uses: pnpm/action-setup@v6 with: - version: 11.1.1 + version: 11.5.0 - uses: actions/setup-node@v6 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 98d5b89..c005cce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,7 +31,7 @@ jobs: - uses: pnpm/action-setup@v6 with: - version: 11.1.1 + version: 11.5.0 - uses: actions/setup-node@v6 with: diff --git a/backend/go.sum b/backend/go.sum index b04c0c4..7065f23 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,4 +1,4 @@ -github.com/DevilGenius/airgate-sdk v0.2.1 h1:6qIJX3NNK0yOaj+OkF3HxZCLNY2k4OfEc6vOGti8+io= +github.com/DevilGenius/airgate-sdk v0.2.1 h1:wNwMlG+tNhKstuW7qbvRhiHCg94cxTxb/fEbXt+D7Yc= github.com/DevilGenius/airgate-sdk v0.2.1/go.mod h1:EnjPPVBiONFv0gcQqiBPqs86HRhcVKICdFaB03yJcI8= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= diff --git a/web/package.json b/web/package.json index c4db823..c0d5613 100644 --- a/web/package.json +++ b/web/package.json @@ -3,7 +3,7 @@ "private": true, "version": "1.0.0", "type": "module", - "packageManager": "pnpm@11.1.1", + "packageManager": "pnpm@11.5.0", "scripts": { "build": "vite build", "dev": "vite build --watch" diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index e65e101..09e5122 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -10,20 +10,20 @@ importers: dependencies: '@devilgenius/airgate-theme': specifier: https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz - version: https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz(react@19.2.5) + version: https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz(react@19.2.6) react: specifier: ^19.0.0 - version: 19.2.5 + version: 19.2.6 react-dom: specifier: ^19.0.0 - version: 19.2.5(react@19.2.5) + version: 19.2.6(react@19.2.6) devDependencies: '@types/react': specifier: ^19.0.0 - version: 19.2.14 + version: 19.2.15 '@types/react-dom': specifier: ^19.0.0 - version: 19.2.3(@types/react@19.2.14) + version: 19.2.3(@types/react@19.2.15) '@vitejs/plugin-react': specifier: ^4.3.0 version: 4.7.0(vite@6.4.2) @@ -36,91 +36,91 @@ importers: packages: - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.29.0': - resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} engines: {node: '>=6.9.0'} - '@babel/core@7.29.0': - resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.28.6': - resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} engines: {node: '>=6.9.0'} - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-plugin-utils@7.28.6': - resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.29.2': - resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.2': - resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-transform-react-jsx-self@7.27.1': - resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx-source@7.27.1': - resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} '@devilgenius/airgate-theme@https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz': - resolution: {tarball: https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz} + resolution: {tarball: https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz, integrity: sha512-OeumFl/SOBeqOrXE/bNR+vPMxc7pwTwrHLik5zQf34ktBQasxPXe+sgWDPiqu5LEfKmtM3w33sH4Uhc6OS3FPg==} version: 0.2.1 peerDependencies: react: ^19.0.0 @@ -300,141 +300,141 @@ packages: '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} - '@rollup/rollup-android-arm-eabi@4.60.2': - resolution: {integrity: sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==} + '@rollup/rollup-android-arm-eabi@4.60.4': + resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.60.2': - resolution: {integrity: sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==} + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.60.2': - resolution: {integrity: sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==} + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.60.2': - resolution: {integrity: sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==} + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.60.2': - resolution: {integrity: sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==} + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.60.2': - resolution: {integrity: sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==} + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.60.2': - resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==} + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.60.2': - resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==} + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.60.2': - resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==} + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.60.2': - resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==} + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.60.2': - resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==} + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.60.2': - resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==} + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.60.2': - resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==} + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.60.2': - resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==} + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.60.2': - resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==} + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.60.2': - resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==} + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.60.2': - resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==} + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.60.2': - resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==} + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.60.2': - resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==} + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.60.2': - resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==} + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.60.2': - resolution: {integrity: sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==} + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.60.2': - resolution: {integrity: sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==} + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.60.2': - resolution: {integrity: sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==} + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.60.2': - resolution: {integrity: sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==} + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.60.2': - resolution: {integrity: sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==} + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} cpu: [x64] os: [win32] @@ -458,8 +458,8 @@ packages: peerDependencies: '@types/react': ^19.2.0 - '@types/react@19.2.14': - resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/react@19.2.15': + resolution: {integrity: sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==} '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} @@ -467,8 +467,8 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - baseline-browser-mapping@2.10.24: - resolution: {integrity: sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA==} + baseline-browser-mapping@2.10.32: + resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==} engines: {node: '>=6.0.0'} hasBin: true @@ -477,8 +477,8 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - caniuse-lite@1.0.30001791: - resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==} + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -495,8 +495,8 @@ packages: supports-color: optional: true - electron-to-chromium@1.5.345: - resolution: {integrity: sha512-F9JXQGiMrz6yVNPI2qOVPvB9HzjH5cGzhs8oJ6A28V5L/YnzN/0KsuiibqF+F1Fd9qxFzD1BUnYSd8JfULxTwg==} + electron-to-chromium@1.5.364: + resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==} esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} @@ -544,13 +544,14 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - node-releases@2.0.38: - resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -559,25 +560,25 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - postcss@8.5.12: - resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} - react-dom@19.2.5: - resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} peerDependencies: - react: ^19.2.5 + react: ^19.2.6 react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} - react@19.2.5: - resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} engines: {node: '>=0.10.0'} - rollup@4.60.2: - resolution: {integrity: sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==} + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -652,25 +653,25 @@ packages: snapshots: - '@babel/code-frame@7.29.0': + '@babel/code-frame@7.29.7': dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.29.0': {} + '@babel/compat-data@7.29.7': {} - '@babel/core@7.29.0': + '@babel/core@7.29.7': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.29.2 - '@babel/parser': 7.29.2 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -680,93 +681,93 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.29.1': + '@babel/generator@7.29.7': dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 - '@babel/helper-compilation-targets@7.28.6': + '@babel/helper-compilation-targets@7.29.7': dependencies: - '@babel/compat-data': 7.29.0 - '@babel/helper-validator-option': 7.27.1 + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 browserslist: 4.28.2 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-globals@7.28.0': {} + '@babel/helper-globals@7.29.7': {} - '@babel/helper-module-imports@7.28.6': + '@babel/helper-module-imports@7.29.7': dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-plugin-utils@7.29.7': {} - '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@7.29.7': {} - '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} - '@babel/helper-validator-option@7.27.1': {} + '@babel/helper-validator-option@7.29.7': {} - '@babel/helpers@7.29.2': + '@babel/helpers@7.29.7': dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 - '@babel/parser@7.29.2': + '@babel/parser@7.29.7': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 - '@babel/template@7.28.6': + '@babel/template@7.29.7': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 - '@babel/traverse@7.29.0': + '@babel/traverse@7.29.7': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.2 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.29.0': + '@babel/types@7.29.7': dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 - '@devilgenius/airgate-theme@https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz(react@19.2.5)': + '@devilgenius/airgate-theme@https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz(react@19.2.6)': dependencies: - react: 19.2.5 + react: 19.2.6 '@esbuild/aix-ppc64@0.25.12': optional: true @@ -867,117 +868,117 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.27': {} - '@rollup/rollup-android-arm-eabi@4.60.2': + '@rollup/rollup-android-arm-eabi@4.60.4': optional: true - '@rollup/rollup-android-arm64@4.60.2': + '@rollup/rollup-android-arm64@4.60.4': optional: true - '@rollup/rollup-darwin-arm64@4.60.2': + '@rollup/rollup-darwin-arm64@4.60.4': optional: true - '@rollup/rollup-darwin-x64@4.60.2': + '@rollup/rollup-darwin-x64@4.60.4': optional: true - '@rollup/rollup-freebsd-arm64@4.60.2': + '@rollup/rollup-freebsd-arm64@4.60.4': optional: true - '@rollup/rollup-freebsd-x64@4.60.2': + '@rollup/rollup-freebsd-x64@4.60.4': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.60.2': + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.60.2': + '@rollup/rollup-linux-arm-musleabihf@4.60.4': optional: true - '@rollup/rollup-linux-arm64-gnu@4.60.2': + '@rollup/rollup-linux-arm64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-arm64-musl@4.60.2': + '@rollup/rollup-linux-arm64-musl@4.60.4': optional: true - '@rollup/rollup-linux-loong64-gnu@4.60.2': + '@rollup/rollup-linux-loong64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-loong64-musl@4.60.2': + '@rollup/rollup-linux-loong64-musl@4.60.4': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.60.2': + '@rollup/rollup-linux-ppc64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-ppc64-musl@4.60.2': + '@rollup/rollup-linux-ppc64-musl@4.60.4': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.60.2': + '@rollup/rollup-linux-riscv64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-riscv64-musl@4.60.2': + '@rollup/rollup-linux-riscv64-musl@4.60.4': optional: true - '@rollup/rollup-linux-s390x-gnu@4.60.2': + '@rollup/rollup-linux-s390x-gnu@4.60.4': optional: true - '@rollup/rollup-linux-x64-gnu@4.60.2': + '@rollup/rollup-linux-x64-gnu@4.60.4': optional: true - '@rollup/rollup-linux-x64-musl@4.60.2': + '@rollup/rollup-linux-x64-musl@4.60.4': optional: true - '@rollup/rollup-openbsd-x64@4.60.2': + '@rollup/rollup-openbsd-x64@4.60.4': optional: true - '@rollup/rollup-openharmony-arm64@4.60.2': + '@rollup/rollup-openharmony-arm64@4.60.4': optional: true - '@rollup/rollup-win32-arm64-msvc@4.60.2': + '@rollup/rollup-win32-arm64-msvc@4.60.4': optional: true - '@rollup/rollup-win32-ia32-msvc@4.60.2': + '@rollup/rollup-win32-ia32-msvc@4.60.4': optional: true - '@rollup/rollup-win32-x64-gnu@4.60.2': + '@rollup/rollup-win32-x64-gnu@4.60.4': optional: true - '@rollup/rollup-win32-x64-msvc@4.60.2': + '@rollup/rollup-win32-x64-msvc@4.60.4': optional: true '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.29.2 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@types/estree@1.0.8': {} - '@types/react-dom@19.2.3(@types/react@19.2.14)': + '@types/react-dom@19.2.3(@types/react@19.2.15)': dependencies: - '@types/react': 19.2.14 + '@types/react': 19.2.15 - '@types/react@19.2.14': + '@types/react@19.2.15': dependencies: csstype: 3.2.3 '@vitejs/plugin-react@4.7.0(vite@6.4.2)': dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 @@ -985,17 +986,17 @@ snapshots: transitivePeerDependencies: - supports-color - baseline-browser-mapping@2.10.24: {} + baseline-browser-mapping@2.10.32: {} browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.24 - caniuse-lite: 1.0.30001791 - electron-to-chromium: 1.5.345 - node-releases: 2.0.38 + baseline-browser-mapping: 2.10.32 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.364 + node-releases: 2.0.46 update-browserslist-db: 1.2.3(browserslist@4.28.2) - caniuse-lite@1.0.30001791: {} + caniuse-lite@1.0.30001793: {} convert-source-map@2.0.0: {} @@ -1005,7 +1006,7 @@ snapshots: dependencies: ms: 2.1.3 - electron-to-chromium@1.5.345: {} + electron-to-chromium@1.5.364: {} esbuild@0.25.12: optionalDependencies: @@ -1059,58 +1060,58 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.11: {} + nanoid@3.3.12: {} - node-releases@2.0.38: {} + node-releases@2.0.46: {} picocolors@1.1.1: {} picomatch@4.0.4: {} - postcss@8.5.12: + postcss@8.5.15: dependencies: - nanoid: 3.3.11 + nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 - react-dom@19.2.5(react@19.2.5): + react-dom@19.2.6(react@19.2.6): dependencies: - react: 19.2.5 + react: 19.2.6 scheduler: 0.27.0 react-refresh@0.17.0: {} - react@19.2.5: {} + react@19.2.6: {} - rollup@4.60.2: + rollup@4.60.4: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.2 - '@rollup/rollup-android-arm64': 4.60.2 - '@rollup/rollup-darwin-arm64': 4.60.2 - '@rollup/rollup-darwin-x64': 4.60.2 - '@rollup/rollup-freebsd-arm64': 4.60.2 - '@rollup/rollup-freebsd-x64': 4.60.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.2 - '@rollup/rollup-linux-arm-musleabihf': 4.60.2 - '@rollup/rollup-linux-arm64-gnu': 4.60.2 - '@rollup/rollup-linux-arm64-musl': 4.60.2 - '@rollup/rollup-linux-loong64-gnu': 4.60.2 - '@rollup/rollup-linux-loong64-musl': 4.60.2 - '@rollup/rollup-linux-ppc64-gnu': 4.60.2 - '@rollup/rollup-linux-ppc64-musl': 4.60.2 - '@rollup/rollup-linux-riscv64-gnu': 4.60.2 - '@rollup/rollup-linux-riscv64-musl': 4.60.2 - '@rollup/rollup-linux-s390x-gnu': 4.60.2 - '@rollup/rollup-linux-x64-gnu': 4.60.2 - '@rollup/rollup-linux-x64-musl': 4.60.2 - '@rollup/rollup-openbsd-x64': 4.60.2 - '@rollup/rollup-openharmony-arm64': 4.60.2 - '@rollup/rollup-win32-arm64-msvc': 4.60.2 - '@rollup/rollup-win32-ia32-msvc': 4.60.2 - '@rollup/rollup-win32-x64-gnu': 4.60.2 - '@rollup/rollup-win32-x64-msvc': 4.60.2 + '@rollup/rollup-android-arm-eabi': 4.60.4 + '@rollup/rollup-android-arm64': 4.60.4 + '@rollup/rollup-darwin-arm64': 4.60.4 + '@rollup/rollup-darwin-x64': 4.60.4 + '@rollup/rollup-freebsd-arm64': 4.60.4 + '@rollup/rollup-freebsd-x64': 4.60.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 + '@rollup/rollup-linux-arm-musleabihf': 4.60.4 + '@rollup/rollup-linux-arm64-gnu': 4.60.4 + '@rollup/rollup-linux-arm64-musl': 4.60.4 + '@rollup/rollup-linux-loong64-gnu': 4.60.4 + '@rollup/rollup-linux-loong64-musl': 4.60.4 + '@rollup/rollup-linux-ppc64-gnu': 4.60.4 + '@rollup/rollup-linux-ppc64-musl': 4.60.4 + '@rollup/rollup-linux-riscv64-gnu': 4.60.4 + '@rollup/rollup-linux-riscv64-musl': 4.60.4 + '@rollup/rollup-linux-s390x-gnu': 4.60.4 + '@rollup/rollup-linux-x64-gnu': 4.60.4 + '@rollup/rollup-linux-x64-musl': 4.60.4 + '@rollup/rollup-openbsd-x64': 4.60.4 + '@rollup/rollup-openharmony-arm64': 4.60.4 + '@rollup/rollup-win32-arm64-msvc': 4.60.4 + '@rollup/rollup-win32-ia32-msvc': 4.60.4 + '@rollup/rollup-win32-x64-gnu': 4.60.4 + '@rollup/rollup-win32-x64-msvc': 4.60.4 fsevents: 2.3.3 scheduler@0.27.0: {} @@ -1137,8 +1138,8 @@ snapshots: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.12 - rollup: 4.60.2 + postcss: 8.5.15 + rollup: 4.60.4 tinyglobby: 0.2.16 optionalDependencies: fsevents: 2.3.3 diff --git a/web/pnpm-workspace.yaml b/web/pnpm-workspace.yaml index 5ed0b5a..4c96ef2 100644 --- a/web/pnpm-workspace.yaml +++ b/web/pnpm-workspace.yaml @@ -1,2 +1,4 @@ allowBuilds: esbuild: true +minimumReleaseAgeExclude: + - '@devilgenius/airgate-theme@0.2.1' diff --git a/web/src/components/UsageCostDetail.tsx b/web/src/components/UsageCostDetail.tsx index 6794bae..0bebec0 100644 --- a/web/src/components/UsageCostDetail.tsx +++ b/web/src/components/UsageCostDetail.tsx @@ -145,7 +145,7 @@ function fallbackDetails(record: UsageRecordLike): UsageCostDetailItem[] { return [ { key: 'input_tokens', label: '输入', account_cost: record.input_cost }, { key: 'cached_input_tokens', label: '缓存读取', account_cost: record.cached_input_cost }, - { key: 'cache_creation_tokens', label: '缓存写入', account_cost: record.cache_creation_cost }, + { key: 'cache_creation_tokens', label: '缓存创建', account_cost: record.cache_creation_cost }, { key: 'output_tokens', label: '输出', account_cost: record.output_cost }, ].filter((item) => (item.account_cost ?? 0) > 0); } @@ -172,7 +172,7 @@ export function UsageCostDetail({ context }: UsageRecordSurfaceProps) { if (record.output_price && record.output_price > 0) unitPrices.push({ label: '输出单价', value: `$${record.output_price.toFixed(4)} / 1M Token` }); if (record.cache_creation_price && record.cache_creation_price > 0) - unitPrices.push({ label: '缓存写入单价', value: `$${record.cache_creation_price.toFixed(4)} / 1M Token` }); + unitPrices.push({ label: '缓存创建单价', value: `$${record.cache_creation_price.toFixed(4)} / 1M Token` }); } const hasRateInfo = !!record.service_tier diff --git a/web/src/components/UsageMetricDetail.tsx b/web/src/components/UsageMetricDetail.tsx index ea0b94d..683a3b5 100644 --- a/web/src/components/UsageMetricDetail.tsx +++ b/web/src/components/UsageMetricDetail.tsx @@ -11,6 +11,15 @@ interface UsageRecordLike { usage_metadata?: Record; } +const TOKEN_COLORS = { + cacheCreation: 'var(--ag-usage-token-cache-creation)', + cacheRead: 'var(--ag-usage-token-cache-read)', + input: 'var(--ag-usage-token-input)', + output: 'var(--ag-usage-token-output)', + reasoning: 'var(--ag-usage-token-reasoning)', + total: 'var(--ag-text)', +} as const; + const panelStyle: CSSProperties = { overflow: 'hidden', borderRadius: 'var(--radius)', @@ -74,9 +83,9 @@ const inlineValueStyle: CSSProperties = { }; const inlineValueMetaStyle: CSSProperties = { + color: TOKEN_COLORS.reasoning, minWidth: 0, overflow: 'hidden', - color: 'var(--ag-text-tertiary)', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }; @@ -98,6 +107,10 @@ const valueStyle: CSSProperties = { whiteSpace: 'nowrap', }; +const strongValueStyle: CSSProperties = { + fontWeight: 700, +}; + function recordFromContext(context: UsageRecordSurfaceProps['context']): UsageRecordLike { const record = context?.record; return record && typeof record === 'object' ? record as UsageRecordLike : {}; @@ -120,11 +133,11 @@ function formatNumber(value: number) { : value.toLocaleString(undefined, { maximumFractionDigits: 4 }); } -function Row({ label, tone, value }: { label: ReactNode; tone?: string; value: ReactNode }) { +function Row({ label, strong, tone, value }: { label: ReactNode; strong?: boolean; tone?: string; value: ReactNode }) { return (
{label} - {value} + {value}
); } @@ -159,13 +172,13 @@ export function UsageMetricDetail({ context }: UsageRecordSurfaceProps) { {record.model ?
{record.model}
: null}
- - - {cacheReadTokens > 0 ? : null} - {cacheCreationTokens > 0 ? : null} - {cacheCreation5mTokens > 0 ? : null} - {cacheCreation1hTokens > 0 ? : null} - + + + {cacheReadTokens > 0 ? : null} + {cacheCreationTokens > 0 ? : null} + {cacheCreation5mTokens > 0 ? : null} + {cacheCreation1hTokens > 0 ? : null} +
); From 6d438089d27a86ec372ac7e3dae8cdd0e76bf38c Mon Sep 17 00:00:00 2001 From: quantal Date: Thu, 4 Jun 2026 01:26:45 +0800 Subject: [PATCH 19/20] =?UTF-8?q?feat(gateway):=20=E6=B7=BB=E5=8A=A0SSE?= =?UTF-8?q?=E6=B5=81=E5=A4=84=E7=90=86=E7=9A=84=E4=B8=8A=E4=B8=8B=E6=96=87?= =?UTF-8?q?=E5=8F=96=E6=B6=88=E5=92=8C=E7=A9=BA=E9=97=B2=E8=B6=85=E6=97=B6?= =?UTF-8?q?=E6=9C=BA=E5=88=B6=20refactor(web):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E5=88=9B=E5=BB=BAToken=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/gateway/headers.go | 7 +- backend/internal/gateway/oauth.go | 4 + backend/internal/gateway/stream.go | 115 +++++++++++++++++------ web/src/components/UsageMetricDetail.tsx | 32 ++++++- web/src/components/UsageModelMeta.tsx | 1 + 5 files changed, 124 insertions(+), 35 deletions(-) diff --git a/backend/internal/gateway/headers.go b/backend/internal/gateway/headers.go index ad106fa..ec64c82 100644 --- a/backend/internal/gateway/headers.go +++ b/backend/internal/gateway/headers.go @@ -174,7 +174,12 @@ func pickRetryCount() string { // setRawHeader 绕过 Go 的 header 大小写规范化,直接设置原始 key func setRawHeader(h http.Header, key, value string) { - h.Del(key) + if strings.ContainsAny(key, "\r\n") || strings.ContainsAny(value, "\r\n") { + return + } + canonicalKey := http.CanonicalHeaderKey(key) + h.Del(canonicalKey) + delete(h, canonicalKey) delete(h, key) h[key] = []string{value} } diff --git a/backend/internal/gateway/oauth.go b/backend/internal/gateway/oauth.go index 1bb6fab..0a02f3b 100644 --- a/backend/internal/gateway/oauth.go +++ b/backend/internal/gateway/oauth.go @@ -353,6 +353,10 @@ func (g *AnthropicGateway) buildOAuthClient(proxyURL string) *http.Client { // exchangeSessionKeyWithScope 通用的 Session Key → OAuth Token 流程 func (g *AnthropicGateway) exchangeSessionKeyWithScope(ctx context.Context, sessionKey, proxyURL, scope string) (*TokenResponse, error) { + if strings.TrimSpace(proxyURL) != "" { + return nil, fmt.Errorf("session_key exchange does not support proxy_url") + } + // claude.ai 请求使用 Chrome 指纹客户端(绕过 Cloudflare) var reqClient *req.Client if g != nil && g.oauthReqPool != nil { diff --git a/backend/internal/gateway/stream.go b/backend/internal/gateway/stream.go index e3b8761..05e712b 100644 --- a/backend/internal/gateway/stream.go +++ b/backend/internal/gateway/stream.go @@ -2,6 +2,7 @@ package gateway import ( "bufio" + "context" "fmt" "io" "net/http" @@ -15,6 +16,7 @@ import ( ) // SSE 流式响应透传 + usage 提取。调用者保证 resp.StatusCode 是 2xx(4xx/5xx 由 handleErrorResponse 处理)。 +const sseIdleTimeout = 90 * time.Second // handleStreamResponse 透传 Anthropic SSE 流,同时累加 usage 字段。 // 流中途断开时返回 OutcomeStreamAborted(字节已开写,Core 不会 failover)。 @@ -29,44 +31,95 @@ func handleStreamResponse(resp *http.Response, w http.ResponseWriter, start time var tokens tokenUsage var firstTokenOnce sync.Once + baseCtx := context.Background() + if resp.Request != nil && resp.Request.Context() != nil { + baseCtx = resp.Request.Context() + } + ctx, cancel := context.WithCancel(baseCtx) + defer cancel() + scanner := bufio.NewScanner(resp.Body) scanner.Buffer(make([]byte, 64*1024), 1024*1024) - - for scanner.Scan() { - line := scanner.Text() - if _, err := fmt.Fprintf(w, "%s\n", line); err != nil { - break - } - if flusher, ok := w.(http.Flusher); ok { - flusher.Flush() + lines := make(chan string) + scanErr := make(chan error, 1) + go func() { + defer close(lines) + for scanner.Scan() { + line := scanner.Text() + select { + case lines <- line: + case <-ctx.Done(): + scanErr <- ctx.Err() + return + } } - - data, ok := extractSSEData(line) - if !ok || data == "" { - continue - } - eventType := gjson.Get(data, "type").String() - - if eventType == "content_block_delta" { - firstTokenOnce.Do(func() { - usage.FirstTokenMs = time.Since(start).Milliseconds() - }) + scanErr <- scanner.Err() + }() + + idleTimer := time.NewTimer(sseIdleTimeout) + defer idleTimer.Stop() + + for { + select { + case line, ok := <-lines: + if !ok { + elapsed := time.Since(start) + if err := <-scanErr; err != nil { + return streamAbortedOutcome(resp.StatusCode, err.Error(), usage), fmt.Errorf("读取上游 SSE 失败: %w", err) + } + fillUsageCost(usage) + return sdk.ForwardOutcome{ + Kind: sdk.OutcomeSuccess, + Upstream: sdk.UpstreamResponse{StatusCode: resp.StatusCode}, + Usage: usage, + Duration: elapsed, + }, nil + } + resetSSEIdleTimer(idleTimer) + + if _, err := fmt.Fprintf(w, "%s\n", line); err != nil { + _ = resp.Body.Close() + return streamAbortedOutcome(resp.StatusCode, err.Error(), usage), fmt.Errorf("写入 SSE 响应失败: %w", err) + } + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } + + data, ok := extractSSEData(line) + if !ok || data == "" { + continue + } + eventType := gjson.Get(data, "type").String() + + if eventType == "content_block_delta" { + firstTokenOnce.Do(func() { + usage.FirstTokenMs = time.Since(start).Milliseconds() + }) + } + extractAnthropicUsage(data, eventType, usage, &tokens) + + case <-idleTimer.C: + err := fmt.Errorf("上游 SSE 空闲超时") + cancel() + _ = resp.Body.Close() + return streamAbortedOutcome(resp.StatusCode, err.Error(), usage), err + + case <-ctx.Done(): + err := ctx.Err() + _ = resp.Body.Close() + return streamAbortedOutcome(resp.StatusCode, err.Error(), usage), err } - extractAnthropicUsage(data, eventType, usage, &tokens) } +} - elapsed := time.Since(start) - if err := scanner.Err(); err != nil { - return streamAbortedOutcome(resp.StatusCode, err.Error(), usage), fmt.Errorf("读取上游 SSE 失败: %w", err) +func resetSSEIdleTimer(timer *time.Timer) { + if !timer.Stop() { + select { + case <-timer.C: + default: + } } - - fillUsageCost(usage) - return sdk.ForwardOutcome{ - Kind: sdk.OutcomeSuccess, - Upstream: sdk.UpstreamResponse{StatusCode: resp.StatusCode}, - Usage: usage, - Duration: elapsed, - }, nil + timer.Reset(sseIdleTimeout) } // handleNonStreamResponse 处理非流式响应。resp.StatusCode 预设 2xx。 diff --git a/web/src/components/UsageMetricDetail.tsx b/web/src/components/UsageMetricDetail.tsx index 683a3b5..90ed71f 100644 --- a/web/src/components/UsageMetricDetail.tsx +++ b/web/src/components/UsageMetricDetail.tsx @@ -90,6 +90,11 @@ const inlineValueMetaStyle: CSSProperties = { whiteSpace: 'nowrap', }; +const cacheCreationDetailStyle: CSSProperties = { + ...inlineValueMetaStyle, + color: TOKEN_COLORS.cacheRead, +}; + const inlineValueNumberStyle: CSSProperties = { flexShrink: 0, }; @@ -153,6 +158,23 @@ function outputTokenValue(reasoningTokens: number, outputTokens: number) { ); } +function cacheCreationTokenValue(totalTokens: number, cacheCreation5mTokens: number, cacheCreation1hTokens: number) { + const parts: Array<[string, number]> = []; + if (cacheCreation5mTokens > 0) parts.push(['5m', cacheCreation5mTokens]); + if (cacheCreation1hTokens > 0) parts.push(['1h', cacheCreation1hTokens]); + + if (parts.length === 0) return formatNumber(totalTokens); + + return ( + + + ({parts.map(([label, value]) => `${label} ${formatNumber(value)}`).join(',')}) + + {formatNumber(totalTokens)} + + ); +} + export function UsageMetricDetail({ context }: UsageRecordSurfaceProps) { const record = recordFromContext(context); const metadata = metadataFromContext(context, record); @@ -175,9 +197,13 @@ export function UsageMetricDetail({ context }: UsageRecordSurfaceProps) { {cacheReadTokens > 0 ? : null} - {cacheCreationTokens > 0 ? : null} - {cacheCreation5mTokens > 0 ? : null} - {cacheCreation1hTokens > 0 ? : null} + {cacheCreationTokens > 0 ? ( + + ) : null}
diff --git a/web/src/components/UsageModelMeta.tsx b/web/src/components/UsageModelMeta.tsx index e22b201..c246561 100644 --- a/web/src/components/UsageModelMeta.tsx +++ b/web/src/components/UsageModelMeta.tsx @@ -10,6 +10,7 @@ const EFFORT_COLORS: Record = { medium: 'rgb(59,130,246)', high: 'rgb(249,115,22)', xhigh: 'rgb(239,68,68)', + max: 'rgb(239,68,68)', }; function chipStyle(color: string): CSSProperties { From 942073e8075a8a85ce62eb1affbe19949e859a12 Mon Sep 17 00:00:00 2001 From: quantal Date: Thu, 4 Jun 2026 02:44:54 +0800 Subject: [PATCH 20/20] chore(deps): update airgate-sdk to v0.2.2 --- backend/go.mod | 2 +- backend/go.sum | 4 ++-- web/package.json | 2 +- web/pnpm-lock.yaml | 12 ++++++------ web/pnpm-workspace.yaml | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/backend/go.mod b/backend/go.mod index 7ea35a8..36f72b3 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -3,7 +3,7 @@ module github.com/DevilGenius/airgate-claude/backend go 1.26.3 require ( - github.com/DevilGenius/airgate-sdk v0.2.1 + github.com/DevilGenius/airgate-sdk v0.2.2 github.com/imroc/req/v3 v3.57.0 github.com/refraction-networking/utls v1.8.2 github.com/tidwall/gjson v1.18.0 diff --git a/backend/go.sum b/backend/go.sum index 7065f23..620b7fa 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,5 +1,5 @@ -github.com/DevilGenius/airgate-sdk v0.2.1 h1:wNwMlG+tNhKstuW7qbvRhiHCg94cxTxb/fEbXt+D7Yc= -github.com/DevilGenius/airgate-sdk v0.2.1/go.mod h1:EnjPPVBiONFv0gcQqiBPqs86HRhcVKICdFaB03yJcI8= +github.com/DevilGenius/airgate-sdk v0.2.2 h1:sw9YbbQGpYQwUfm7BmOpcArqSnasEeAqUNO/TEbE0+0= +github.com/DevilGenius/airgate-sdk v0.2.2/go.mod h1:EnjPPVBiONFv0gcQqiBPqs86HRhcVKICdFaB03yJcI8= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= diff --git a/web/package.json b/web/package.json index c0d5613..b12e557 100644 --- a/web/package.json +++ b/web/package.json @@ -9,7 +9,7 @@ "dev": "vite build --watch" }, "dependencies": { - "@devilgenius/airgate-theme": "https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz", + "@devilgenius/airgate-theme": "https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.2/airgate-theme-v0.2.2.tgz", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 09e5122..9abc73b 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@devilgenius/airgate-theme': - specifier: https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz - version: https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz(react@19.2.6) + specifier: https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.2/airgate-theme-v0.2.2.tgz + version: https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.2/airgate-theme-v0.2.2.tgz(react@19.2.6) react: specifier: ^19.0.0 version: 19.2.6 @@ -119,9 +119,9 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} - '@devilgenius/airgate-theme@https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz': - resolution: {tarball: https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz, integrity: sha512-OeumFl/SOBeqOrXE/bNR+vPMxc7pwTwrHLik5zQf34ktBQasxPXe+sgWDPiqu5LEfKmtM3w33sH4Uhc6OS3FPg==} - version: 0.2.1 + '@devilgenius/airgate-theme@https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.2/airgate-theme-v0.2.2.tgz': + resolution: {integrity: sha512-vN/msFVriS/yMfcLUJrvYPr8z/d4GcEJse4HNvswHoQg78+ujBs0jtPEJD7RN40PHrUZ0qoOwI5G4VTsHyc3KA==, tarball: https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.2/airgate-theme-v0.2.2.tgz} + version: 0.2.2 peerDependencies: react: ^19.0.0 @@ -765,7 +765,7 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@devilgenius/airgate-theme@https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.1/airgate-theme-v0.2.1.tgz(react@19.2.6)': + '@devilgenius/airgate-theme@https://github.com/DevilGenius/airgate-sdk/releases/download/v0.2.2/airgate-theme-v0.2.2.tgz(react@19.2.6)': dependencies: react: 19.2.6 diff --git a/web/pnpm-workspace.yaml b/web/pnpm-workspace.yaml index 4c96ef2..f2f342e 100644 --- a/web/pnpm-workspace.yaml +++ b/web/pnpm-workspace.yaml @@ -1,4 +1,4 @@ allowBuilds: esbuild: true minimumReleaseAgeExclude: - - '@devilgenius/airgate-theme@0.2.1' + - '@devilgenius/airgate-theme@0.2.2'