diff --git a/src/server/routes/api/accountTokens.ts b/src/server/routes/api/accountTokens.ts index 2ab0279..8763485 100644 --- a/src/server/routes/api/accountTokens.ts +++ b/src/server/routes/api/accountTokens.ts @@ -12,8 +12,9 @@ import { syncTokensFromUpstream, } from '../../services/accountTokenService.js'; import { getAdapter } from '../../services/platforms/index.js'; -import { getCredentialModeFromExtraConfig, resolvePlatformUserId } from '../../services/accountExtraConfig.js'; +import { getCredentialModeFromExtraConfig, getProxyUrlFromExtraConfig, resolvePlatformUserId } from '../../services/accountExtraConfig.js'; import { startBackgroundTask } from '../../services/backgroundTaskService.js'; +import { withAccountProxyOverride } from '../../services/siteProxy.js'; type AccountWithSiteRow = { accounts: typeof schema.accounts.$inferSelect; @@ -231,15 +232,18 @@ async function executeAccountTokenSync(row: AccountWithSiteRow): Promise adapter.getApiTokens(row.sites.url, row.accounts.accessToken, platformUserId), + () => withAccountProxyOverride(accountProxyUrl, + () => adapter.getApiTokens(row.sites.url, row.accounts.accessToken, platformUserId)), TOKEN_SYNC_TIMEOUT_MS, `token sync timeout (${Math.round(TOKEN_SYNC_TIMEOUT_MS / 1000)}s)`, ); if (tokens.length === 0) { const fallback = await withTimeout( - () => adapter.getApiToken(row.sites.url, row.accounts.accessToken, platformUserId), + () => withAccountProxyOverride(accountProxyUrl, + () => adapter.getApiToken(row.sites.url, row.accounts.accessToken, platformUserId)), TOKEN_SYNC_TIMEOUT_MS, `token sync timeout (${Math.round(TOKEN_SYNC_TIMEOUT_MS / 1000)}s)`, ); @@ -468,20 +472,23 @@ export async function accountTokensRoutes(app: FastifyInstance) { } const platformUserId = resolvePlatformUserId(account.extraConfig, account.username); - const createdViaUpstream = await adapter.createApiToken( - site.url, - account.accessToken, - platformUserId, - { - name: asTrimmedString(body.name), - group: asTrimmedString(body.group), - unlimitedQuota, - remainQuota, - expiredTime, - allowIps: asTrimmedString(body.allowIps), - modelLimitsEnabled, - modelLimits: asTrimmedString(body.modelLimits), - }, + const createdViaUpstream = await withAccountProxyOverride( + getProxyUrlFromExtraConfig(account.extraConfig), + () => adapter.createApiToken( + site.url, + account.accessToken, + platformUserId, + { + name: asTrimmedString(body.name), + group: asTrimmedString(body.group), + unlimitedQuota, + remainQuota, + expiredTime, + allowIps: asTrimmedString(body.allowIps), + modelLimitsEnabled, + modelLimits: asTrimmedString(body.modelLimits), + }, + ), ); if (!createdViaUpstream) { return reply.code(502).send({ success: false, message: '站点创建令牌失败' }); @@ -536,11 +543,14 @@ export async function accountTokensRoutes(app: FastifyInstance) { if (shouldDeleteUpstream) { const platformUserId = resolvePlatformUserId(account.extraConfig, account.username); - const upstreamDeleted = await adapter!.deleteApiToken( - site.url, - account.accessToken, - existing.token, - platformUserId, + const upstreamDeleted = await withAccountProxyOverride( + getProxyUrlFromExtraConfig(account.extraConfig), + () => adapter!.deleteApiToken( + site.url, + account.accessToken, + existing.token, + platformUserId, + ), ); if (!upstreamDeleted) { return { success: false, message: '站点删除令牌失败,本地未删除' }; @@ -771,7 +781,10 @@ export async function accountTokensRoutes(app: FastifyInstance) { try { const platformUserId = resolvePlatformUserId(account.extraConfig, account.username); - const groups = await adapter.getUserGroups(site.url, account.accessToken, platformUserId); + const groups = await withAccountProxyOverride( + getProxyUrlFromExtraConfig(account.extraConfig), + () => adapter.getUserGroups(site.url, account.accessToken, platformUserId), + ); const normalized = Array.from(new Set((groups || []).map((item) => String(item || '').trim()).filter(Boolean))); return { success: true, groups: normalized.length > 0 ? normalized : ['default'] }; } catch (error: any) { diff --git a/src/server/routes/api/accounts.ts b/src/server/routes/api/accounts.ts index c298ec8..e9d451c 100644 --- a/src/server/routes/api/accounts.ts +++ b/src/server/routes/api/accounts.ts @@ -7,6 +7,7 @@ import { refreshModelsForAccount, rebuildTokenRoutesFromAvailability } from '../ import { ensureDefaultTokenForAccount, syncTokensFromUpstream } from '../../services/accountTokenService.js'; import { getCredentialModeFromExtraConfig, + getProxyUrlFromExtraConfig, guessPlatformUserIdFromUsername, getSub2ApiAuthFromExtraConfig, mergeAccountExtraConfig, @@ -25,7 +26,7 @@ import { type RuntimeHealthState, } from '../../services/accountHealthService.js'; import { appendSessionTokenRebindHint } from '../../services/alertRules.js'; -import { withSiteRecordProxyRequestInit } from '../../services/siteProxy.js'; +import { parseSiteProxyUrlInput, withAccountProxyOverride, withSiteRecordProxyRequestInit } from '../../services/siteProxy.js'; import { createRateLimitGuard } from '../../middleware/requestRateLimit.js'; type AccountWithSiteRow = { @@ -890,7 +891,10 @@ export async function accountsRoutes(app: FastifyInstance) { let verifyResult: any; try { - verifyResult = await adapter.verifyToken(site.url, nextAccessToken, candidatePlatformUserId); + verifyResult = await withAccountProxyOverride( + getProxyUrlFromExtraConfig(account.extraConfig), + () => adapter.verifyToken(site.url, nextAccessToken, candidatePlatformUserId), + ); } catch (err: any) { return reply.code(400).send({ success: false, @@ -1210,6 +1214,18 @@ export async function accountsRoutes(app: FastifyInstance) { updates.sortOrder = normalizedSortOrder; } + if (Object.prototype.hasOwnProperty.call(body, 'proxyUrl')) { + const baseExtraConfig = typeof updates.extraConfig === 'string' + ? updates.extraConfig : account.extraConfig; + const { present, valid, proxyUrl: normalizedProxy } = parseSiteProxyUrlInput(body.proxyUrl); + if (present && !valid) { + return reply.code(400).send({ message: 'Invalid proxy URL format' }); + } + updates.extraConfig = mergeAccountExtraConfig(baseExtraConfig, { + proxyUrl: normalizedProxy ?? undefined, + }); + } + updates.updatedAt = new Date().toISOString(); await db.update(schema.accounts).set(updates).where(eq(schema.accounts.id, id)).run(); diff --git a/src/server/services/balanceService.ts b/src/server/services/balanceService.ts index f5806b2..b9afa19 100644 --- a/src/server/services/balanceService.ts +++ b/src/server/services/balanceService.ts @@ -6,6 +6,7 @@ import { reportTokenExpired } from './alertService.js'; import { getAutoReloginConfig, getCredentialModeFromExtraConfig, + getProxyUrlFromExtraConfig, getSub2ApiAuthFromExtraConfig, mergeAccountExtraConfig, resolvePlatformUserId, @@ -14,7 +15,7 @@ import { decryptAccountPassword } from './accountCredentialService.js'; import { extractRuntimeHealth, setAccountRuntimeHealth } from './accountHealthService.js'; import { updateTodayIncomeSnapshot } from './todayIncomeRewardService.js'; import type { BalanceInfo } from './platforms/base.js'; -import { withSiteProxyRequestInit, withSiteRecordProxyRequestInit } from './siteProxy.js'; +import { withAccountProxyOverride, withSiteProxyRequestInit, withSiteRecordProxyRequestInit } from './siteProxy.js'; function isSiteDisabled(status?: string | null): boolean { return (status || 'active') === 'disabled'; @@ -316,7 +317,10 @@ async function tryAutoRelogin(account: any, site: any): Promise { const password = decryptAccountPassword(relogin.passwordCipher); if (!password) return null; - const loginResult = await adapter.login(site.url, relogin.username, password); + const loginResult = await withAccountProxyOverride( + getProxyUrlFromExtraConfig(account.extraConfig), + () => adapter.login(site.url, relogin.username, password), + ); if (!loginResult.success || !loginResult.accessToken) return null; await db.update(schema.accounts) @@ -393,7 +397,9 @@ export async function refreshBalance(accountId: number) { } } - const readBalance = async (token: string) => adapter.getBalance(site.url, token, platformUserId); + const accountProxyUrl = getProxyUrlFromExtraConfig(account.extraConfig); + const readBalance = async (token: string) => withAccountProxyOverride(accountProxyUrl, + () => adapter.getBalance(site.url, token, platformUserId)); const handleBalanceError = async (err: any) => { const message = appendSessionTokenRebindHint(err?.message || 'unknown error'); setAccountRuntimeHealth(account.id, { diff --git a/src/server/services/checkinService.ts b/src/server/services/checkinService.ts index fc5c77c..4ada9f7 100644 --- a/src/server/services/checkinService.ts +++ b/src/server/services/checkinService.ts @@ -8,6 +8,7 @@ import { refreshBalance } from './balanceService.js'; import { parseCheckinRewardAmount } from './checkinRewardParser.js'; import { getAutoReloginConfig, + getProxyUrlFromExtraConfig, getPlatformUserIdFromExtraConfig, guessPlatformUserIdFromUsername, mergeAccountExtraConfig, @@ -16,6 +17,7 @@ import { import { decryptAccountPassword } from './accountCredentialService.js'; import { setAccountRuntimeHealth } from './accountHealthService.js'; import { formatUtcSqlDateTime } from './localTimeService.js'; +import { withAccountProxyOverride } from './siteProxy.js'; type CheckinExecutionStatus = 'success' | 'failed' | 'skipped'; @@ -100,7 +102,10 @@ async function tryAutoRelogin(account: any, site: any): Promise { const password = decryptAccountPassword(relogin.passwordCipher); if (!password) return null; - const result = await adapter.login(site.url, relogin.username, password); + const result = await withAccountProxyOverride( + getProxyUrlFromExtraConfig(account.extraConfig), + () => adapter.login(site.url, relogin.username, password), + ); if (!result.success || !result.accessToken) return null; await db.update(schema.accounts) @@ -172,14 +177,17 @@ export async function checkinAccount(accountId: number, options?: { skipEvent?: : guessPlatformUserIdFromUsername(account.username); const platformUserId = resolvePlatformUserId(account.extraConfig, account.username); + const accountProxyUrl = getProxyUrlFromExtraConfig(account.extraConfig); let activeAccessToken = account.accessToken; - let result = await adapter.checkin(site.url, activeAccessToken, platformUserId); + let result = await withAccountProxyOverride(accountProxyUrl, + () => adapter.checkin(site.url, activeAccessToken, platformUserId)); if (!result.success && shouldAttemptAutoRelogin(result.message)) { const refreshedAccessToken = await tryAutoRelogin(account, site); if (refreshedAccessToken) { activeAccessToken = refreshedAccessToken; - result = await adapter.checkin(site.url, activeAccessToken, platformUserId); + result = await withAccountProxyOverride(accountProxyUrl, + () => adapter.checkin(site.url, activeAccessToken, platformUserId)); } } diff --git a/src/server/services/modelService.ts b/src/server/services/modelService.ts index b97ad79..df70983 100644 --- a/src/server/services/modelService.ts +++ b/src/server/services/modelService.ts @@ -2,10 +2,11 @@ import { and, eq } from 'drizzle-orm'; import { db, schema } from '../db/index.js'; import { getAdapter } from './platforms/index.js'; import { ensureDefaultTokenForAccount, getPreferredAccountToken } from './accountTokenService.js'; -import { getCredentialModeFromExtraConfig, resolvePlatformUserId } from './accountExtraConfig.js'; +import { getCredentialModeFromExtraConfig, getProxyUrlFromExtraConfig, resolvePlatformUserId } from './accountExtraConfig.js'; import { invalidateTokenRouterCache } from './tokenRouter.js'; import { setAccountRuntimeHealth } from './accountHealthService.js'; import { clearAllRouteDecisionSnapshots } from './routeDecisionSnapshotStore.js'; +import { withAccountProxyOverride } from './siteProxy.js'; const API_TOKEN_DISCOVERY_TIMEOUT_MS = 8_000; const MODEL_DISCOVERY_TIMEOUT_MS = 12_000; @@ -129,12 +130,14 @@ export async function refreshModelsForAccount(accountId: number) { } const platformUserId = resolvePlatformUserId(account.extraConfig, account.username); + const accountProxyUrl = getProxyUrlFromExtraConfig(account.extraConfig); let discoveredApiToken: string | null = null; if (!account.apiToken && account.accessToken) { try { discoveredApiToken = await withTimeout( - () => adapter.getApiToken(site.url, account.accessToken, platformUserId), + () => withAccountProxyOverride(accountProxyUrl, + () => adapter.getApiToken(site.url, account.accessToken, platformUserId)), API_TOKEN_DISCOVERY_TIMEOUT_MS, `api token discovery timeout (${Math.round(API_TOKEN_DISCOVERY_TIMEOUT_MS / 1000)}s)`, ); @@ -200,7 +203,8 @@ export async function refreshModelsForAccount(accountId: number) { try { models = normalizeModels( await withTimeout( - () => adapter.getModels(site.url, credential, platformUserId), + () => withAccountProxyOverride(accountProxyUrl, + () => adapter.getModels(site.url, credential, platformUserId)), MODEL_DISCOVERY_TIMEOUT_MS, `model discovery timeout (${Math.round(MODEL_DISCOVERY_TIMEOUT_MS / 1000)}s)`, ), @@ -227,7 +231,8 @@ export async function refreshModelsForAccount(accountId: number) { try { models = normalizeModels( await withTimeout( - () => adapter.getModels(site.url, token.token, platformUserId), + () => withAccountProxyOverride(accountProxyUrl, + () => adapter.getModels(site.url, token.token, platformUserId)), MODEL_DISCOVERY_TIMEOUT_MS, `model discovery timeout (${Math.round(MODEL_DISCOVERY_TIMEOUT_MS / 1000)}s)`, ),