fix: reconcile current upstream accounts
This commit is contained in:
@@ -353,7 +353,7 @@ func (a *App) syncUpstreamAccounts(ctx context.Context) error {
|
||||
Disabled: entry.Disabled, Unavailable: entry.Unavailable, Priority: entry.Priority, LastSeenAt: now,
|
||||
})
|
||||
}
|
||||
return store.SyncUpstreamAccounts(ctx, accounts)
|
||||
return store.ReconcileUpstreamAccounts(ctx, accounts)
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
|
||||
@@ -282,11 +282,27 @@ func (r *SQLiteUsageRepository) managedKeyModels(ctx context.Context, keyID stri
|
||||
}
|
||||
|
||||
func (r *SQLiteUsageRepository) SyncUpstreamAccounts(ctx context.Context, accounts []managedaccess.UpstreamAccount) error {
|
||||
return r.syncUpstreamAccounts(ctx, accounts, false)
|
||||
}
|
||||
|
||||
// ReconcileUpstreamAccounts treats accounts as the complete current CPA auth snapshot.
|
||||
// Historical rows remain available for existing strict bindings, but are excluded from
|
||||
// new route selection once CPA no longer reports them.
|
||||
func (r *SQLiteUsageRepository) ReconcileUpstreamAccounts(ctx context.Context, accounts []managedaccess.UpstreamAccount) error {
|
||||
return r.syncUpstreamAccounts(ctx, accounts, true)
|
||||
}
|
||||
|
||||
func (r *SQLiteUsageRepository) syncUpstreamAccounts(ctx context.Context, accounts []managedaccess.UpstreamAccount, snapshot bool) error {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("开始同步上游账号: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
if snapshot {
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE upstream_accounts SET current=0`); err != nil {
|
||||
return fmt.Errorf("重置上游账号快照: %w", err)
|
||||
}
|
||||
}
|
||||
for _, account := range accounts {
|
||||
var existingID string
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
@@ -300,14 +316,14 @@ SELECT id FROM upstream_accounts WHERE cpa_auth_id=? OR (cpa_auth_index <> '' AN
|
||||
existingID = fmt.Sprintf("up_%x", sum[:10])
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO upstream_accounts (id, cpa_auth_id, cpa_auth_index, provider, display_name, status, status_message, disabled, unavailable, priority, last_seen_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO upstream_accounts (id, cpa_auth_id, cpa_auth_index, provider, display_name, status, status_message, disabled, unavailable, current, priority, last_seen_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET cpa_auth_id=excluded.cpa_auth_id, provider=excluded.provider,
|
||||
cpa_auth_index=CASE WHEN excluded.cpa_auth_index <> '' THEN excluded.cpa_auth_index ELSE upstream_accounts.cpa_auth_index END,
|
||||
display_name=CASE WHEN excluded.display_name = excluded.cpa_auth_id AND upstream_accounts.display_name <> '' THEN upstream_accounts.display_name ELSE excluded.display_name END,
|
||||
status=excluded.status,
|
||||
status_message=excluded.status_message, disabled=excluded.disabled, unavailable=excluded.unavailable,
|
||||
priority=excluded.priority, last_seen_at=excluded.last_seen_at`, existingID, account.CPAAuthID,
|
||||
current=1, priority=excluded.priority, last_seen_at=excluded.last_seen_at`, existingID, account.CPAAuthID,
|
||||
account.CPAAuthIndex, account.Provider, account.DisplayName, account.Status, account.StatusMessage,
|
||||
account.Disabled, account.Unavailable, account.Priority, formatTime(account.LastSeenAt))
|
||||
if err != nil {
|
||||
@@ -324,7 +340,7 @@ func (r *SQLiteUsageRepository) ListUpstreamAccounts(ctx context.Context) ([]man
|
||||
rows, err := r.readDB.QueryContext(ctx, `
|
||||
SELECT id, cpa_auth_id, cpa_auth_index, provider, display_name, status, status_message,
|
||||
disabled, unavailable, priority, last_seen_at
|
||||
FROM upstream_accounts ORDER BY provider, display_name COLLATE NOCASE`)
|
||||
FROM upstream_accounts WHERE current=1 ORDER BY provider, display_name COLLATE NOCASE, cpa_auth_index`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询上游账号: %w", err)
|
||||
}
|
||||
|
||||
@@ -86,6 +86,46 @@ func TestManagedKeyStatsUseShanghaiDayBoundary(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileUpstreamAccountsListsOnlyCurrentSnapshot(t *testing.T) {
|
||||
store, err := repository.OpenSQLiteUsage(filepath.Join(t.TempDir(), "usage.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer store.Close()
|
||||
ctx := context.Background()
|
||||
old := managedaccess.UpstreamAccount{
|
||||
CPAAuthID: "codex-user-prolite.json", CPAAuthIndex: "old-index",
|
||||
Provider: "codex", DisplayName: "user@example.com", LastSeenAt: time.Now().Add(-time.Hour),
|
||||
}
|
||||
current := managedaccess.UpstreamAccount{
|
||||
CPAAuthID: "codex-user-pro.json", CPAAuthIndex: "current-index",
|
||||
Provider: "codex", DisplayName: "user@example.com", LastSeenAt: time.Now(),
|
||||
}
|
||||
if err := store.ReconcileUpstreamAccounts(ctx, []managedaccess.UpstreamAccount{old, current}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
initial, err := store.ListUpstreamAccounts(ctx)
|
||||
if err != nil || len(initial) != 2 {
|
||||
t.Fatalf("initial accounts = %+v, err=%v", initial, err)
|
||||
}
|
||||
var oldID string
|
||||
for _, account := range initial {
|
||||
if account.CPAAuthID == old.CPAAuthID {
|
||||
oldID = account.ID
|
||||
}
|
||||
}
|
||||
if err := store.ReconcileUpstreamAccounts(ctx, []managedaccess.UpstreamAccount{current}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
listed, err := store.ListUpstreamAccounts(ctx)
|
||||
if err != nil || len(listed) != 1 || listed[0].CPAAuthID != current.CPAAuthID {
|
||||
t.Fatalf("current accounts = %+v, err=%v", listed, err)
|
||||
}
|
||||
if historical, err := store.UpstreamAccountByID(ctx, oldID); err != nil || historical.CPAAuthID != old.CPAAuthID {
|
||||
t.Fatalf("historical account = %+v, err=%v", historical, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagedKeyStatsCountsOrphanUsageAndAliasLifecycleOnce(t *testing.T) {
|
||||
store, err := repository.OpenSQLiteUsage(filepath.Join(t.TempDir(), "usage.db"))
|
||||
if err != nil {
|
||||
|
||||
@@ -149,6 +149,7 @@ CREATE TABLE IF NOT EXISTS upstream_accounts (
|
||||
status_message TEXT NOT NULL DEFAULT '',
|
||||
disabled INTEGER NOT NULL DEFAULT 0,
|
||||
unavailable INTEGER NOT NULL DEFAULT 0,
|
||||
current INTEGER NOT NULL DEFAULT 1,
|
||||
priority INTEGER NOT NULL DEFAULT 0,
|
||||
last_seen_at TEXT NOT NULL
|
||||
);
|
||||
@@ -290,6 +291,7 @@ func OpenSQLiteUsage(databasePath string) (*SQLiteUsageRepository, error) {
|
||||
`ALTER TABLE model_prices ADD COLUMN source_revision TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE model_prices ADD COLUMN source_fetched_at TEXT NOT NULL DEFAULT ''`,
|
||||
`ALTER TABLE billing_accounts ADD COLUMN lifetime_spent_micros INTEGER NOT NULL DEFAULT 0`,
|
||||
`ALTER TABLE upstream_accounts ADD COLUMN current INTEGER NOT NULL DEFAULT 1`,
|
||||
} {
|
||||
if _, err := db.Exec(migration); err != nil && !strings.Contains(strings.ToLower(err.Error()), "duplicate column") {
|
||||
_ = db.Close()
|
||||
|
||||
@@ -467,7 +467,14 @@ function modelRules() {
|
||||
}
|
||||
|
||||
function upstreamLabel(account) {
|
||||
return `${account.provider || "unknown"} · ${account.display_name || account.cpa_auth_id || account.id}`;
|
||||
const provider = account.provider || "unknown";
|
||||
const name = account.display_name || account.cpa_auth_id || account.id;
|
||||
const duplicated = upstreamAccounts.some(item => item !== account &&
|
||||
(item.provider || "unknown") === provider &&
|
||||
(item.display_name || item.cpa_auth_id || item.id) === name);
|
||||
if (!duplicated) return `${provider} · ${name}`;
|
||||
const identity = (account.cpa_auth_index || account.cpa_auth_id || account.id).slice(0, 8);
|
||||
return `${provider} · ${name} · ${identity}`;
|
||||
}
|
||||
|
||||
function routeLabel(key) {
|
||||
|
||||
Reference in New Issue
Block a user