fix: preserve availability on inactive refresh failure

This commit is contained in:
Cita
2026-03-21 20:27:18 +08:00
parent 4480f504e4
commit fa372d121a
2 changed files with 133 additions and 13 deletions
@@ -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) => (
+47 -4
View File
@@ -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,