feat(proxy): propagate account-level proxy across all surfaces

- chatSurface/openAiResponsesSurface: extract channelProxyUrl once and pass to withSiteRecordProxyRequestInit
- geminiSurface: switch from withSiteProxyRequestInit to withSiteRecordProxyRequestInit with channelProxyUrl
- balanceService: pass accountProxyUrl to sub2api token refresh requests
- siteProxy: fix isAccountOverride to only be true when account proxy differs from site proxy
- Accounts.tsx: use parseAccountExtraConfig helper for proxy badge display
This commit is contained in:
Hureru
2026-03-21 11:44:57 +08:00
parent 8df3b1431f
commit 4d0c4b8129
6 changed files with 38 additions and 38 deletions
@@ -204,6 +204,7 @@ export async function handleChatSurfaceRequest(
runtime: endpointRequest.runtime, runtime: endpointRequest.runtime,
}; };
}; };
const channelProxyUrl = resolveChannelProxyUrl(selected.site, selected.account.extraConfig);
const dispatchRequest = ( const dispatchRequest = (
compatibilityRequest: BuiltEndpointRequest, compatibilityRequest: BuiltEndpointRequest,
targetUrl?: string, targetUrl?: string,
@@ -216,7 +217,7 @@ export async function handleChatSurfaceRequest(
method: 'POST', method: 'POST',
headers: requestForFetch.headers, headers: requestForFetch.headers,
body: JSON.stringify(requestForFetch.body), body: JSON.stringify(requestForFetch.body),
}), }, channelProxyUrl),
}) })
); );
const endpointStrategy = downstreamTransformer.compatibility.createEndpointStrategy({ const endpointStrategy = downstreamTransformer.compatibility.createEndpointStrategy({
@@ -265,7 +266,6 @@ export async function handleChatSurfaceRequest(
try { try {
const endpointResult = await executeEndpointFlow({ const endpointResult = await executeEndpointFlow({
siteUrl: selected.site.url, siteUrl: selected.site.url,
proxyUrl: resolveChannelProxyUrl(selected.site, selected.account.extraConfig),
endpointCandidates, endpointCandidates,
buildRequest: (endpoint) => buildEndpointRequest(endpoint), buildRequest: (endpoint) => buildEndpointRequest(endpoint),
dispatchRequest, dispatchRequest,
@@ -9,7 +9,7 @@ import { isModelAllowedByPolicyOrAllowedRoutes } from '../../services/downstream
import { tokenRouter } from '../../services/tokenRouter.js'; import { tokenRouter } from '../../services/tokenRouter.js';
import { buildOauthProviderHeaders } from '../../services/oauth/service.js'; import { buildOauthProviderHeaders } from '../../services/oauth/service.js';
import { getOauthInfoFromExtraConfig } from '../../services/oauth/oauthAccount.js'; import { getOauthInfoFromExtraConfig } from '../../services/oauth/oauthAccount.js';
import { withSiteProxyRequestInit } from '../../services/siteProxy.js'; import { resolveChannelProxyUrl, withSiteRecordProxyRequestInit } from '../../services/siteProxy.js';
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js'; import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
import { getDownstreamRoutingPolicy } from '../../routes/proxy/downstreamPolicy.js'; import { getDownstreamRoutingPolicy } from '../../routes/proxy/downstreamPolicy.js';
import { executeEndpointFlow, type BuiltEndpointRequest } from '../../routes/proxy/endpointFlow.js'; import { executeEndpointFlow, type BuiltEndpointRequest } from '../../routes/proxy/endpointFlow.js';
@@ -470,6 +470,7 @@ export async function geminiProxyRoute(app: FastifyInstance) {
selected.tokenValue, selected.tokenValue,
query, query,
); );
const channelProxyUrl = resolveChannelProxyUrl(selected.site, selected.account.extraConfig);
const upstream = isInternalGemini const upstream = isInternalGemini
? await dispatchRuntimeRequest({ ? await dispatchRuntimeRequest({
siteUrl: selected.site.url, siteUrl: selected.site.url,
@@ -489,11 +490,11 @@ export async function geminiProxyRoute(app: FastifyInstance) {
: (isStreamAction ? 'streamGenerateContent' : 'generateContent'), : (isStreamAction ? 'streamGenerateContent' : 'generateContent'),
}, },
}, },
buildInit: async (requestUrl, requestForFetch) => withSiteProxyRequestInit(requestUrl, { buildInit: async (_requestUrl, requestForFetch) => withSiteRecordProxyRequestInit(selected.site, {
method: 'POST', method: 'POST',
headers: requestForFetch.headers, headers: requestForFetch.headers,
body: JSON.stringify(requestForFetch.body), body: JSON.stringify(requestForFetch.body),
}), }, channelProxyUrl),
}) })
: await fetch(targetUrl, { : await fetch(targetUrl, {
method: 'POST', method: 'POST',
@@ -741,16 +742,17 @@ export async function geminiProxyRoute(app: FastifyInstance) {
runtime: endpointRequest.runtime, runtime: endpointRequest.runtime,
}; };
}; };
const channelProxyUrl = resolveChannelProxyUrl(selected.site, selected.account.extraConfig);
const dispatchRequest = (compatibilityRequest: BuiltEndpointRequest, targetUrl?: string) => ( const dispatchRequest = (compatibilityRequest: BuiltEndpointRequest, targetUrl?: string) => (
dispatchRuntimeRequest({ dispatchRuntimeRequest({
siteUrl: selected.site.url, siteUrl: selected.site.url,
targetUrl, targetUrl,
request: compatibilityRequest, request: compatibilityRequest,
buildInit: async (requestUrl, requestForFetch) => withSiteProxyRequestInit(requestUrl, { buildInit: async (_requestUrl, requestForFetch) => withSiteRecordProxyRequestInit(selected.site, {
method: 'POST', method: 'POST',
headers: requestForFetch.headers, headers: requestForFetch.headers,
body: JSON.stringify(requestForFetch.body), body: JSON.stringify(requestForFetch.body),
}), }, channelProxyUrl),
}) })
); );
const endpointStrategy = createChatEndpointStrategy({ const endpointStrategy = createChatEndpointStrategy({
@@ -304,6 +304,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
runtime: endpointRequest.runtime, runtime: endpointRequest.runtime,
}; };
}; };
const channelProxyUrl = resolveChannelProxyUrl(selected.site, selected.account.extraConfig);
const dispatchRequest = (compatibilityRequest: BuiltEndpointRequest, targetUrl?: string) => ( const dispatchRequest = (compatibilityRequest: BuiltEndpointRequest, targetUrl?: string) => (
dispatchRuntimeRequest({ dispatchRuntimeRequest({
siteUrl: selected.site.url, siteUrl: selected.site.url,
@@ -313,7 +314,7 @@ export async function handleOpenAiResponsesSurfaceRequest(
method: 'POST', method: 'POST',
headers: requestForFetch.headers, headers: requestForFetch.headers,
body: JSON.stringify(requestForFetch.body), body: JSON.stringify(requestForFetch.body),
}), }, channelProxyUrl),
}) })
); );
const endpointStrategy = openAiResponsesTransformer.compatibility.createEndpointStrategy({ const endpointStrategy = openAiResponsesTransformer.compatibility.createEndpointStrategy({
@@ -355,7 +356,6 @@ export async function handleOpenAiResponsesSurfaceRequest(
try { try {
const endpointResult = await executeEndpointFlow({ const endpointResult = await executeEndpointFlow({
siteUrl: selected.site.url, siteUrl: selected.site.url,
proxyUrl: resolveChannelProxyUrl(selected.site, selected.account.extraConfig),
endpointCandidates, endpointCandidates,
buildRequest: (endpoint) => buildEndpointRequest(endpoint), buildRequest: (endpoint) => buildEndpointRequest(endpoint),
dispatchRequest, dispatchRequest,
+5 -5
View File
@@ -201,7 +201,7 @@ async function refreshSub2ApiManagedSession(params: {
method: 'POST', method: 'POST',
headers, headers,
body: JSON.stringify({ refresh_token: refreshToken }), body: JSON.stringify({ refresh_token: refreshToken }),
})); }, getProxyUrlFromExtraConfig(params.account.extraConfig)));
payload = await response.json().catch(() => null); payload = await response.json().catch(() => null);
} catch (err: any) { } catch (err: any) {
throw new Error(err?.message || 'sub2api token refresh request failed'); throw new Error(err?.message || 'sub2api token refresh request failed');
@@ -382,23 +382,23 @@ export async function refreshBalance(accountId: number) {
let activeExtraConfig = account.extraConfig; let activeExtraConfig = account.extraConfig;
let balanceInfo: BalanceInfo | null = null; let balanceInfo: BalanceInfo | null = null;
const accountProxyUrl = getProxyUrlFromExtraConfig(account.extraConfig);
if (isSub2ApiPlatform(site.platform)) { if (isSub2ApiPlatform(site.platform)) {
const managedAuth = getSub2ApiAuthFromExtraConfig(activeExtraConfig); const managedAuth = getSub2ApiAuthFromExtraConfig(activeExtraConfig);
if (managedAuth?.refreshToken && isNearTokenExpiry(managedAuth.tokenExpiresAt)) { if (managedAuth?.refreshToken && isNearTokenExpiry(managedAuth.tokenExpiresAt)) {
try { try {
const refreshed = await refreshSub2ApiManagedSession({ const refreshed = await withAccountProxyOverride(accountProxyUrl, () => refreshSub2ApiManagedSession({
account, account,
site, site,
currentAccessToken: activeAccessToken, currentAccessToken: activeAccessToken,
currentExtraConfig: activeExtraConfig, currentExtraConfig: activeExtraConfig,
}); }));
activeAccessToken = refreshed.accessToken; activeAccessToken = refreshed.accessToken;
activeExtraConfig = refreshed.extraConfig; activeExtraConfig = refreshed.extraConfig;
} catch {} } catch {}
} }
} }
const accountProxyUrl = getProxyUrlFromExtraConfig(account.extraConfig);
const readBalance = async (token: string) => withAccountProxyOverride(accountProxyUrl, const readBalance = async (token: string) => withAccountProxyOverride(accountProxyUrl,
() => adapter.getBalance(site.url, token, platformUserId)); () => adapter.getBalance(site.url, token, platformUserId));
const handleBalanceError = async (err: any) => { const handleBalanceError = async (err: any) => {
+4 -2
View File
@@ -470,8 +470,10 @@ export function withSiteRecordProxyRequestInit(
nextOptions.headers = mergedHeaders; nextOptions.headers = mergedHeaders;
} }
const accountNormalized = normalizeSiteProxyUrl(accountProxyUrl); const accountNormalized = normalizeSiteProxyUrl(accountProxyUrl);
const proxyUrl = accountNormalized || resolveProxyUrlForSite(site); const siteProxyUrl = resolveProxyUrlForSite(site);
return withExplicitProxyRequestInit(proxyUrl, nextOptions, !!accountNormalized); const proxyUrl = accountNormalized || siteProxyUrl;
const isAccountOverride = !!accountNormalized && accountNormalized !== siteProxyUrl;
return withExplicitProxyRequestInit(proxyUrl, nextOptions, isAccountOverride);
} }
export function resolveChannelProxyUrl( export function resolveChannelProxyUrl(
+10 -14
View File
@@ -653,25 +653,23 @@ export default function Accounts() {
} }
}; };
const parseAccountExtraConfig = (account: any): Record<string, any> => {
try { return JSON.parse(account?.extraConfig || '{}') || {}; }
catch { return {}; }
};
const extractManagedSub2ApiAuth = (account: any) => { const extractManagedSub2ApiAuth = (account: any) => {
try { const parsed = parseAccountExtraConfig(account);
const parsed = JSON.parse(account?.extraConfig || '{}');
const auth = parsed?.sub2apiAuth || {}; const auth = parsed?.sub2apiAuth || {};
return { return {
refreshToken: typeof auth.refreshToken === 'string' ? auth.refreshToken : '', refreshToken: typeof auth.refreshToken === 'string' ? auth.refreshToken : '',
tokenExpiresAt: auth.tokenExpiresAt ? String(auth.tokenExpiresAt) : '', tokenExpiresAt: auth.tokenExpiresAt ? String(auth.tokenExpiresAt) : '',
}; };
} catch {
return { refreshToken: '', tokenExpiresAt: '' };
}
}; };
const openEditPanel = (account: any) => { const openEditPanel = (account: any) => {
const managedAuth = extractManagedSub2ApiAuth(account); const managedAuth = extractManagedSub2ApiAuth(account);
const proxyUrl = (() => { const proxyUrl = parseAccountExtraConfig(account)?.proxyUrl || '';
try { return JSON.parse(account?.extraConfig || '{}')?.proxyUrl || ''; }
catch { return ''; }
})();
closeAddPanel(); closeAddPanel();
setRebindTarget(null); setRebindTarget(null);
setEditingAccount(account); setEditingAccount(account);
@@ -793,12 +791,10 @@ export default function Accounts() {
}; };
const extractPlatformUserId = (account: any): string => { const extractPlatformUserId = (account: any): string => {
try { const parsed = parseAccountExtraConfig(account);
const parsed = JSON.parse(account?.extraConfig || '{}');
const raw = parsed?.platformUserId; const raw = parsed?.platformUserId;
const value = Number.parseInt(String(raw ?? ''), 10); const value = Number.parseInt(String(raw ?? ''), 10);
if (Number.isFinite(value) && value > 0) return String(value); if (Number.isFinite(value) && value > 0) return String(value);
} catch { }
const guessed = Number.parseInt(String(account?.username || '').match(/(\d{3,8})$/)?.[1] || '', 10); const guessed = Number.parseInt(String(account?.username || '').match(/(\d{3,8})$/)?.[1] || '', 10);
return Number.isFinite(guessed) && guessed > 0 ? String(guessed) : ''; return Number.isFinite(guessed) && guessed > 0 ? String(guessed) : '';
}; };
@@ -1605,7 +1601,7 @@ export default function Accounts() {
<span className={`badge ${connectionMode === 'apikey' ? 'badge-warning' : 'badge-info'}`} style={{ fontSize: 10 }}> <span className={`badge ${connectionMode === 'apikey' ? 'badge-warning' : 'badge-info'}`} style={{ fontSize: 10 }}>
{connectionMode === 'apikey' ? 'API Key' : 'Session'} {connectionMode === 'apikey' ? 'API Key' : 'Session'}
</span> </span>
{(() => { try { return JSON.parse(a.extraConfig || '{}')?.proxyUrl; } catch { return null; } })() && ( {parseAccountExtraConfig(a)?.proxyUrl && (
<span className="badge badge-purple" style={{ fontSize: 10 }}>代理</span> <span className="badge badge-purple" style={{ fontSize: 10 }}>代理</span>
)} )}
</div> </div>
@@ -1818,7 +1814,7 @@ export default function Accounts() {
<span className={`badge ${connectionMode === 'apikey' ? 'badge-warning' : 'badge-info'}`} style={{ fontSize: 10 }}> <span className={`badge ${connectionMode === 'apikey' ? 'badge-warning' : 'badge-info'}`} style={{ fontSize: 10 }}>
{connectionMode === 'apikey' ? 'API Key' : 'Session'} {connectionMode === 'apikey' ? 'API Key' : 'Session'}
</span> </span>
{(() => { try { return JSON.parse(a.extraConfig || '{}')?.proxyUrl; } catch { return null; } })() && ( {parseAccountExtraConfig(a)?.proxyUrl && (
<span className="badge badge-purple" style={{ fontSize: 10 }}>代理</span> <span className="badge badge-purple" style={{ fontSize: 10 }}>代理</span>
)} )}
</div> </div>