forked from boos/BoosAPI
refactor: centralize oauth identity and route refresh workflow
This commit is contained in:
@@ -1537,6 +1537,69 @@ describe('oauth routes', { timeout: 15_000 }, () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('backfills structured oauth identity columns for legacy oauth rows before listing connections', async () => {
|
||||
const site = await db.insert(schema.sites).values({
|
||||
name: 'Legacy Codex',
|
||||
url: 'https://codex.example.com',
|
||||
platform: 'codex',
|
||||
status: 'active',
|
||||
useSystemProxy: false,
|
||||
isPinned: false,
|
||||
globalWeight: 1,
|
||||
sortOrder: 0,
|
||||
}).returning().get();
|
||||
|
||||
const account = await db.insert(schema.accounts).values({
|
||||
siteId: site.id,
|
||||
username: 'legacy-user@example.com',
|
||||
accessToken: 'legacy-oauth-access-token',
|
||||
status: 'active',
|
||||
extraConfig: JSON.stringify({
|
||||
oauth: {
|
||||
provider: 'codex',
|
||||
accountKey: 'legacy-chatgpt-account',
|
||||
projectId: 'legacy-project',
|
||||
refreshToken: 'legacy-refresh-token',
|
||||
modelDiscoveryStatus: 'healthy',
|
||||
},
|
||||
}),
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
}).returning().get();
|
||||
|
||||
expect(account.oauthProvider).toBeNull();
|
||||
expect(account.oauthAccountKey).toBeNull();
|
||||
expect(account.oauthProjectId).toBeNull();
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/oauth/connections',
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.json()).toMatchObject({
|
||||
total: 1,
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
provider: 'codex',
|
||||
accountKey: 'legacy-chatgpt-account',
|
||||
projectId: 'legacy-project',
|
||||
username: 'legacy-user@example.com',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const backfilled = await db.select().from(schema.accounts)
|
||||
.where(eq(schema.accounts.id, account.id))
|
||||
.get();
|
||||
|
||||
expect(backfilled).toEqual(expect.objectContaining({
|
||||
oauthProvider: 'codex',
|
||||
oauthAccountKey: 'legacy-chatgpt-account',
|
||||
oauthProjectId: 'legacy-project',
|
||||
}));
|
||||
});
|
||||
|
||||
it('rejects malformed manual callback submissions', async () => {
|
||||
const startResponse = await app.inject({
|
||||
method: 'POST',
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
function readSource(relativePath: string): string {
|
||||
return readFileSync(new URL(relativePath, import.meta.url), 'utf8');
|
||||
}
|
||||
|
||||
function expectNoDirectModelServiceRouteRefresh(source: string): void {
|
||||
expect(source).not.toMatch(/import\s*\{[^}]*\brefreshModelsAndRebuildRoutes\b[^}]*\}\s*from\s*['"][^'"]*modelService\.js['"]/m);
|
||||
expect(source).not.toMatch(/import\s*\{[^}]*\brebuildTokenRoutesFromAvailability\b[^}]*\}\s*from\s*['"][^'"]*modelService\.js['"]/m);
|
||||
}
|
||||
|
||||
describe('route refresh workflow architecture boundaries', () => {
|
||||
it('keeps api controllers on the shared route refresh workflow instead of modelService', () => {
|
||||
const tokensSource = readSource('./tokens.ts');
|
||||
const settingsSource = readSource('./settings.ts');
|
||||
const statsSource = readSource('./stats.ts');
|
||||
|
||||
expect(tokensSource).toContain("from '../../services/routeRefreshWorkflow.js'");
|
||||
expect(tokensSource).not.toContain("from '../../services/modelService.js'");
|
||||
|
||||
expect(settingsSource).toContain("from '../../services/routeRefreshWorkflow.js'");
|
||||
expect(settingsSource).not.toContain("from '../../services/modelService.js'");
|
||||
|
||||
expect(statsSource).toContain("from '../../services/routeRefreshWorkflow.js'");
|
||||
expectNoDirectModelServiceRouteRefresh(statsSource);
|
||||
});
|
||||
|
||||
it('keeps proxy fallback refreshes and scheduler hooks on the route refresh workflow', () => {
|
||||
const completionsSource = readSource('../proxy/completions.ts');
|
||||
const embeddingsSource = readSource('../proxy/embeddings.ts');
|
||||
const imagesSource = readSource('../proxy/images.ts');
|
||||
const modelsRouteSource = readSource('../proxy/models.ts');
|
||||
const searchSource = readSource('../proxy/search.ts');
|
||||
const videosSource = readSource('../proxy/videos.ts');
|
||||
const schedulerSource = readSource('../../services/checkinScheduler.ts');
|
||||
const oauthServiceSource = readSource('../../services/oauth/service.ts');
|
||||
const sharedSurfaceSource = readSource('../../proxy-core/surfaces/sharedSurface.ts');
|
||||
const geminiSurfaceSource = readSource('../../proxy-core/surfaces/geminiSurface.ts');
|
||||
|
||||
for (const source of [
|
||||
completionsSource,
|
||||
embeddingsSource,
|
||||
imagesSource,
|
||||
modelsRouteSource,
|
||||
searchSource,
|
||||
videosSource,
|
||||
schedulerSource,
|
||||
oauthServiceSource,
|
||||
sharedSurfaceSource,
|
||||
geminiSurfaceSource,
|
||||
]) {
|
||||
expect(source).toContain('routeRefreshWorkflow');
|
||||
expectNoDirectModelServiceRouteRefresh(source);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { fetch } from 'undici';
|
||||
import { config } from '../../config.js';
|
||||
import { db, runtimeDbDialect, schema } from '../../db/index.js';
|
||||
import { upsertSetting } from '../../db/upsertSetting.js';
|
||||
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
|
||||
import * as routeRefreshWorkflow from '../../services/routeRefreshWorkflow.js';
|
||||
import { updateBalanceRefreshCron, updateCheckinSchedule, updateLogCleanupSettings } from '../../services/checkinScheduler.js';
|
||||
import { sendNotification } from '../../services/notifyService.js';
|
||||
import {
|
||||
@@ -1404,7 +1404,7 @@ export async function settingsRoutes(app: FastifyInstance) {
|
||||
},
|
||||
failureMessage: (currentTask) => `缓存清理后重建失败:${currentTask.error || 'unknown error'}`,
|
||||
},
|
||||
async () => refreshModelsAndRebuildRoutes(),
|
||||
async () => routeRefreshWorkflow.refreshModelsAndRebuildRoutes(),
|
||||
);
|
||||
|
||||
return reply.code(202).send({
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { db, schema } from '../../db/index.js';
|
||||
import { and, desc, eq, gte, lt, sql } from 'drizzle-orm';
|
||||
import {
|
||||
refreshModelsForAccount,
|
||||
refreshModelsAndRebuildRoutes,
|
||||
rebuildTokenRoutesFromAvailability,
|
||||
} from '../../services/modelService.js';
|
||||
import { refreshModelsForAccount } from '../../services/modelService.js';
|
||||
import * as routeRefreshWorkflow from '../../services/routeRefreshWorkflow.js';
|
||||
import { buildModelAnalysis } from '../../services/modelAnalysisService.js';
|
||||
import { fallbackTokenCost, fetchModelPricingCatalog } from '../../services/modelPricingService.js';
|
||||
import { getUpstreamModelDescriptionsCached } from '../../services/upstreamModelDescriptionService.js';
|
||||
@@ -847,7 +844,7 @@ export async function statsRoutes(app: FastifyInstance) {
|
||||
},
|
||||
failureMessage: (currentTask) => `模型广场刷新失败:${currentTask.error || 'unknown error'}`,
|
||||
},
|
||||
async () => refreshModelsAndRebuildRoutes(),
|
||||
async () => routeRefreshWorkflow.refreshModelsAndRebuildRoutes(),
|
||||
);
|
||||
refreshQueued = !reused;
|
||||
refreshReused = reused;
|
||||
@@ -1390,7 +1387,7 @@ export async function statsRoutes(app: FastifyInstance) {
|
||||
}
|
||||
|
||||
const refresh = await refreshModelsForAccount(accountId);
|
||||
const rebuild = rebuildTokenRoutesFromAvailability();
|
||||
const rebuild = routeRefreshWorkflow.rebuildRoutesOnly();
|
||||
return { success: true, refresh, rebuild };
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { and, eq, inArray } from 'drizzle-orm';
|
||||
import { db, schema } from '../../db/index.js';
|
||||
import { rebuildTokenRoutesFromAvailability, refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
|
||||
import * as routeRefreshWorkflow from '../../services/routeRefreshWorkflow.js';
|
||||
import {
|
||||
ACCOUNT_TOKEN_VALUE_STATUS_READY,
|
||||
isUsableAccountToken,
|
||||
@@ -1170,12 +1170,12 @@ export async function tokensRoutes(app: FastifyInstance) {
|
||||
app.post<{ Body?: { refreshModels?: boolean; wait?: boolean } }>('/api/routes/rebuild', async (request, reply) => {
|
||||
const body = (request.body || {}) as { refreshModels?: boolean };
|
||||
if (body.refreshModels === false) {
|
||||
const rebuild = rebuildTokenRoutesFromAvailability();
|
||||
const rebuild = routeRefreshWorkflow.rebuildRoutesOnly();
|
||||
return { success: true, rebuild };
|
||||
}
|
||||
|
||||
if ((request.body as { wait?: boolean } | undefined)?.wait) {
|
||||
const result = await refreshModelsAndRebuildRoutes();
|
||||
const result = await routeRefreshWorkflow.refreshModelsAndRebuildRoutes();
|
||||
return { success: true, ...result };
|
||||
}
|
||||
|
||||
@@ -1192,8 +1192,8 @@ export async function tokensRoutes(app: FastifyInstance) {
|
||||
},
|
||||
failureMessage: (currentTask) => `刷新模型并重建路由失败:${currentTask.error || 'unknown error'}`,
|
||||
},
|
||||
async () => refreshModelsAndRebuildRoutes(),
|
||||
);
|
||||
async () => routeRefreshWorkflow.refreshModelsAndRebuildRoutes(),
|
||||
);
|
||||
|
||||
return reply.code(202).send({
|
||||
success: true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { fetch } from 'undici';
|
||||
import { tokenRouter } from '../../services/tokenRouter.js';
|
||||
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
|
||||
import * as routeRefreshWorkflow from '../../services/routeRefreshWorkflow.js';
|
||||
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
|
||||
import { isTokenExpiredError } from '../../services/alertRules.js';
|
||||
import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
|
||||
@@ -48,7 +48,7 @@ export async function completionsProxyRoute(app: FastifyInstance) {
|
||||
: await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy);
|
||||
|
||||
if (!selected && retryCount === 0) {
|
||||
await refreshModelsAndRebuildRoutes();
|
||||
await routeRefreshWorkflow.refreshModelsAndRebuildRoutes();
|
||||
selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { fetch } from 'undici';
|
||||
import { tokenRouter } from '../../services/tokenRouter.js';
|
||||
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
|
||||
import * as routeRefreshWorkflow from '../../services/routeRefreshWorkflow.js';
|
||||
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
|
||||
import { isTokenExpiredError } from '../../services/alertRules.js';
|
||||
import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
|
||||
@@ -46,7 +46,7 @@ export async function embeddingsProxyRoute(app: FastifyInstance) {
|
||||
: await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy);
|
||||
|
||||
if (!selected && retryCount === 0) {
|
||||
await refreshModelsAndRebuildRoutes();
|
||||
await routeRefreshWorkflow.refreshModelsAndRebuildRoutes();
|
||||
selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { fetch } from 'undici';
|
||||
import { tokenRouter } from '../../services/tokenRouter.js';
|
||||
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
|
||||
import * as routeRefreshWorkflow from '../../services/routeRefreshWorkflow.js';
|
||||
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
|
||||
import { isTokenExpiredError } from '../../services/alertRules.js';
|
||||
import { estimateProxyCost } from '../../services/modelPricingService.js';
|
||||
@@ -43,7 +43,7 @@ export async function imagesProxyRoute(app: FastifyInstance) {
|
||||
: await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy);
|
||||
|
||||
if (!selected && retryCount === 0) {
|
||||
await refreshModelsAndRebuildRoutes();
|
||||
await routeRefreshWorkflow.refreshModelsAndRebuildRoutes();
|
||||
selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy);
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ export async function imagesProxyRoute(app: FastifyInstance) {
|
||||
: await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy);
|
||||
|
||||
if (!selected && retryCount === 0) {
|
||||
await refreshModelsAndRebuildRoutes();
|
||||
await routeRefreshWorkflow.refreshModelsAndRebuildRoutes();
|
||||
selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { listModelsSurface } from '../../proxy-core/surfaces/modelsSurface.js';
|
||||
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
|
||||
import * as routeRefreshWorkflow from '../../services/routeRefreshWorkflow.js';
|
||||
import { getDownstreamRoutingPolicy } from './downstreamPolicy.js';
|
||||
import { tokenRouter } from '../../services/tokenRouter.js';
|
||||
import { isModelAllowedByPolicyOrAllowedRoutes } from '../../services/downstreamApiKeyService.js';
|
||||
@@ -14,7 +14,7 @@ export async function modelsProxyRoute(app: FastifyInstance) {
|
||||
downstreamPolicy,
|
||||
responseFormat: wantsClaudeFormat ? 'claude' : 'openai',
|
||||
tokenRouter,
|
||||
refreshModelsAndRebuildRoutes,
|
||||
refreshModelsAndRebuildRoutes: routeRefreshWorkflow.refreshModelsAndRebuildRoutes,
|
||||
isModelAllowed: isModelAllowedByPolicyOrAllowedRoutes,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { fetch } from 'undici';
|
||||
import { tokenRouter } from '../../services/tokenRouter.js';
|
||||
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
|
||||
import * as routeRefreshWorkflow from '../../services/routeRefreshWorkflow.js';
|
||||
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
|
||||
import { isTokenExpiredError } from '../../services/alertRules.js';
|
||||
import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js';
|
||||
@@ -71,7 +71,7 @@ export async function searchProxyRoute(app: FastifyInstance) {
|
||||
: await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy);
|
||||
|
||||
if (!selected && retryCount === 0) {
|
||||
await refreshModelsAndRebuildRoutes();
|
||||
await routeRefreshWorkflow.refreshModelsAndRebuildRoutes();
|
||||
selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { fetch } from 'undici';
|
||||
import { tokenRouter } from '../../services/tokenRouter.js';
|
||||
import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js';
|
||||
import * as routeRefreshWorkflow from '../../services/routeRefreshWorkflow.js';
|
||||
import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js';
|
||||
import { isTokenExpiredError } from '../../services/alertRules.js';
|
||||
import { estimateProxyCost } from '../../services/modelPricingService.js';
|
||||
@@ -57,7 +57,7 @@ export async function videosProxyRoute(app: FastifyInstance) {
|
||||
: await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy);
|
||||
|
||||
if (!selected && retryCount === 0) {
|
||||
await refreshModelsAndRebuildRoutes();
|
||||
await routeRefreshWorkflow.refreshModelsAndRebuildRoutes();
|
||||
selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user