[codex] Fix Anyrouter model sync shield handling (#348)

* fix anyrouter model sync shield handling

* fix shield challenge message precedence
This commit is contained in:
Cita
2026-03-31 19:32:25 +08:00
committed by GitHub
parent 87390651f4
commit d930b87762
4 changed files with 181 additions and 3 deletions
@@ -274,6 +274,75 @@ describe('refreshModelsForAccount credential discovery', () => {
expect(parsed.runtimeHealth?.checkedAt).toMatch(/\d{4}-\d{2}-\d{2}T/);
});
it('normalizes anyrouter html challenge parse errors during model discovery', async () => {
getApiTokenMock.mockResolvedValue(null);
getModelsMock.mockRejectedValue(new Error("Unexpected token '<', \"<html><scr\"... is not valid JSON"));
const site = await db.insert(schema.sites).values({
name: 'site-anyrouter',
url: 'https://anyrouter.example.com',
platform: 'anyrouter',
status: 'active',
}).returning().get();
const account = await db.insert(schema.accounts).values({
siteId: site.id,
username: 'shielded-user',
accessToken: 'session-token',
apiToken: null,
status: 'active',
}).returning().get();
const result = await refreshModelsForAccount(account.id);
expect(result).toMatchObject({
accountId: account.id,
refreshed: true,
modelCount: 0,
modelsPreview: [],
tokenScanned: 0,
status: 'failed',
errorCode: 'unknown',
errorMessage: '模型获取失败:站点返回了防护页面,请在目标站点创建 API Key 后再同步模型',
});
const latest = await db.select().from(schema.accounts)
.where(eq(schema.accounts.id, account.id))
.get();
const parsed = JSON.parse(latest!.extraConfig || '{}');
expect(parsed.runtimeHealth?.reason).toBe('模型获取失败:站点返回了防护页面,请在目标站点创建 API Key 后再同步模型');
});
it('keeps shield guidance when challenge html arrives with http 403 discovery failure', async () => {
getApiTokenMock.mockResolvedValue(null);
getModelsMock.mockRejectedValue(new Error('HTTP 403: <html><script>var arg1="abc123"</script></html>'));
const site = await db.insert(schema.sites).values({
name: 'site-anyrouter-403',
url: 'https://anyrouter-403.example.com',
platform: 'anyrouter',
status: 'active',
}).returning().get();
const account = await db.insert(schema.accounts).values({
siteId: site.id,
username: 'shielded-user-403',
accessToken: 'session-token',
apiToken: null,
status: 'active',
}).returning().get();
const result = await refreshModelsForAccount(account.id);
expect(result).toMatchObject({
accountId: account.id,
refreshed: true,
status: 'failed',
errorCode: 'unauthorized',
errorMessage: '模型获取失败:站点返回了防护页面,请在目标站点创建 API Key 后再同步模型',
});
});
it('does not scan hidden managed tokens for direct apikey connections', async () => {
getApiTokenMock.mockResolvedValue(null);
getModelsMock.mockImplementation(async (_baseUrl: string, token: string) => (
+30 -2
View File
@@ -106,6 +106,26 @@ export type ModelRefreshResult =
| ModelRefreshFailureResult
| ModelRefreshSuccessResult;
function looksLikeHtmlJsonParseError(message: string): boolean {
const lowered = String(message || '').trim().toLowerCase();
return (
lowered.includes('unexpected token')
&& lowered.includes('not valid json')
&& (lowered.includes('<html') || lowered.includes('<script'))
);
}
function looksLikeShieldChallenge(message: string): boolean {
const lowered = String(message || '').trim().toLowerCase();
return (
lowered.includes('acw_sc__v2')
|| lowered.includes('var arg1')
|| lowered.includes('captcha')
|| lowered.includes('challenge')
|| lowered.includes('cloudflare tunnel error')
);
}
function classifyModelDiscoveryError(message: string): ModelRefreshErrorCode {
const lowered = message.toLowerCase();
if (lowered.includes('timeout') || lowered.includes('timed out') || lowered.includes('请求超时')) return 'timeout';
@@ -115,7 +135,15 @@ function classifyModelDiscoveryError(message: string): ModelRefreshErrorCode {
return 'unknown';
}
function buildModelFailureMessage(code: ModelRefreshErrorCode, fallback?: string) {
function buildModelFailureMessage(code: ModelRefreshErrorCode, fallback?: string, platform?: string | null) {
const raw = String(fallback || '').trim();
if (looksLikeHtmlJsonParseError(raw) || looksLikeShieldChallenge(raw)) {
const normalizedPlatform = String(platform || '').trim().toLowerCase();
if (normalizedPlatform === 'new-api' || normalizedPlatform === 'anyrouter') {
return '模型获取失败:站点返回了防护页面,请在目标站点创建 API Key 后再同步模型';
}
return '模型获取失败:站点返回了网页而不是 JSON 响应';
}
if (code === 'timeout') return '模型获取失败(请求超时)';
if (code === 'unauthorized') return '模型获取失败,API Key 已无效';
if (code === 'empty_models') return '模型获取失败:未获取到可用模型';
@@ -844,7 +872,7 @@ export async function refreshModelsForAccount(
if (accountModels.size === 0) {
const firstMessage = failureMessages[0] || '';
const errorCode = firstMessage ? classifyModelDiscoveryError(firstMessage) : 'empty_models';
const errorMessage = buildModelFailureMessage(errorCode, firstMessage);
const errorMessage = buildModelFailureMessage(errorCode, firstMessage, site.platform);
await setAccountRuntimeHealth(account.id, {
state: 'unhealthy',
reason: errorMessage,
@@ -4,6 +4,7 @@ import { AddressInfo } from 'node:net';
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { NewApiAdapter } from './newApi.js';
import { AnyRouterAdapter } from './anyrouter.js';
interface RequestSnapshot {
method: string;
@@ -25,6 +26,7 @@ const SHIELD_LOGIN_COOKIE = 'challenge-seed';
const COOKIE_ONLY_LOGIN_USERNAME = 'cookie-only-user';
const COOKIE_ONLY_LOGIN_PASSWORD = 'cookie-only-pass';
const COOKIE_ONLY_LOGIN_SESSION = 'cookie-only-session';
const OPENAI_MODELS_SHIELDED_TOKEN = 'openai-models-shielded-token';
const COOKIE_SHIELDED_TOKEN = Buffer.from(
`1771864970|${Buffer.from('username=linuxdo_131936').toString('base64')}|sig`,
).toString('base64');
@@ -67,6 +69,34 @@ describe('NewApiAdapter', () => {
});
if (req.url === '/v1/models') {
if (typeof req.headers.authorization === 'string' && req.headers.authorization === `Bearer ${OPENAI_MODELS_SHIELDED_TOKEN}`) {
const cookieHeader = typeof req.headers.cookie === 'string' ? req.headers.cookie : '';
if (!cookieHeader.includes(`acw_sc__v2=${ANYROUTER_CHALLENGE_ACW}`)) {
res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
'Set-Cookie': `cdn_sec_tc=${SHIELD_LOGIN_COOKIE}; Path=/; HttpOnly`,
});
res.end(ANYROUTER_CHALLENGE_HTML);
return;
}
if (
!cookieHeader.includes(`cdn_sec_tc=${SHIELD_LOGIN_COOKIE}`)
|| !cookieHeader.includes(`session=${OPENAI_MODELS_SHIELDED_TOKEN}`)
) {
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { message: 'missing shield cookie context' } }));
return;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
data: [
{ id: 'claude-sonnet-4-5-20250929' },
{ id: 'claude-opus-4-6' },
],
}));
return;
}
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { message: 'invalid token' } }));
return;
@@ -426,6 +456,29 @@ describe('NewApiAdapter', () => {
).toBe(true);
});
it('reuses shield cookie retry when anyrouter /v1/models returns challenge html', async () => {
const adapter = new AnyRouterAdapter();
const models = await adapter.getModels(baseUrl, OPENAI_MODELS_SHIELDED_TOKEN);
expect(models).toEqual(['claude-sonnet-4-5-20250929', 'claude-opus-4-6']);
expect(
requests.some(
(r) =>
r.url === '/v1/models'
&& typeof r.headers.cookie === 'string'
&& r.headers.cookie.includes(`session=${OPENAI_MODELS_SHIELDED_TOKEN}`),
),
).toBe(true);
expect(
requests.some(
(r) =>
r.url === '/v1/models'
&& typeof r.headers.cookie === 'string'
&& r.headers.cookie.includes(`acw_sc__v2=${ANYROUTER_CHALLENGE_ACW}`),
),
).toBe(true);
});
it('parses token list response with data.items[] shape', async () => {
const adapter = new NewApiAdapter();
const token = await adapter.getApiToken(baseUrl, 'session-token', 11494);
+29 -1
View File
@@ -2,6 +2,7 @@ import { ApiTokenInfo, BasePlatformAdapter, CheckinResult, BalanceInfo, UserInfo
import type { RequestInit as UndiciRequestInit } from 'undici';
import { createContext, runInContext } from 'node:vm';
import { withSiteProxyRequestInit } from '../siteProxy.js';
import { fetchJsonWithShieldCookieRetry } from './newApiShield.js';
export class NewApiAdapter extends BasePlatformAdapter {
readonly platformName: string = 'new-api';
@@ -761,12 +762,39 @@ export class NewApiAdapter extends BasePlatformAdapter {
return [];
}
private extractOpenAiModels(payload: any): string[] {
if (!Array.isArray(payload?.data)) return [];
return payload.data.map((m: any) => m?.id).filter(Boolean);
}
private async getOpenAiModelsViaShieldCookie(baseUrl: string, token: string): Promise<string[]> {
for (const cookie of this.buildCookieCandidates(token)) {
try {
const { data } = await fetchJsonWithShieldCookieRetry<any>(`${baseUrl}/v1/models`, {
headers: {
Authorization: `Bearer ${token}`,
Cookie: cookie,
},
});
const models = this.extractOpenAiModels(data);
if (models.length > 0) return models;
} catch {}
}
return [];
}
private async getOpenAiModels(baseUrl: string, token: string): Promise<string[]> {
const shouldTryShieldCookie = this.platformName === 'anyrouter' || token.includes('=');
if (shouldTryShieldCookie) {
const shieldModels = await this.getOpenAiModelsViaShieldCookie(baseUrl, token);
if (shieldModels.length > 0) return shieldModels;
}
try {
const res = await this.fetchJson<any>(`${baseUrl}/v1/models`, {
headers: { Authorization: `Bearer ${token}` },
});
return (res?.data || []).map((m: any) => m.id).filter(Boolean);
return this.extractOpenAiModels(res);
} catch {
return [];
}