487bd23a02
- Implemented `CliProxyApiAdapter` with detection logic for CLIProxyAPI. - Added tests for `CliProxyApiAdapter` to verify detection and response handling. - Created `OneHubAdapter` with methods for fetching models and user groups. - Added tests for `OneHubAdapter` to ensure correct fallback and response parsing. - Developed `Sub2ApiAdapter` with detection and user info fetching capabilities. - Added comprehensive tests for `Sub2ApiAdapter` covering various scenarios. - Introduced `TitleHintPlatform` type and detection logic based on HTML title. - Implemented tests for title hint detection to identify platforms from URLs.
84 lines
2.6 KiB
TypeScript
84 lines
2.6 KiB
TypeScript
import { BasePlatformAdapter, type BalanceInfo, type CheckinResult, type UserInfo } from './base.js';
|
|
|
|
function normalizeBaseUrl(baseUrl: string): string {
|
|
return (baseUrl || '').replace(/\/+$/, '');
|
|
}
|
|
|
|
export class CliProxyApiAdapter extends BasePlatformAdapter {
|
|
readonly platformName = 'cliproxyapi';
|
|
|
|
async detect(url: string): Promise<boolean> {
|
|
const normalized = (url || '').toLowerCase();
|
|
|
|
// Quick check: default CLIProxyAPI port
|
|
if (/:8317(\/|$)/.test(normalized)) {
|
|
return true;
|
|
}
|
|
|
|
// Quick check: common hostname keyword
|
|
if (normalized.includes('cliproxy')) {
|
|
return true;
|
|
}
|
|
|
|
// Probe management endpoint with strict signature checks.
|
|
// Do not trust bare 401/403 because many non-CLIProxy sites may return
|
|
// those statuses for unknown/protected paths.
|
|
try {
|
|
const base = normalizeBaseUrl(url);
|
|
const { fetch } = await import('undici');
|
|
const res = await fetch(`${base}/v0/management/openai-compatibility`, {
|
|
method: 'GET',
|
|
signal: AbortSignal.timeout(5000),
|
|
});
|
|
|
|
const hasCpaHeaders = Boolean(
|
|
res.headers.get('x-cpa-version')
|
|
|| res.headers.get('x-cpa-commit')
|
|
|| res.headers.get('x-cpa-build-date'),
|
|
);
|
|
if (hasCpaHeaders) {
|
|
return res.status === 200 || res.status === 401 || res.status === 403;
|
|
}
|
|
|
|
if (res.status === 200) {
|
|
const payload = await res.json().catch(() => null);
|
|
if (payload && typeof payload === 'object') {
|
|
return Object.prototype.hasOwnProperty.call(payload, 'openai-compatibility');
|
|
}
|
|
}
|
|
|
|
return false;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
override async login(_baseUrl: string, _username: string, _password: string) {
|
|
return { success: false as const, message: 'CLIProxyAPI does not support login' };
|
|
}
|
|
|
|
override async getUserInfo(_baseUrl: string, _accessToken: string): Promise<UserInfo | null> {
|
|
return null;
|
|
}
|
|
|
|
async checkin(_baseUrl: string, _accessToken: string): Promise<CheckinResult> {
|
|
return { success: false, message: 'CLIProxyAPI does not support checkin' };
|
|
}
|
|
|
|
async getBalance(_baseUrl: string, _accessToken: string): Promise<BalanceInfo> {
|
|
return { balance: 0, used: 0, quota: 0 };
|
|
}
|
|
|
|
async getModels(baseUrl: string, apiToken: string): Promise<string[]> {
|
|
try {
|
|
const base = normalizeBaseUrl(baseUrl);
|
|
const res = await this.fetchJson<any>(`${base}/v1/models`, {
|
|
headers: { Authorization: `Bearer ${apiToken}` },
|
|
});
|
|
return (res?.data || []).map((m: any) => m?.id).filter(Boolean);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
}
|