fix: recognize structured oauth accounts in routing (#369)
* fix: recognize structured oauth accounts in routing * fix: use structured oauth accounts in account list
This commit is contained in:
@@ -136,13 +136,13 @@ export function bindSurfaceStickyChannel(input: {
|
|||||||
stickySessionKey?: string | null;
|
stickySessionKey?: string | null;
|
||||||
selected: {
|
selected: {
|
||||||
channel: { id: number };
|
channel: { id: number };
|
||||||
account?: { extraConfig?: string | null } | null;
|
account?: { extraConfig?: string | null; oauthProvider?: string | null } | null;
|
||||||
};
|
};
|
||||||
}): void {
|
}): void {
|
||||||
proxyChannelCoordinator.bindStickyChannel(
|
proxyChannelCoordinator.bindStickyChannel(
|
||||||
input.stickySessionKey,
|
input.stickySessionKey,
|
||||||
input.selected.channel.id,
|
input.selected.channel.id,
|
||||||
input.selected.account?.extraConfig,
|
input.selected.account || undefined,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ export async function acquireSurfaceChannelLease(input: {
|
|||||||
stickySessionKey?: string | null;
|
stickySessionKey?: string | null;
|
||||||
selected: {
|
selected: {
|
||||||
channel: { id: number };
|
channel: { id: number };
|
||||||
account?: { extraConfig?: string | null } | null;
|
account?: { extraConfig?: string | null; oauthProvider?: string | null } | null;
|
||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
return await proxyChannelCoordinator.acquireChannelLease({
|
return await proxyChannelCoordinator.acquireChannelLease({
|
||||||
@@ -171,6 +171,7 @@ export async function acquireSurfaceChannelLease(input: {
|
|||||||
// the pre-sticky-session parallel behavior instead of contending globally.
|
// the pre-sticky-session parallel behavior instead of contending globally.
|
||||||
channelId: input.stickySessionKey ? input.selected.channel.id : 0,
|
channelId: input.stickySessionKey ? input.selected.channel.id : 0,
|
||||||
accountExtraConfig: input.selected.account?.extraConfig,
|
accountExtraConfig: input.selected.account?.extraConfig,
|
||||||
|
accountOauthProvider: input.selected.account?.oauthProvider,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -276,6 +276,68 @@ describe('accounts credential mode', { timeout: 15_000 }, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses structured oauth columns when listing oauth account capabilities and runtime health', async () => {
|
||||||
|
const site = await db.insert(schema.sites).values({
|
||||||
|
name: 'Structured Codex Site',
|
||||||
|
url: 'https://chatgpt.com/backend-api/codex',
|
||||||
|
platform: 'codex',
|
||||||
|
}).returning().get();
|
||||||
|
|
||||||
|
const account = await db.insert(schema.accounts).values({
|
||||||
|
siteId: site.id,
|
||||||
|
username: 'structured-oauth@example.com',
|
||||||
|
accessToken: 'oauth-access-token',
|
||||||
|
apiToken: null,
|
||||||
|
status: 'active',
|
||||||
|
checkinEnabled: false,
|
||||||
|
oauthProvider: 'codex',
|
||||||
|
oauthAccountKey: 'chatgpt-account-structured-123',
|
||||||
|
extraConfig: JSON.stringify({
|
||||||
|
credentialMode: 'session',
|
||||||
|
oauth: {
|
||||||
|
email: 'structured-oauth@example.com',
|
||||||
|
planType: 'team',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}).returning().get();
|
||||||
|
|
||||||
|
await db.insert(schema.modelAvailability).values({
|
||||||
|
accountId: account.id,
|
||||||
|
modelName: 'gpt-5.2-codex',
|
||||||
|
available: true,
|
||||||
|
checkedAt: '2026-04-01T12:00:00.000Z',
|
||||||
|
}).run();
|
||||||
|
|
||||||
|
const listResponse = await app.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/accounts',
|
||||||
|
});
|
||||||
|
expect(listResponse.statusCode).toBe(200);
|
||||||
|
|
||||||
|
const list = listResponse.json() as Array<{
|
||||||
|
id: number;
|
||||||
|
capabilities?: {
|
||||||
|
canCheckin?: boolean;
|
||||||
|
canRefreshBalance?: boolean;
|
||||||
|
proxyOnly?: boolean;
|
||||||
|
};
|
||||||
|
runtimeHealth?: {
|
||||||
|
state?: string;
|
||||||
|
reason?: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
const item = list.find((entry) => entry.id === account.id);
|
||||||
|
expect(item?.capabilities).toMatchObject({
|
||||||
|
canCheckin: false,
|
||||||
|
canRefreshBalance: false,
|
||||||
|
proxyOnly: true,
|
||||||
|
});
|
||||||
|
expect(item?.runtimeHealth).toMatchObject({
|
||||||
|
state: 'healthy',
|
||||||
|
reason: '模型探测成功',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('stores managed refresh token for sub2api session account', async () => {
|
it('stores managed refresh token for sub2api session account', async () => {
|
||||||
verifyTokenMock.mockResolvedValueOnce({
|
verifyTokenMock.mockResolvedValueOnce({
|
||||||
tokenType: 'session',
|
tokenType: 'session',
|
||||||
|
|||||||
@@ -105,9 +105,9 @@ function resolveStoredCredentialMode(account: typeof schema.accounts.$inferSelec
|
|||||||
function buildCapabilitiesFromCredentialMode(
|
function buildCapabilitiesFromCredentialMode(
|
||||||
credentialMode: AccountCredentialMode,
|
credentialMode: AccountCredentialMode,
|
||||||
hasSessionToken: boolean,
|
hasSessionToken: boolean,
|
||||||
extraConfig?: string | null,
|
oauthIdentity?: string | null | Pick<typeof schema.accounts.$inferSelect, 'extraConfig' | 'oauthProvider'>,
|
||||||
): AccountCapabilities {
|
): AccountCapabilities {
|
||||||
if (hasOauthProvider(extraConfig)) {
|
if (hasOauthProvider(oauthIdentity)) {
|
||||||
return {
|
return {
|
||||||
canCheckin: false,
|
canCheckin: false,
|
||||||
canRefreshBalance: false,
|
canRefreshBalance: false,
|
||||||
@@ -126,7 +126,7 @@ function buildCapabilitiesFromCredentialMode(
|
|||||||
|
|
||||||
function buildCapabilitiesForAccount(account: typeof schema.accounts.$inferSelect): AccountCapabilities {
|
function buildCapabilitiesForAccount(account: typeof schema.accounts.$inferSelect): AccountCapabilities {
|
||||||
const credentialMode = resolveStoredCredentialMode(account);
|
const credentialMode = resolveStoredCredentialMode(account);
|
||||||
return buildCapabilitiesFromCredentialMode(credentialMode, hasSessionTokenValue(account.accessToken), account.extraConfig);
|
return buildCapabilitiesFromCredentialMode(credentialMode, hasSessionTokenValue(account.accessToken), account);
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeBatchIds(input: unknown): number[] {
|
function normalizeBatchIds(input: unknown): number[] {
|
||||||
@@ -498,15 +498,12 @@ export async function accountsRoutes(app: FastifyInstance) {
|
|||||||
|
|
||||||
return rows.map((r) => {
|
return rows.map((r) => {
|
||||||
const credentialMode = resolveStoredCredentialMode(r.accounts);
|
const credentialMode = resolveStoredCredentialMode(r.accounts);
|
||||||
|
const capabilities = buildCapabilitiesForAccount(r.accounts);
|
||||||
return {
|
return {
|
||||||
...r.accounts,
|
...r.accounts,
|
||||||
site: r.sites,
|
site: r.sites,
|
||||||
credentialMode,
|
credentialMode,
|
||||||
capabilities: buildCapabilitiesFromCredentialMode(
|
capabilities,
|
||||||
credentialMode,
|
|
||||||
hasSessionTokenValue(r.accounts.accessToken),
|
|
||||||
r.accounts.extraConfig,
|
|
||||||
),
|
|
||||||
todaySpend: Math.round((spendByAccount[r.accounts.id] || 0) * 1_000_000) / 1_000_000,
|
todaySpend: Math.round((spendByAccount[r.accounts.id] || 0) * 1_000_000) / 1_000_000,
|
||||||
todayReward: Math.round(estimateRewardWithTodayIncomeFallback({
|
todayReward: Math.round(estimateRewardWithTodayIncomeFallback({
|
||||||
day: localDay,
|
day: localDay,
|
||||||
@@ -519,11 +516,7 @@ export async function accountsRoutes(app: FastifyInstance) {
|
|||||||
accountStatus: r.accounts.status,
|
accountStatus: r.accounts.status,
|
||||||
siteStatus: r.sites.status,
|
siteStatus: r.sites.status,
|
||||||
extraConfig: r.accounts.extraConfig,
|
extraConfig: r.accounts.extraConfig,
|
||||||
sessionCapable: buildCapabilitiesFromCredentialMode(
|
sessionCapable: capabilities.canRefreshBalance,
|
||||||
credentialMode,
|
|
||||||
hasSessionTokenValue(r.accounts.accessToken),
|
|
||||||
r.accounts.extraConfig,
|
|
||||||
).canRefreshBalance,
|
|
||||||
hasDiscoveredModels: (modelCountByAccount[r.accounts.id] || 0) > 0,
|
hasDiscoveredModels: (modelCountByAccount[r.accounts.id] || 0) > 0,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
|
|||||||
import {
|
import {
|
||||||
buildStoredSub2ApiSubscriptionSummary,
|
buildStoredSub2ApiSubscriptionSummary,
|
||||||
getCredentialModeFromExtraConfig,
|
getCredentialModeFromExtraConfig,
|
||||||
|
hasOauthProvider,
|
||||||
getPlatformUserIdFromExtraConfig,
|
getPlatformUserIdFromExtraConfig,
|
||||||
getProxyUrlFromExtraConfig,
|
getProxyUrlFromExtraConfig,
|
||||||
getSub2ApiAuthFromExtraConfig,
|
getSub2ApiAuthFromExtraConfig,
|
||||||
@@ -145,6 +146,24 @@ describe('accountExtraConfig', () => {
|
|||||||
})).toBe(true);
|
})).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('recognizes structured oauth provider columns even when extraConfig omits oauth.provider', () => {
|
||||||
|
const structuredOauthAccount = {
|
||||||
|
oauthProvider: 'codex',
|
||||||
|
accessToken: 'oauth-access-token',
|
||||||
|
apiToken: null,
|
||||||
|
extraConfig: JSON.stringify({
|
||||||
|
credentialMode: 'session',
|
||||||
|
oauth: {
|
||||||
|
email: 'oauth-user@example.com',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(hasOauthProvider(structuredOauthAccount)).toBe(true);
|
||||||
|
expect(supportsDirectAccountRoutingConnection(structuredOauthAccount)).toBe(true);
|
||||||
|
expect(requiresManagedAccountTokens(structuredOauthAccount)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('parses stored sub2api subscription summary from extra config', () => {
|
it('parses stored sub2api subscription summary from extra config', () => {
|
||||||
const extraConfig = mergeAccountExtraConfig(null, {
|
const extraConfig = mergeAccountExtraConfig(null, {
|
||||||
sub2apiSubscription: buildStoredSub2ApiSubscriptionSummary({
|
sub2apiSubscription: buildStoredSub2ApiSubscriptionSummary({
|
||||||
|
|||||||
@@ -40,11 +40,20 @@ type AccountExtraConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type ExtraConfigInput = string | Record<string, unknown> | null | undefined;
|
type ExtraConfigInput = string | Record<string, unknown> | null | undefined;
|
||||||
|
type OauthProviderCarrier = {
|
||||||
|
extraConfig?: ExtraConfigInput;
|
||||||
|
oauthProvider?: unknown;
|
||||||
|
};
|
||||||
|
type OauthProviderInput = ExtraConfigInput | OauthProviderCarrier;
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
return !!value && typeof value === 'object' && !Array.isArray(value);
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isOauthProviderCarrier(value: unknown): value is OauthProviderCarrier {
|
||||||
|
return isRecord(value) && ('extraConfig' in value || 'oauthProvider' in value);
|
||||||
|
}
|
||||||
|
|
||||||
function parseExtraConfig(extraConfig?: ExtraConfigInput): AccountExtraConfig {
|
function parseExtraConfig(extraConfig?: ExtraConfigInput): AccountExtraConfig {
|
||||||
if (!extraConfig) return {};
|
if (!extraConfig) return {};
|
||||||
if (isRecord(extraConfig)) return extraConfig as AccountExtraConfig;
|
if (isRecord(extraConfig)) return extraConfig as AccountExtraConfig;
|
||||||
@@ -142,14 +151,23 @@ export function getOauthProviderFromExtraConfig(extraConfig?: ExtraConfigInput):
|
|||||||
return normalizeNonEmptyString(parsed.oauth?.provider);
|
return normalizeNonEmptyString(parsed.oauth?.provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasOauthProvider(extraConfig?: ExtraConfigInput): boolean {
|
function getOauthProvider(input?: OauthProviderInput): string | undefined {
|
||||||
return !!getOauthProviderFromExtraConfig(extraConfig);
|
if (!isOauthProviderCarrier(input)) {
|
||||||
|
return getOauthProviderFromExtraConfig(input);
|
||||||
|
}
|
||||||
|
return normalizeNonEmptyString(input.oauthProvider)
|
||||||
|
?? getOauthProviderFromExtraConfig(input.extraConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasOauthProvider(input?: OauthProviderInput): boolean {
|
||||||
|
return !!getOauthProvider(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
type DirectAccountRoutingInput = {
|
type DirectAccountRoutingInput = {
|
||||||
accessToken?: string | null;
|
accessToken?: string | null;
|
||||||
apiToken?: string | null;
|
apiToken?: string | null;
|
||||||
extraConfig?: ExtraConfigInput;
|
extraConfig?: ExtraConfigInput;
|
||||||
|
oauthProvider?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function hasCredentialValue(value: string | null | undefined): boolean {
|
function hasCredentialValue(value: string | null | undefined): boolean {
|
||||||
@@ -158,7 +176,7 @@ function hasCredentialValue(value: string | null | undefined): boolean {
|
|||||||
|
|
||||||
export function supportsDirectAccountRoutingConnection(account: DirectAccountRoutingInput): boolean {
|
export function supportsDirectAccountRoutingConnection(account: DirectAccountRoutingInput): boolean {
|
||||||
const credentialMode = getCredentialModeFromExtraConfig(account.extraConfig);
|
const credentialMode = getCredentialModeFromExtraConfig(account.extraConfig);
|
||||||
if (hasOauthProvider(account.extraConfig)) {
|
if (hasOauthProvider(account)) {
|
||||||
return hasCredentialValue(account.accessToken) || hasCredentialValue(account.apiToken);
|
return hasCredentialValue(account.accessToken) || hasCredentialValue(account.apiToken);
|
||||||
}
|
}
|
||||||
if (credentialMode === 'apikey') {
|
if (credentialMode === 'apikey') {
|
||||||
@@ -173,7 +191,7 @@ export function supportsDirectAccountRoutingConnection(account: DirectAccountRou
|
|||||||
|
|
||||||
export function requiresManagedAccountTokens(account: DirectAccountRoutingInput): boolean {
|
export function requiresManagedAccountTokens(account: DirectAccountRoutingInput): boolean {
|
||||||
const credentialMode = getCredentialModeFromExtraConfig(account.extraConfig);
|
const credentialMode = getCredentialModeFromExtraConfig(account.extraConfig);
|
||||||
if (hasOauthProvider(account.extraConfig)) return false;
|
if (hasOauthProvider(account)) return false;
|
||||||
if (credentialMode === 'apikey') return false;
|
if (credentialMode === 'apikey') return false;
|
||||||
if (credentialMode === 'session') return true;
|
if (credentialMode === 'session') return true;
|
||||||
if (hasCredentialValue(account.apiToken) && !hasCredentialValue(account.accessToken)) return false;
|
if (hasCredentialValue(account.apiToken) && !hasCredentialValue(account.accessToken)) return false;
|
||||||
|
|||||||
@@ -199,6 +199,59 @@ describe('rebuildTokenRoutesFromAvailability', () => {
|
|||||||
expect(channels[0]?.manualOverride).toBe(false);
|
expect(channels[0]?.manualOverride).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('creates an exact route with an account-direct channel for oauth accounts stored via structured identity columns', async () => {
|
||||||
|
const site = await db.insert(schema.sites).values({
|
||||||
|
name: 'codex-site-structured',
|
||||||
|
url: 'https://chatgpt.com/backend-api/codex',
|
||||||
|
platform: 'codex',
|
||||||
|
}).returning().get();
|
||||||
|
|
||||||
|
const account = await db.insert(schema.accounts).values({
|
||||||
|
siteId: site.id,
|
||||||
|
username: 'codex-structured@example.com',
|
||||||
|
accessToken: 'oauth-access-token',
|
||||||
|
apiToken: null,
|
||||||
|
status: 'active',
|
||||||
|
oauthProvider: 'codex',
|
||||||
|
oauthAccountKey: 'chatgpt-account-structured-123',
|
||||||
|
extraConfig: JSON.stringify({
|
||||||
|
credentialMode: 'session',
|
||||||
|
oauth: {
|
||||||
|
email: 'codex-structured@example.com',
|
||||||
|
planType: 'team',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}).returning().get();
|
||||||
|
|
||||||
|
await db.insert(schema.modelAvailability).values({
|
||||||
|
accountId: account.id,
|
||||||
|
modelName: 'gpt-5.2-codex',
|
||||||
|
available: true,
|
||||||
|
latencyMs: 320,
|
||||||
|
checkedAt: '2026-04-01T00:00:00.000Z',
|
||||||
|
}).run();
|
||||||
|
|
||||||
|
const rebuild = await rebuildTokenRoutesFromAvailability();
|
||||||
|
|
||||||
|
expect(rebuild.models).toBe(1);
|
||||||
|
|
||||||
|
const route = await db.select().from(schema.tokenRoutes)
|
||||||
|
.where(eq(schema.tokenRoutes.modelPattern, 'gpt-5.2-codex'))
|
||||||
|
.get();
|
||||||
|
expect(route).toBeDefined();
|
||||||
|
|
||||||
|
const channels = await db.select().from(schema.routeChannels)
|
||||||
|
.where(and(
|
||||||
|
eq(schema.routeChannels.routeId, route!.id),
|
||||||
|
eq(schema.routeChannels.accountId, account.id),
|
||||||
|
))
|
||||||
|
.all();
|
||||||
|
|
||||||
|
expect(channels).toHaveLength(1);
|
||||||
|
expect(channels[0]?.tokenId ?? null).toBeNull();
|
||||||
|
expect(channels[0]?.manualOverride).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('removes stale exact routes and keeps wildcard routes on rebuild', async () => {
|
it('removes stale exact routes and keeps wildcard routes on rebuild', async () => {
|
||||||
const site = await db.insert(schema.sites).values({
|
const site = await db.insert(schema.sites).values({
|
||||||
name: 'site-1',
|
name: 'site-1',
|
||||||
|
|||||||
@@ -64,6 +64,22 @@ describe('proxyChannelCoordinator', () => {
|
|||||||
expect(proxyChannelCoordinator.getStickyChannelId(key)).toBeNull();
|
expect(proxyChannelCoordinator.getStickyChannelId(key)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('treats structured oauth providers as session-scoped even when extraConfig omits oauth.provider', () => {
|
||||||
|
const key = proxyChannelCoordinator.buildStickySessionKey({
|
||||||
|
clientKind: 'codex',
|
||||||
|
sessionId: 'turn-oauth-structured',
|
||||||
|
requestedModel: 'gpt-5.2',
|
||||||
|
downstreamPath: '/v1/responses',
|
||||||
|
downstreamApiKeyId: 9,
|
||||||
|
});
|
||||||
|
|
||||||
|
proxyChannelCoordinator.bindStickyChannel(key, 42, {
|
||||||
|
oauthProvider: 'codex',
|
||||||
|
extraConfig: JSON.stringify({ credentialMode: 'session' }),
|
||||||
|
});
|
||||||
|
expect(proxyChannelCoordinator.getStickyChannelId(key)).toBe(42);
|
||||||
|
});
|
||||||
|
|
||||||
it('queues requests behind the active lease and grants the next waiter after release', async () => {
|
it('queues requests behind the active lease and grants the next waiter after release', async () => {
|
||||||
const first = await proxyChannelCoordinator.acquireChannelLease({
|
const first = await proxyChannelCoordinator.acquireChannelLease({
|
||||||
channelId: 11,
|
channelId: 11,
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ export type AcquireProxyChannelLeaseResult =
|
|||||||
const stickySessionBindings = new Map<string, StickyEntry>();
|
const stickySessionBindings = new Map<string, StickyEntry>();
|
||||||
const channelRuntimeStates = new Map<number, ChannelRuntimeState>();
|
const channelRuntimeStates = new Map<number, ChannelRuntimeState>();
|
||||||
let nextLeaseId = 1;
|
let nextLeaseId = 1;
|
||||||
|
type SessionScopedChannelInput =
|
||||||
|
| string
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
| {
|
||||||
|
extraConfig?: string | null;
|
||||||
|
oauthProvider?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
function shouldUnrefTimer(timer: ReturnType<typeof setTimeout> | ReturnType<typeof setInterval>) {
|
function shouldUnrefTimer(timer: ReturnType<typeof setTimeout> | ReturnType<typeof setInterval>) {
|
||||||
if (typeof (timer as { unref?: () => void }).unref === 'function') {
|
if (typeof (timer as { unref?: () => void }).unref === 'function') {
|
||||||
@@ -53,9 +61,14 @@ function cleanupExpiredStickyBindings(nowMs = Date.now()): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSessionScopedChannel(extraConfig?: string | null): boolean {
|
function getSessionScopedExtraConfig(input?: SessionScopedChannelInput): string | null | undefined {
|
||||||
return getCredentialModeFromExtraConfig(extraConfig) === 'session'
|
if (typeof input === 'string' || input == null) return input;
|
||||||
|| hasOauthProvider(extraConfig);
|
return input.extraConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSessionScopedChannel(input?: SessionScopedChannelInput): boolean {
|
||||||
|
return getCredentialModeFromExtraConfig(getSessionScopedExtraConfig(input)) === 'session'
|
||||||
|
|| hasOauthProvider(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStickySessionTtlMs(): number {
|
function getStickySessionTtlMs(): number {
|
||||||
@@ -74,8 +87,8 @@ function getChannelQueueWaitMs(): number {
|
|||||||
return Math.max(0, Math.trunc(config.proxySessionChannelQueueWaitMs || 0));
|
return Math.max(0, Math.trunc(config.proxySessionChannelQueueWaitMs || 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getChannelConcurrencyLimit(extraConfig?: string | null): number {
|
function getChannelConcurrencyLimit(input?: SessionScopedChannelInput): number {
|
||||||
if (!isSessionScopedChannel(extraConfig)) return 0;
|
if (!isSessionScopedChannel(input)) return 0;
|
||||||
return Math.max(0, Math.trunc(config.proxySessionChannelConcurrencyLimit || 0));
|
return Math.max(0, Math.trunc(config.proxySessionChannelConcurrencyLimit || 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,9 +154,9 @@ class ProxyChannelCoordinator {
|
|||||||
return entry.channelId;
|
return entry.channelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
bindStickyChannel(stickySessionKey: string | null | undefined, channelId: number, extraConfig?: string | null): void {
|
bindStickyChannel(stickySessionKey: string | null | undefined, channelId: number, accountIdentity?: SessionScopedChannelInput): void {
|
||||||
if (!config.proxyStickySessionEnabled) return;
|
if (!config.proxyStickySessionEnabled) return;
|
||||||
if (!isSessionScopedChannel(extraConfig)) return;
|
if (!isSessionScopedChannel(accountIdentity)) return;
|
||||||
const normalizedKey = String(stickySessionKey || '').trim();
|
const normalizedKey = String(stickySessionKey || '').trim();
|
||||||
if (!normalizedKey || !Number.isFinite(channelId) || channelId <= 0) return;
|
if (!normalizedKey || !Number.isFinite(channelId) || channelId <= 0) return;
|
||||||
cleanupExpiredStickyBindings();
|
cleanupExpiredStickyBindings();
|
||||||
@@ -167,6 +180,7 @@ class ProxyChannelCoordinator {
|
|||||||
async acquireChannelLease(input: {
|
async acquireChannelLease(input: {
|
||||||
channelId: number;
|
channelId: number;
|
||||||
accountExtraConfig?: string | null;
|
accountExtraConfig?: string | null;
|
||||||
|
accountOauthProvider?: string | null;
|
||||||
}): Promise<AcquireProxyChannelLeaseResult> {
|
}): Promise<AcquireProxyChannelLeaseResult> {
|
||||||
const channelId = Math.trunc(input.channelId || 0);
|
const channelId = Math.trunc(input.channelId || 0);
|
||||||
if (channelId <= 0) {
|
if (channelId <= 0) {
|
||||||
@@ -176,7 +190,10 @@ class ProxyChannelCoordinator {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const concurrencyLimit = getChannelConcurrencyLimit(input.accountExtraConfig);
|
const concurrencyLimit = getChannelConcurrencyLimit({
|
||||||
|
extraConfig: input.accountExtraConfig,
|
||||||
|
oauthProvider: input.accountOauthProvider,
|
||||||
|
});
|
||||||
if (concurrencyLimit <= 0) {
|
if (concurrencyLimit <= 0) {
|
||||||
return {
|
return {
|
||||||
status: 'acquired',
|
status: 'acquired',
|
||||||
@@ -288,8 +305,8 @@ export function resetProxyChannelCoordinatorState(): void {
|
|||||||
nextLeaseId = 1;
|
nextLeaseId = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isProxyChannelSessionScoped(extraConfig?: string | null): boolean {
|
export function isProxyChannelSessionScoped(input?: SessionScopedChannelInput): boolean {
|
||||||
return isSessionScopedChannel(extraConfig);
|
return isSessionScopedChannel(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const proxyChannelCoordinator = new ProxyChannelCoordinator();
|
export const proxyChannelCoordinator = new ProxyChannelCoordinator();
|
||||||
|
|||||||
Reference in New Issue
Block a user