fix: preserve availability on inactive refresh failure
This commit is contained in:
@@ -387,6 +387,83 @@ describe('refreshModelsForAccount credential discovery', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('preserves existing availability when allowInactive refresh fails', async () => {
|
||||||
|
getApiTokenMock.mockResolvedValue(null);
|
||||||
|
getModelsMock.mockRejectedValue(new Error('upstream unavailable'));
|
||||||
|
|
||||||
|
const site = await db.insert(schema.sites).values({
|
||||||
|
name: 'site-rebind-refresh',
|
||||||
|
url: 'https://site-rebind-refresh.example.com',
|
||||||
|
platform: 'new-api',
|
||||||
|
status: 'active',
|
||||||
|
}).returning().get();
|
||||||
|
|
||||||
|
const account = await db.insert(schema.accounts).values({
|
||||||
|
siteId: site.id,
|
||||||
|
username: 'rebind-user',
|
||||||
|
accessToken: 'session-token',
|
||||||
|
apiToken: null,
|
||||||
|
status: 'disabled',
|
||||||
|
extraConfig: JSON.stringify({ credentialMode: 'session' }),
|
||||||
|
}).returning().get();
|
||||||
|
|
||||||
|
const token = await db.insert(schema.accountTokens).values({
|
||||||
|
accountId: account.id,
|
||||||
|
name: 'default',
|
||||||
|
token: 'sk-stored-token',
|
||||||
|
source: 'manual',
|
||||||
|
enabled: true,
|
||||||
|
isDefault: true,
|
||||||
|
valueStatus: 'ready' as any,
|
||||||
|
}).returning().get();
|
||||||
|
|
||||||
|
await db.insert(schema.modelAvailability).values({
|
||||||
|
accountId: account.id,
|
||||||
|
modelName: 'gpt-4.1',
|
||||||
|
available: true,
|
||||||
|
latencyMs: 120,
|
||||||
|
checkedAt: '2026-03-21T11:30:00.000Z',
|
||||||
|
}).run();
|
||||||
|
|
||||||
|
await db.insert(schema.tokenModelAvailability).values({
|
||||||
|
tokenId: token.id,
|
||||||
|
modelName: 'gpt-4.1',
|
||||||
|
available: true,
|
||||||
|
latencyMs: 90,
|
||||||
|
checkedAt: '2026-03-21T11:30:00.000Z',
|
||||||
|
}).run();
|
||||||
|
|
||||||
|
const result = await refreshModelsForAccount(account.id, { allowInactive: true });
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
accountId: account.id,
|
||||||
|
refreshed: true,
|
||||||
|
status: 'failed',
|
||||||
|
modelCount: 0,
|
||||||
|
discoveredByCredential: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const modelRows = await db.select().from(schema.modelAvailability)
|
||||||
|
.where(eq(schema.modelAvailability.accountId, account.id))
|
||||||
|
.all();
|
||||||
|
expect(modelRows).toHaveLength(1);
|
||||||
|
expect(modelRows[0]).toMatchObject({
|
||||||
|
accountId: account.id,
|
||||||
|
modelName: 'gpt-4.1',
|
||||||
|
available: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokenRows = await db.select().from(schema.tokenModelAvailability)
|
||||||
|
.where(eq(schema.tokenModelAvailability.tokenId, token.id))
|
||||||
|
.all();
|
||||||
|
expect(tokenRows).toHaveLength(1);
|
||||||
|
expect(tokenRows[0]).toMatchObject({
|
||||||
|
tokenId: token.id,
|
||||||
|
modelName: 'gpt-4.1',
|
||||||
|
available: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('does not scan masked_pending placeholders as token credentials', async () => {
|
it('does not scan masked_pending placeholders as token credentials', async () => {
|
||||||
getApiTokenMock.mockResolvedValue(null);
|
getApiTokenMock.mockResolvedValue(null);
|
||||||
getModelsMock.mockImplementation(async (_baseUrl: string, token: string) => (
|
getModelsMock.mockImplementation(async (_baseUrl: string, token: string) => (
|
||||||
|
|||||||
@@ -499,21 +499,59 @@ export async function refreshModelsForAccount(
|
|||||||
const oauth = getOauthInfoFromExtraConfig(account.extraConfig);
|
const oauth = getOauthInfoFromExtraConfig(account.extraConfig);
|
||||||
const adapter = getAdapter(site.platform);
|
const adapter = getAdapter(site.platform);
|
||||||
const accountProxyUrl = getProxyUrlFromExtraConfig(account.extraConfig);
|
const accountProxyUrl = getProxyUrlFromExtraConfig(account.extraConfig);
|
||||||
|
const restoreAvailabilityOnFailure = options?.allowInactive === true;
|
||||||
const accountTokens = await db.select()
|
const previousAccountTokens = restoreAvailabilityOnFailure
|
||||||
|
? await db.select()
|
||||||
.from(schema.accountTokens)
|
.from(schema.accountTokens)
|
||||||
.where(eq(schema.accountTokens.accountId, accountId))
|
.where(eq(schema.accountTokens.accountId, accountId))
|
||||||
.all();
|
.all()
|
||||||
|
: [];
|
||||||
|
const previousModelAvailability = restoreAvailabilityOnFailure
|
||||||
|
? await db.select()
|
||||||
|
.from(schema.modelAvailability)
|
||||||
|
.where(eq(schema.modelAvailability.accountId, accountId))
|
||||||
|
.all()
|
||||||
|
: [];
|
||||||
|
const previousTokenModelAvailability = restoreAvailabilityOnFailure
|
||||||
|
? (await Promise.all(previousAccountTokens.map(async (token) => db.select()
|
||||||
|
.from(schema.tokenModelAvailability)
|
||||||
|
.where(eq(schema.tokenModelAvailability.tokenId, token.id))
|
||||||
|
.all()))).flat()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const clearExistingAvailability = async () => {
|
||||||
await db.delete(schema.modelAvailability)
|
await db.delete(schema.modelAvailability)
|
||||||
.where(eq(schema.modelAvailability.accountId, accountId))
|
.where(eq(schema.modelAvailability.accountId, accountId))
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
for (const token of accountTokens) {
|
const currentAccountTokens = await db.select({ id: schema.accountTokens.id })
|
||||||
|
.from(schema.accountTokens)
|
||||||
|
.where(eq(schema.accountTokens.accountId, accountId))
|
||||||
|
.all();
|
||||||
|
|
||||||
|
for (const token of currentAccountTokens) {
|
||||||
await db.delete(schema.tokenModelAvailability)
|
await db.delete(schema.tokenModelAvailability)
|
||||||
.where(eq(schema.tokenModelAvailability.tokenId, token.id))
|
.where(eq(schema.tokenModelAvailability.tokenId, token.id))
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const restorePreviousAvailability = async () => {
|
||||||
|
if (!restoreAvailabilityOnFailure) return;
|
||||||
|
await clearExistingAvailability();
|
||||||
|
if (previousModelAvailability.length > 0) {
|
||||||
|
await db.insert(schema.modelAvailability).values(
|
||||||
|
previousModelAvailability.map(({ id: _id, ...row }) => row),
|
||||||
|
).run();
|
||||||
|
}
|
||||||
|
if (previousTokenModelAvailability.length > 0) {
|
||||||
|
await db.insert(schema.tokenModelAvailability).values(
|
||||||
|
previousTokenModelAvailability.map(({ id: _id, ...row }) => row),
|
||||||
|
).run();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await clearExistingAvailability();
|
||||||
|
|
||||||
if (isSiteDisabled(site.status)) {
|
if (isSiteDisabled(site.status)) {
|
||||||
return buildSkippedRefreshResult(accountId, 'site_disabled', '站点已禁用');
|
return buildSkippedRefreshResult(accountId, 'site_disabled', '站点已禁用');
|
||||||
@@ -583,6 +621,7 @@ export async function refreshModelsForAccount(
|
|||||||
source: 'model-discovery',
|
source: 'model-discovery',
|
||||||
checkedAt,
|
checkedAt,
|
||||||
});
|
});
|
||||||
|
await restorePreviousAvailability();
|
||||||
return buildFailedRefreshResult({
|
return buildFailedRefreshResult({
|
||||||
accountId,
|
accountId,
|
||||||
errorCode,
|
errorCode,
|
||||||
@@ -650,6 +689,7 @@ export async function refreshModelsForAccount(
|
|||||||
source: 'model-discovery',
|
source: 'model-discovery',
|
||||||
checkedAt,
|
checkedAt,
|
||||||
});
|
});
|
||||||
|
await restorePreviousAvailability();
|
||||||
return buildFailedRefreshResult({
|
return buildFailedRefreshResult({
|
||||||
accountId,
|
accountId,
|
||||||
errorCode,
|
errorCode,
|
||||||
@@ -717,6 +757,7 @@ export async function refreshModelsForAccount(
|
|||||||
source: 'model-discovery',
|
source: 'model-discovery',
|
||||||
checkedAt,
|
checkedAt,
|
||||||
});
|
});
|
||||||
|
await restorePreviousAvailability();
|
||||||
return buildFailedRefreshResult({
|
return buildFailedRefreshResult({
|
||||||
accountId,
|
accountId,
|
||||||
errorCode,
|
errorCode,
|
||||||
@@ -788,6 +829,7 @@ export async function refreshModelsForAccount(
|
|||||||
source: 'model-discovery',
|
source: 'model-discovery',
|
||||||
checkedAt,
|
checkedAt,
|
||||||
});
|
});
|
||||||
|
await restorePreviousAvailability();
|
||||||
return buildFailedRefreshResult({
|
return buildFailedRefreshResult({
|
||||||
accountId,
|
accountId,
|
||||||
errorCode,
|
errorCode,
|
||||||
@@ -960,6 +1002,7 @@ export async function refreshModelsForAccount(
|
|||||||
source: 'model-discovery',
|
source: 'model-discovery',
|
||||||
checkedAt: new Date().toISOString(),
|
checkedAt: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
await restorePreviousAvailability();
|
||||||
return buildFailedRefreshResult({
|
return buildFailedRefreshResult({
|
||||||
accountId,
|
accountId,
|
||||||
errorCode,
|
errorCode,
|
||||||
|
|||||||
Reference in New Issue
Block a user