chore: initialize clean metapi repository
This commit is contained in:
@@ -0,0 +1 @@
|
||||
<html><script>var arg1='B8B78DDA1D5CD2598AAEE9DD6975913AA490BAE3';(function(a,c){var G=a0j,d=a();while(!![]){try{var e=-parseInt(G(0x117))/0x1*(parseInt(G(0x111))/0x2)+-parseInt(G(0xfb))/0x3*(parseInt(G(0x10e))/0x4)+-parseInt(G(0x101))/0x5*(-parseInt(G(0xfd))/0x6)+-parseInt(G(0x102))/0x7*(parseInt(G(0x122))/0x8)+parseInt(G(0x112))/0x9+parseInt(G(0x11d))/0xa*(parseInt(G(0x11c))/0xb)+parseInt(G(0x114))/0xc;if(e===c)break;else d['push'](d['shift']());}catch(f){d['push'](d['shift']());}}}(a0i,0x760bf),!(function(){var L=a0j,j=(function(){var B=!![];return function(C,D){var E=B?function(){var H=a0j;if(D){var F=D[H(0x10d)](C,arguments);return D=null,F;}}:function(){};return B=![],E;};}()),k=j(this,function(){var I=a0j;return k[I(0xff)]()[I(0x123)](I(0x10f))[I(0xff)]()[I(0x107)](k)[I(0x123)](I(0x10f));});k();var l=(function(){var B=!![];return function(C,D){var E=B?function(){var J=a0j;if(D){var F=D[J(0x10d)](C,arguments);return D=null,F;}}:function(){};return B=![],E;};}());(function(){l(this,function(){var K=a0j,B=new RegExp(K(0x118)),C=new RegExp(K(0x106),'i'),D=b(K(0x100));!B[K(0x104)](D+K(0x105))||!C[K(0x104)](D+K(0x11b))?D('0'):b();})();}());for(var m=[0xf,0x23,0x1d,0x18,0x21,0x10,0x1,0x26,0xa,0x9,0x13,0x1f,0x28,0x1b,0x16,0x17,0x19,0xd,0x6,0xb,0x27,0x12,0x14,0x8,0xe,0x15,0x20,0x1a,0x2,0x1e,0x7,0x4,0x11,0x5,0x3,0x1c,0x22,0x25,0xc,0x24],p=L(0x115),q=[],u='',v='',w=L(0x116),x=0x0;x<arg1[w];x++)for(var y=arg1[x],z=0x0;z<m[w];z++)m[z]==x+0x1&&(q[z]=y);for(u=q[L(0xfc)](''),x=0x0;x<u[w]&&x<p[w];x+=0x2){var A=(parseInt(u[L(0x11a)](x,x+0x2),0x10)^parseInt(p[L(0x11a)](x,x+0x2),0x10))[L(0xff)](0x10);0x1==A[w]&&(A='0'+A),v+=A;}document[L(0x121)]='acw_sc__v2='+v+L(0x120)+new Date(Date[L(0x119)]()+0x36ee80)[L(0x10c)]()+L(0x109),document[L(0xfe)][L(0x103)]();}()));function a0i(){var N=['mJKZmgTStNvVyq','C3rYAw5N','y2fSBa','o2v4CgLYzxm9','y29VA2LL','mteZmZy3mNLbu2PszW','C2vHCMnO','D2HPBguGkhrYDwuPihT9','mJq1ndi0rKHuthnj','AM9PBG','nNH1rKHOuq','Bg9JyxrPB24','Dg9tDhjPBMC','Aw5PDa','mJi2odi1nwnMre1IyG','n0HxChPJva','CMvSB2fK','DgvZDa','y2HHAw4','xcTCkYaQkd86w2eTEKeTwL8KxvSWltLHlxPblvPFjf0Qkq','y29UC3rYDwn0B3i','y291BNrLCG','o21HEc1Hz2u9mZyWmdTWyxrOps87','zgvIDq','ywn0Aw9U','Dg9htvrtDhjPBMC','yxbWBhK','mJbiru1MChi','kcGOlISPkYKRksSK','z2DLCG','nKHJq01Aqq','nJe5nZu5ogH1twPUDa','C3rHDgvpyMPLy3q','mZu5mdu5mNbcB2Pxyq','mZaWmde3nJaWmdG1nJaWnJa2mtuWmtuZmZaWmZy5mdaYnZGWmdm3nq','BgvUz3rO','mtqWnti2shvUBNDv','zNvUy3rPB24GkLWOicPCkq','BM93','C2XPy2u','Aw5WDxq','ntm5BwrLuMXi'];a0i=function(){return N;};return a0i();}function a0j(a,b){var c=a0i();return a0j=function(d,e){d=d-0xfb;var f=c[d];if(a0j['tGHEKR']===undefined){var g=function(l){var m='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var n='',o='',p=n+g;for(var q=0x0,r,s,t=0x0;s=l['charAt'](t++);~s&&(r=q%0x4?r*0x40+s:s,q++%0x4)?n+=p['charCodeAt'](t+0xa)-0xa!==0x0?String['fromCharCode'](0xff&r>>(-0x2*q&0x6)):q:0x0){s=m['indexOf'](s);}for(var u=0x0,v=n['length'];u<v;u++){o+='%'+('00'+n['charCodeAt'](u)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(o);};a0j['CrMtTV']=g,a=arguments,a0j['tGHEKR']=!![];}var h=c[0x0],i=d+h,j=a[i];if(!j){var k=function(l){this['jyamLv']=l,this['WxwaRR']=[0x1,0x0,0x0],this['GuwnVk']=function(){return'newState';},this['CxWuMi']='\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*',this['FxAgTK']='[\x27|\x22].+[\x27|\x22];?\x20*}';};k['prototype']['dQvDam']=function(){var l=new RegExp(this['CxWuMi']+this['FxAgTK']),m=l['test'](this['GuwnVk']['toString']())?--this['WxwaRR'][0x1]:--this['WxwaRR'][0x0];return this['fQYikn'](m);},k['prototype']['fQYikn']=function(l){if(!Boolean(~l))return l;return this['Hbwxmd'](this['jyamLv']);},k['prototype']['Hbwxmd']=function(l){for(var m=0x0,n=this['WxwaRR']['length'];m<n;m++){this['WxwaRR']['push'](Math['round'](Math['random']())),n=this['WxwaRR']['length'];}return l(this['WxwaRR'][0x0]);},new k(a0j)['dQvDam'](),f=a0j['CrMtTV'](f),a[i]=f;}else f=j;return f;},a0j(a,b);}function b(a){function c(d){var M=a0j;if(typeof d===M(0x11e))return function(e){}[M(0x107)](M(0x124))[M(0x10d)](M(0x108));else(''+d/d)[M(0x116)]!==0x1||d%0x14===0x0?function(){return!![];}[M(0x107)](M(0x10a)+M(0x110))[M(0x11f)](M(0x10b)):function(){return![];}[M(0x107)](M(0x10a)+M(0x110))[M(0x10d)](M(0x113));c(++d);}try{if(a)return c;else c(0x0);}catch(d){}}</script></html>
|
||||
@@ -0,0 +1,10 @@
|
||||
import { NewApiAdapter } from './newApi.js';
|
||||
|
||||
export class AnyRouterAdapter extends NewApiAdapter {
|
||||
readonly platformName = 'anyrouter';
|
||||
|
||||
async detect(url: string): Promise<boolean> {
|
||||
const normalized = (url || '').toLowerCase();
|
||||
return normalized.includes('anyrouter');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import type { RequestInit as UndiciRequestInit } from 'undici';
|
||||
|
||||
export interface CheckinResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
reward?: string;
|
||||
}
|
||||
|
||||
export interface BalanceInfo {
|
||||
balance: number;
|
||||
used: number;
|
||||
quota: number;
|
||||
todayIncome?: number;
|
||||
todayQuotaConsumption?: number;
|
||||
}
|
||||
|
||||
interface LoginResult {
|
||||
success: boolean;
|
||||
accessToken?: string;
|
||||
username?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
username: string;
|
||||
displayName?: string;
|
||||
email?: string;
|
||||
role?: number;
|
||||
}
|
||||
|
||||
export interface TokenVerifyResult {
|
||||
tokenType: 'session' | 'apikey' | 'unknown';
|
||||
userInfo?: UserInfo | null;
|
||||
balance?: BalanceInfo | null;
|
||||
apiToken?: string | null;
|
||||
models?: string[];
|
||||
}
|
||||
|
||||
export interface ApiTokenInfo {
|
||||
name: string;
|
||||
key: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateApiTokenOptions {
|
||||
name?: string;
|
||||
group?: string;
|
||||
unlimitedQuota?: boolean;
|
||||
remainQuota?: number;
|
||||
expiredTime?: number;
|
||||
allowIps?: string;
|
||||
modelLimitsEnabled?: boolean;
|
||||
modelLimits?: string;
|
||||
}
|
||||
|
||||
export interface PlatformAdapter {
|
||||
readonly platformName: string;
|
||||
detect(url: string): Promise<boolean>;
|
||||
login(baseUrl: string, username: string, password: string): Promise<LoginResult>;
|
||||
getUserInfo(baseUrl: string, accessToken: string, platformUserId?: number): Promise<UserInfo | null>;
|
||||
verifyToken(baseUrl: string, token: string, platformUserId?: number): Promise<TokenVerifyResult>;
|
||||
checkin(baseUrl: string, accessToken: string, platformUserId?: number): Promise<CheckinResult>;
|
||||
getBalance(baseUrl: string, accessToken: string, platformUserId?: number): Promise<BalanceInfo>;
|
||||
getModels(baseUrl: string, token: string, platformUserId?: number): Promise<string[]>;
|
||||
getApiToken(baseUrl: string, accessToken: string, platformUserId?: number): Promise<string | null>;
|
||||
getApiTokens(baseUrl: string, accessToken: string, platformUserId?: number): Promise<ApiTokenInfo[]>;
|
||||
getUserGroups(baseUrl: string, accessToken: string, platformUserId?: number): Promise<string[]>;
|
||||
createApiToken(baseUrl: string, accessToken: string, platformUserId?: number, options?: CreateApiTokenOptions): Promise<boolean>;
|
||||
deleteApiToken(baseUrl: string, accessToken: string, tokenKey: string, platformUserId?: number): Promise<boolean>;
|
||||
}
|
||||
|
||||
export abstract class BasePlatformAdapter implements PlatformAdapter {
|
||||
abstract readonly platformName: string;
|
||||
|
||||
abstract detect(url: string): Promise<boolean>;
|
||||
abstract checkin(baseUrl: string, accessToken: string): Promise<CheckinResult>;
|
||||
abstract getBalance(baseUrl: string, accessToken: string): Promise<BalanceInfo>;
|
||||
abstract getModels(baseUrl: string, token: string, platformUserId?: number): Promise<string[]>;
|
||||
|
||||
async verifyToken(baseUrl: string, token: string, _platformUserId?: number): Promise<TokenVerifyResult> {
|
||||
// 1. Try as session/access token first (for management APIs)
|
||||
const userInfo = await this.getUserInfo(baseUrl, token);
|
||||
if (userInfo) {
|
||||
let balance: BalanceInfo | null = null;
|
||||
try { balance = await this.getBalance(baseUrl, token); } catch {}
|
||||
let apiToken: string | null = null;
|
||||
try { apiToken = await this.getApiToken(baseUrl, token); } catch {}
|
||||
return { tokenType: 'session', userInfo, balance, apiToken };
|
||||
}
|
||||
|
||||
// 2. Try as API key (for /v1/models)
|
||||
try {
|
||||
const models = await this.getModels(baseUrl, token);
|
||||
if (models && models.length > 0) {
|
||||
return { tokenType: 'apikey', models };
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return { tokenType: 'unknown' };
|
||||
}
|
||||
|
||||
async getUserInfo(baseUrl: string, accessToken: string): Promise<UserInfo | null> {
|
||||
try {
|
||||
const res = await this.fetchJson<any>(`${baseUrl}/api/user/self`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
if (res?.success && res?.data) {
|
||||
return {
|
||||
username: res.data.username || res.data.display_name || '',
|
||||
displayName: res.data.display_name,
|
||||
email: res.data.email,
|
||||
role: res.data.role,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async login(baseUrl: string, username: string, password: string): Promise<LoginResult> {
|
||||
try {
|
||||
const res = await this.fetchJson<any>(`${baseUrl}/api/user/login`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
if (res?.success && res?.data) {
|
||||
return {
|
||||
success: true,
|
||||
accessToken: typeof res.data === 'string' ? res.data : res.data.token || res.data.access_token,
|
||||
username,
|
||||
};
|
||||
}
|
||||
return { success: false, message: res?.message || '登录失败' };
|
||||
} catch (err: any) {
|
||||
return { success: false, message: err.message || '登录请求失败' };
|
||||
}
|
||||
}
|
||||
|
||||
async getApiToken(_baseUrl: string, _accessToken: string, _platformUserId?: number): Promise<string | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async getApiTokens(baseUrl: string, accessToken: string, platformUserId?: number): Promise<ApiTokenInfo[]> {
|
||||
const token = await this.getApiToken(baseUrl, accessToken, platformUserId);
|
||||
if (!token) return [];
|
||||
return [{ name: 'default', key: token, enabled: true }];
|
||||
}
|
||||
|
||||
async createApiToken(
|
||||
_baseUrl: string,
|
||||
_accessToken: string,
|
||||
_platformUserId?: number,
|
||||
_options?: CreateApiTokenOptions,
|
||||
): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
async getUserGroups(
|
||||
_baseUrl: string,
|
||||
_accessToken: string,
|
||||
_platformUserId?: number,
|
||||
): Promise<string[]> {
|
||||
return ['default'];
|
||||
}
|
||||
|
||||
async deleteApiToken(
|
||||
_baseUrl: string,
|
||||
_accessToken: string,
|
||||
_tokenKey: string,
|
||||
_platformUserId?: number,
|
||||
): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected async fetchJson<T>(url: string, options?: UndiciRequestInit): Promise<T> {
|
||||
const { fetch } = await import('undici');
|
||||
const requestOptions: UndiciRequestInit = {
|
||||
...options,
|
||||
body: options?.body ?? undefined,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
};
|
||||
const res = await fetch(url, requestOptions);
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}: ${await res.text()}`);
|
||||
}
|
||||
return res.json() as Promise<T>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
||||
import { AddressInfo } from 'node:net';
|
||||
import { DoneHubAdapter } from './doneHub.js';
|
||||
|
||||
describe('DoneHubAdapter', () => {
|
||||
let server: ReturnType<typeof createServer>;
|
||||
let baseUrl: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
||||
if (req.url === '/v1/models') {
|
||||
res.writeHead(401, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: { message: 'unauthorized' } }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url === '/api/available_model') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
'gpt-4o': { price: { input: 0.5, output: 1.5 } },
|
||||
'deepseek-chat': { price: { input: 0.1, output: 0.2 } },
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url?.startsWith('/api/token/')) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
data: {
|
||||
data: [
|
||||
{ key: 'donehub-token-key', name: 'sys_playground', status: 1 },
|
||||
],
|
||||
},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'not found' }));
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
const addr = server.address() as AddressInfo;
|
||||
baseUrl = `http://127.0.0.1:${addr.port}`;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((err?: Error) => (err ? reject(err) : resolve()));
|
||||
});
|
||||
});
|
||||
|
||||
it('marks checkin as unsupported', async () => {
|
||||
const adapter = new DoneHubAdapter();
|
||||
const result = await adapter.checkin(baseUrl, 'token');
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe('checkin endpoint not found');
|
||||
});
|
||||
|
||||
it('falls back to /api/available_model when /v1/models is unavailable', async () => {
|
||||
const adapter = new DoneHubAdapter();
|
||||
const models = await adapter.getModels(baseUrl, 'token');
|
||||
expect(models).toEqual(['gpt-4o', 'deepseek-chat']);
|
||||
});
|
||||
|
||||
it('parses token list from nested data.data shape', async () => {
|
||||
const adapter = new DoneHubAdapter();
|
||||
const tokens = await adapter.getApiTokens(baseUrl, 'token');
|
||||
expect(tokens).toEqual([
|
||||
{ key: 'donehub-token-key', name: 'sys_playground', enabled: true },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { OneApiAdapter } from './oneApi.js';
|
||||
import type { CheckinResult } from './base.js';
|
||||
|
||||
export class DoneHubAdapter extends OneApiAdapter {
|
||||
readonly platformName = 'done-hub';
|
||||
|
||||
async detect(url: string): Promise<boolean> {
|
||||
const normalized = url.toLowerCase();
|
||||
return normalized.includes('donehub') || normalized.includes('done-hub');
|
||||
}
|
||||
|
||||
// DoneHub deployments generally do not expose /api/user/checkin.
|
||||
// Mark as unsupported so higher-level logic records it as skipped instead of failed.
|
||||
override async checkin(_baseUrl: string, _accessToken: string): Promise<CheckinResult> {
|
||||
return { success: false, message: 'checkin endpoint not found' };
|
||||
}
|
||||
|
||||
override async getModels(baseUrl: string, apiToken: string, _platformUserId?: number): Promise<string[]> {
|
||||
let openAiModels: string[] = [];
|
||||
try {
|
||||
openAiModels = await super.getModels(baseUrl, apiToken, _platformUserId);
|
||||
} catch {}
|
||||
if (openAiModels.length > 0) return openAiModels;
|
||||
|
||||
try {
|
||||
const res = await this.fetchJson<any>(`${baseUrl}/api/available_model`, {
|
||||
headers: { Authorization: `Bearer ${apiToken}` },
|
||||
});
|
||||
|
||||
const payload = (res?.data && typeof res.data === 'object') ? res.data : res;
|
||||
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
||||
const models = Object.keys(payload).filter(Boolean);
|
||||
if (models.length > 0) return models;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { detectPlatform, getAdapter } from './index.js';
|
||||
|
||||
describe('getAdapter platform aliases', () => {
|
||||
it('returns dedicated anyrouter adapter for anyrouter alias', () => {
|
||||
const adapter = getAdapter('anyrouter');
|
||||
expect(adapter?.platformName).toBe('anyrouter');
|
||||
});
|
||||
|
||||
it('handles case-insensitive platform strings', () => {
|
||||
const adapter = getAdapter('Veloera');
|
||||
expect(adapter?.platformName).toBe('veloera');
|
||||
});
|
||||
|
||||
it('returns undefined for unknown platforms', () => {
|
||||
expect(getAdapter('unknown-platform')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('detects anyrouter URL before generic new-api adapter', async () => {
|
||||
const adapter = await detectPlatform('https://anyrouter.top');
|
||||
expect(adapter?.platformName).toBe('anyrouter');
|
||||
});
|
||||
|
||||
it('detects done-hub URL before generic adapters', async () => {
|
||||
const adapter = await detectPlatform('https://demo.donehub.example');
|
||||
expect(adapter?.platformName).toBe('done-hub');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { PlatformAdapter } from './base.js';
|
||||
import { AnyRouterAdapter } from './anyrouter.js';
|
||||
import { NewApiAdapter } from './newApi.js';
|
||||
import { OneApiAdapter } from './oneApi.js';
|
||||
import { VeloeraAdapter } from './veloera.js';
|
||||
import { OneHubAdapter } from './oneHub.js';
|
||||
import { DoneHubAdapter } from './doneHub.js';
|
||||
import { Sub2ApiAdapter } from './sub2api.js';
|
||||
|
||||
const adapters: PlatformAdapter[] = [
|
||||
// Specific forks before generic adapters for better auto-detection.
|
||||
new AnyRouterAdapter(),
|
||||
new DoneHubAdapter(),
|
||||
new OneHubAdapter(),
|
||||
new NewApiAdapter(),
|
||||
new Sub2ApiAdapter(),
|
||||
new OneApiAdapter(),
|
||||
new VeloeraAdapter(),
|
||||
];
|
||||
|
||||
const platformAliases: Record<string, string> = {
|
||||
// NewAPI family aliases
|
||||
anyrouter: 'anyrouter',
|
||||
'wong-gongyi': 'new-api',
|
||||
'vo-api': 'new-api',
|
||||
'super-api': 'new-api',
|
||||
'rix-api': 'new-api',
|
||||
'neo-api': 'new-api',
|
||||
newapi: 'new-api',
|
||||
'new api': 'new-api',
|
||||
// OneAPI family aliases
|
||||
oneapi: 'one-api',
|
||||
'one api': 'one-api',
|
||||
// Keep canonical forms explicit for clarity
|
||||
'new-api': 'new-api',
|
||||
'one-api': 'one-api',
|
||||
veloera: 'veloera',
|
||||
'one-hub': 'one-hub',
|
||||
'done-hub': 'done-hub',
|
||||
sub2api: 'sub2api',
|
||||
};
|
||||
|
||||
function normalizePlatform(platform: string): string {
|
||||
const raw = (platform || '').trim().toLowerCase();
|
||||
return platformAliases[raw] ?? raw;
|
||||
}
|
||||
|
||||
export function getAdapter(platform: string): PlatformAdapter | undefined {
|
||||
const normalized = normalizePlatform(platform);
|
||||
return adapters.find((a) => a.platformName === normalized);
|
||||
}
|
||||
|
||||
export async function detectPlatform(url: string): Promise<PlatformAdapter | undefined> {
|
||||
for (const adapter of adapters) {
|
||||
if (await adapter.detect(url)) return adapter;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
||||
import { AddressInfo } from 'node:net';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { NewApiAdapter } from './newApi.js';
|
||||
|
||||
interface RequestSnapshot {
|
||||
method: string;
|
||||
url: string;
|
||||
headers: IncomingMessage['headers'];
|
||||
}
|
||||
|
||||
const COOKIE_SESSION_TOKEN = 'cookie-session-token';
|
||||
const COOKIE_REQUIRES_USER_TOKEN = 'cookie-requires-user';
|
||||
const CHECKIN_ALREADY_TOKEN = 'checkin-already-token';
|
||||
const CHECKIN_INVALID_URL_TOKEN = 'checkin-invalid-url-token';
|
||||
const CHECKIN_CLOUDFLARE_530_TOKEN = 'checkin-cloudflare-530-token';
|
||||
const BALANCE_FAIL_TOKEN = 'balance-fail-token';
|
||||
const COOKIE_SHIELDED_TOKEN = Buffer.from(
|
||||
`1771864970|${Buffer.from('username=linuxdo_131936').toString('base64')}|sig`,
|
||||
).toString('base64');
|
||||
const ANYROUTER_CHALLENGE_HTML = readFileSync(
|
||||
new URL('./__fixtures__/anyrouter-challenge.html', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
const ANYROUTER_CHALLENGE_ACW = '699dbedad126579b6bc0ebb91eaae8d7af3548b5';
|
||||
const CLOUDFLARE_530_HTML = `
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<head>
|
||||
<title>Cloudflare Tunnel error | newapi.tanmw.top | Cloudflare</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1><span>Error</span><span>1033</span></h1>
|
||||
<h2>Cloudflare Tunnel error</h2>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
describe('NewApiAdapter', () => {
|
||||
let server: ReturnType<typeof createServer>;
|
||||
let baseUrl: string;
|
||||
let requests: RequestSnapshot[] = [];
|
||||
|
||||
beforeEach(async () => {
|
||||
requests = [];
|
||||
server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
||||
requests.push({
|
||||
method: req.method || 'GET',
|
||||
url: req.url || '/',
|
||||
headers: req.headers,
|
||||
});
|
||||
|
||||
if (req.url === '/v1/models') {
|
||||
res.writeHead(401, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: { message: 'invalid token' } }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url === '/api/user/models') {
|
||||
if (req.headers['new-api-user'] !== '11494') {
|
||||
res.writeHead(401, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: 'missing New-Api-User' }));
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ data: ['gpt-4o', 'gpt-4.1'] }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url?.startsWith('/api/token/')) {
|
||||
if (typeof req.headers.authorization === 'string' && req.headers.authorization === `Bearer ${COOKIE_SHIELDED_TOKEN}`) {
|
||||
res.writeHead(401, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: 'unauthorized' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof req.headers.cookie === 'string' && req.headers.cookie.includes(`session=${COOKIE_SHIELDED_TOKEN}`)) {
|
||||
if (!req.headers.cookie.includes(`acw_sc__v2=${ANYROUTER_CHALLENGE_ACW}`)) {
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(ANYROUTER_CHALLENGE_HTML);
|
||||
return;
|
||||
}
|
||||
if (req.headers['new-api-user'] !== '131936') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: 'missing New-Api-User' }));
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
data: {
|
||||
items: [{ key: 'shielded-cookie-key' }],
|
||||
},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof req.headers.authorization === 'string' && req.headers.authorization === `Bearer ${COOKIE_SESSION_TOKEN}`) {
|
||||
res.writeHead(401, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: 'unauthorized' }));
|
||||
return;
|
||||
}
|
||||
if (typeof req.headers.authorization === 'string' && req.headers.authorization === `Bearer ${COOKIE_REQUIRES_USER_TOKEN}`) {
|
||||
res.writeHead(401, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: 'unauthorized' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof req.headers.cookie === 'string' && req.headers.cookie.includes(`session=${COOKIE_SESSION_TOKEN}`)) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
data: {
|
||||
items: [{ key: 'cookie-api-key' }],
|
||||
},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof req.headers.cookie === 'string' && req.headers.cookie.includes(`session=${COOKIE_REQUIRES_USER_TOKEN}`)) {
|
||||
if (req.headers['new-api-user'] !== '8899') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: 'missing New-Api-User' }));
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
data: {
|
||||
items: [{ key: 'cookie-user-key' }],
|
||||
},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
data: {
|
||||
items: [{ key: 'api-key-from-token-list' }],
|
||||
},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url === '/api/user/self') {
|
||||
if (typeof req.headers.authorization === 'string' && req.headers.authorization === `Bearer ${BALANCE_FAIL_TOKEN}`) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: '閺冪姵娼堟潻娑滎攽濮濄倖鎼锋担婊愮礉閺堫亞娅ヨぐ鏇氱瑬閺堫亝褰佹笟?access token' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof req.headers.cookie === 'string' &&
|
||||
(
|
||||
req.headers.cookie.includes(`session=${BALANCE_FAIL_TOKEN}`) ||
|
||||
req.headers.cookie.includes(`token=${BALANCE_FAIL_TOKEN}`)
|
||||
)
|
||||
) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: '閺冪姵娼堟潻娑滎攽濮濄倖鎼锋担婊愮礉閺堫亞娅ヨぐ鏇氱瑬閺堫亝褰佹笟?access token' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof req.headers.cookie === 'string' && req.headers.cookie.includes(`session=${COOKIE_SHIELDED_TOKEN}`)) {
|
||||
if (!req.headers.cookie.includes(`acw_sc__v2=${ANYROUTER_CHALLENGE_ACW}`)) {
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(ANYROUTER_CHALLENGE_HTML);
|
||||
return;
|
||||
}
|
||||
if (req.headers['new-api-user'] !== '131936') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: 'missing New-Api-User' }));
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
data: { id: 131936, username: 'linuxdo_131936', quota: 3000000, used_quota: 1200000 },
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof req.headers.authorization === 'string' && req.headers.authorization === 'Bearer session-token') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
data: { id: 11494, username: 'demo-user', quota: 1000000, used_quota: 1000 },
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof req.headers.authorization === 'string' && req.headers.authorization === `Bearer ${COOKIE_SESSION_TOKEN}`) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: 'invalid token' }));
|
||||
return;
|
||||
}
|
||||
if (typeof req.headers.authorization === 'string' && req.headers.authorization === `Bearer ${COOKIE_REQUIRES_USER_TOKEN}`) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: 'invalid token' }));
|
||||
return;
|
||||
}
|
||||
if (typeof req.headers.authorization === 'string') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: 'invalid token' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof req.headers.cookie === 'string' && req.headers.cookie.includes(`session=${COOKIE_SESSION_TOKEN}`)) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
data: { id: 7788, username: 'cookie-user', quota: 2000000, used_quota: 500000 },
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof req.headers.cookie === 'string' && req.headers.cookie.includes(`session=${COOKIE_REQUIRES_USER_TOKEN}`)) {
|
||||
if (req.headers['new-api-user'] !== '8899') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: 'missing New-Api-User' }));
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
data: { id: 8899, username: 'cookie-user-id-required', quota: 1500000, used_quota: 100000 },
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(401, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: 'unauthorized' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.url === '/api/user/checkin') {
|
||||
if (typeof req.headers.authorization === 'string' && req.headers.authorization === `Bearer ${CHECKIN_CLOUDFLARE_530_TOKEN}`) {
|
||||
res.writeHead(530, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(CLOUDFLARE_530_HTML);
|
||||
return;
|
||||
}
|
||||
if (typeof req.headers.authorization === 'string' && req.headers.authorization === `Bearer ${CHECKIN_INVALID_URL_TOKEN}`) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: { message: 'Invalid URL (POST /api/user/checkin)' } }));
|
||||
return;
|
||||
}
|
||||
if (typeof req.headers.cookie === 'string' && req.headers.cookie.includes(`session=${CHECKIN_INVALID_URL_TOKEN}`)) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: { message: 'Invalid URL (POST /api/user/checkin)' } }));
|
||||
return;
|
||||
}
|
||||
if (typeof req.headers.authorization === 'string' && req.headers.authorization === `Bearer ${CHECKIN_ALREADY_TOKEN}`) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: '浠婂ぉ宸茬粡绛惧埌杩囧暒' }));
|
||||
return;
|
||||
}
|
||||
if (typeof req.headers.cookie === 'string' && req.headers.cookie.includes(`session=${CHECKIN_ALREADY_TOKEN}`)) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: '鏃犳潈杩涜姝ゆ搷浣滐紝鏈櫥褰曚笖鏈彁渚?access token' }));
|
||||
return;
|
||||
}
|
||||
if (typeof req.headers.authorization === 'string' && req.headers.authorization === `Bearer ${COOKIE_SHIELDED_TOKEN}`) {
|
||||
res.writeHead(401, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: 'unauthorized' }));
|
||||
return;
|
||||
}
|
||||
if (typeof req.headers.cookie === 'string' && req.headers.cookie.includes(`session=${COOKIE_SHIELDED_TOKEN}`)) {
|
||||
if (!req.headers.cookie.includes(`acw_sc__v2=${ANYROUTER_CHALLENGE_ACW}`)) {
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(ANYROUTER_CHALLENGE_HTML);
|
||||
return;
|
||||
}
|
||||
if (req.headers['new-api-user'] !== '131936') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: 'missing New-Api-User' }));
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: true, message: 'checked-in-ok' }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (req.url === '/api/user/sign_in') {
|
||||
if (typeof req.headers.cookie === 'string' && req.headers.cookie.includes(`session=${CHECKIN_ALREADY_TOKEN}`)) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: '鏃犳潈杩涜姝ゆ搷浣滐紝鏈櫥褰曚笖鏈彁渚?access token' }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'not found' }));
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => resolve());
|
||||
});
|
||||
const addr = server.address() as AddressInfo;
|
||||
baseUrl = `http://127.0.0.1:${addr.port}`;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((err?: Error) => (err ? reject(err) : resolve()));
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to session model endpoint when /v1/models rejects token', async () => {
|
||||
const adapter = new NewApiAdapter();
|
||||
const models = await adapter.getModels(baseUrl, 'session-token', 11494);
|
||||
|
||||
expect(models).toEqual(['gpt-4o', 'gpt-4.1']);
|
||||
expect(requests.some((r) => r.url === '/v1/models')).toBe(true);
|
||||
expect(
|
||||
requests.some((r) => r.url === '/api/user/models' && r.headers['new-api-user'] === '11494'),
|
||||
).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);
|
||||
|
||||
expect(token).toBe('api-key-from-token-list');
|
||||
});
|
||||
|
||||
it('detects cookie session values as session tokens for anyrouter-like deployments', async () => {
|
||||
const adapter = new NewApiAdapter();
|
||||
const result = await adapter.verifyToken(baseUrl, COOKIE_SESSION_TOKEN);
|
||||
|
||||
expect(result.tokenType).toBe('session');
|
||||
expect(result.userInfo?.username).toBe('cookie-user');
|
||||
expect(result.apiToken).toBe('cookie-api-key');
|
||||
expect(
|
||||
requests.some((r) => r.url === '/api/user/self' && typeof r.headers.cookie === 'string' && r.headers.cookie.includes(`session=${COOKIE_SESSION_TOKEN}`)),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('auto-probes New-Api-User for cookie sessions when header is required', async () => {
|
||||
const adapter = new NewApiAdapter();
|
||||
const result = await adapter.verifyToken(baseUrl, COOKIE_REQUIRES_USER_TOKEN);
|
||||
|
||||
expect(result.tokenType).toBe('session');
|
||||
expect(result.userInfo?.username).toBe('cookie-user-id-required');
|
||||
expect(result.apiToken).toBe('cookie-user-key');
|
||||
expect(
|
||||
requests.some((r) => r.url === '/api/user/self' && r.headers['new-api-user'] === '8899'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('solves anyrouter acw challenge and probes user id from session payload', async () => {
|
||||
const adapter = new NewApiAdapter();
|
||||
const result = await adapter.verifyToken(baseUrl, COOKIE_SHIELDED_TOKEN);
|
||||
|
||||
expect(result.tokenType).toBe('session');
|
||||
expect(result.userInfo?.username).toBe('linuxdo_131936');
|
||||
expect(typeof result.apiToken === 'string' && result.apiToken.length > 0).toBe(true);
|
||||
expect(
|
||||
requests.some(
|
||||
(r) =>
|
||||
r.url === '/api/user/self' &&
|
||||
typeof r.headers.cookie === 'string' &&
|
||||
r.headers.cookie.includes(`acw_sc__v2=${ANYROUTER_CHALLENGE_ACW}`),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
requests.some((r) => r.url === '/api/user/self' && r.headers['new-api-user'] === '131936'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('uses shielded cookie flow for balance and checkin', async () => {
|
||||
const adapter = new NewApiAdapter();
|
||||
const balance = await adapter.getBalance(baseUrl, COOKIE_SHIELDED_TOKEN);
|
||||
const checkin = await adapter.checkin(baseUrl, COOKIE_SHIELDED_TOKEN);
|
||||
|
||||
expect(balance).toEqual({
|
||||
quota: 8.4,
|
||||
used: 2.4,
|
||||
balance: 6,
|
||||
});
|
||||
expect(checkin.success).toBe(true);
|
||||
expect(
|
||||
requests.some((r) => r.url === '/api/user/checkin' && r.headers['new-api-user'] === '131936'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves upstream balance failure message for UI feedback', async () => {
|
||||
const adapter = new NewApiAdapter();
|
||||
|
||||
await expect(adapter.getBalance(baseUrl, BALANCE_FAIL_TOKEN)).rejects.toThrow('access token');
|
||||
});
|
||||
|
||||
it('preserves nested checkin error message instead of generic fallback', async () => {
|
||||
const adapter = new NewApiAdapter();
|
||||
const result = await adapter.checkin(baseUrl, CHECKIN_INVALID_URL_TOKEN, 11494);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('Invalid URL');
|
||||
});
|
||||
|
||||
it('summarizes cloudflare tunnel HTML failures to concise checkin error', async () => {
|
||||
const adapter = new NewApiAdapter();
|
||||
const result = await adapter.checkin(baseUrl, CHECKIN_CLOUDFLARE_530_TOKEN, 11494);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe('HTTP 530: Cloudflare Tunnel error (Error 1033)');
|
||||
});
|
||||
|
||||
it('preserves already-checked-in message instead of overriding with cookie fallback error', async () => {
|
||||
const adapter = new NewApiAdapter();
|
||||
const result = await adapter.checkin(baseUrl, CHECKIN_ALREADY_TOKEN, 11494);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toBe('浠婂ぉ宸茬粡绛惧埌杩囧暒');
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,216 @@
|
||||
import { ApiTokenInfo, BasePlatformAdapter, CheckinResult, BalanceInfo, CreateApiTokenOptions } from './base.js';
|
||||
|
||||
type CreateApiTokenPayload = {
|
||||
name: string;
|
||||
unlimited_quota: boolean;
|
||||
expired_time: number;
|
||||
remain_quota: number;
|
||||
allow_ips: string;
|
||||
model_limits_enabled: boolean;
|
||||
model_limits: string;
|
||||
group: string;
|
||||
};
|
||||
|
||||
export class OneApiAdapter extends BasePlatformAdapter {
|
||||
readonly platformName: string = 'one-api';
|
||||
|
||||
private normalizeTokenKeyForCompare(value?: string | null): string {
|
||||
const trimmed = (value || '').trim();
|
||||
return trimmed.startsWith('Bearer ') ? trimmed.slice(7).trim() : trimmed;
|
||||
}
|
||||
|
||||
private buildDefaultTokenPayload(options?: CreateApiTokenOptions): CreateApiTokenPayload {
|
||||
const normalizedName = (options?.name || '').trim() || 'metapi';
|
||||
const unlimitedQuota = options?.unlimitedQuota ?? true;
|
||||
const remainQuota = Number.isFinite(options?.remainQuota)
|
||||
? Math.max(0, Math.trunc(options?.remainQuota as number))
|
||||
: 0;
|
||||
const expiredTime = Number.isFinite(options?.expiredTime)
|
||||
? Math.trunc(options?.expiredTime as number)
|
||||
: -1;
|
||||
return {
|
||||
name: normalizedName,
|
||||
unlimited_quota: unlimitedQuota,
|
||||
expired_time: expiredTime,
|
||||
remain_quota: remainQuota,
|
||||
allow_ips: (options?.allowIps || '').trim(),
|
||||
model_limits_enabled: options?.modelLimitsEnabled ?? false,
|
||||
model_limits: (options?.modelLimits || '').trim(),
|
||||
group: (options?.group || '').trim(),
|
||||
};
|
||||
}
|
||||
|
||||
async detect(url: string): Promise<boolean> {
|
||||
try {
|
||||
const res = await this.fetchJson<any>(`${url}/api/status`);
|
||||
return res?.success === true && !res?.data?.system_name;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async checkin(baseUrl: string, accessToken: string): Promise<CheckinResult> {
|
||||
try {
|
||||
const res = await this.fetchJson<any>(`${baseUrl}/api/user/checkin`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
if (res?.success) {
|
||||
return { success: true, message: res.message || 'Check-in successful', reward: res.data?.reward?.toString() };
|
||||
}
|
||||
return { success: false, message: res?.message || 'Check-in failed' };
|
||||
} catch (err: any) {
|
||||
return { success: false, message: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
async getBalance(baseUrl: string, accessToken: string): Promise<BalanceInfo> {
|
||||
const res = await this.fetchJson<any>(`${baseUrl}/api/user/self`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
const data = res?.data;
|
||||
const quota = (data?.quota || 0) / 500000;
|
||||
const used = (data?.used_quota || 0) / 500000;
|
||||
const todayIncome = Number.isFinite(data?.today_income) ? (data.today_income / 500000) : undefined;
|
||||
const todayQuotaConsumption = Number.isFinite(data?.today_quota_consumption) ? (data.today_quota_consumption / 500000) : undefined;
|
||||
return { balance: quota - used, used, quota, todayIncome, todayQuotaConsumption };
|
||||
}
|
||||
|
||||
async getModels(baseUrl: string, apiToken: string, _platformUserId?: number): Promise<string[]> {
|
||||
const res = await this.fetchJson<any>(`${baseUrl}/v1/models`, {
|
||||
headers: { Authorization: `Bearer ${apiToken}` },
|
||||
});
|
||||
return (res?.data || []).map((m: any) => m.id).filter(Boolean);
|
||||
}
|
||||
|
||||
async getApiTokens(baseUrl: string, accessToken: string): Promise<ApiTokenInfo[]> {
|
||||
try {
|
||||
const res = await this.fetchJson<any>(`${baseUrl}/api/token/?p=0&size=100`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
|
||||
const items = (() => {
|
||||
if (Array.isArray(res?.data)) return res.data;
|
||||
if (Array.isArray(res?.data?.items)) return res.data.items;
|
||||
if (Array.isArray(res?.data?.data)) return res.data.data;
|
||||
if (Array.isArray(res?.items)) return res.items;
|
||||
if (Array.isArray(res?.list)) return res.list;
|
||||
if (Array.isArray(res?.data?.list)) return res.data.list;
|
||||
return [];
|
||||
})();
|
||||
|
||||
return items
|
||||
.map((item: any, index: number) => {
|
||||
const key = typeof item?.key === 'string' ? item.key.trim() : '';
|
||||
if (!key) return null;
|
||||
const rawName = typeof item?.name === 'string' ? item.name.trim() : '';
|
||||
const status = typeof item?.status === 'number' ? item.status : undefined;
|
||||
return {
|
||||
name: rawName || (index === 0 ? 'default' : `token-${index + 1}`),
|
||||
key,
|
||||
enabled: status === undefined ? true : status === 1,
|
||||
} satisfies ApiTokenInfo;
|
||||
})
|
||||
.filter((item: ApiTokenInfo | null): item is ApiTokenInfo => !!item);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getApiToken(baseUrl: string, accessToken: string): Promise<string | null> {
|
||||
const tokens = await this.getApiTokens(baseUrl, accessToken);
|
||||
return tokens.find((token) => token.enabled !== false)?.key || tokens[0]?.key || null;
|
||||
}
|
||||
|
||||
async getUserGroups(baseUrl: string, accessToken: string): Promise<string[]> {
|
||||
const headers = { Authorization: `Bearer ${accessToken}` };
|
||||
const extractGroupKeys = (payload: any): string[] => {
|
||||
const source = payload?.data || payload;
|
||||
if (!source || typeof source !== 'object') return [];
|
||||
return Object.keys(source).map((key) => key.trim()).filter(Boolean);
|
||||
};
|
||||
|
||||
try {
|
||||
const groupMap = await this.fetchJson<any>(`${baseUrl}/api/user_group_map`, { headers });
|
||||
const keys = extractGroupKeys(groupMap);
|
||||
if (keys.length > 0) return Array.from(new Set(keys));
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const groups = await this.fetchJson<any>(`${baseUrl}/api/user/self/groups`, { headers });
|
||||
const keys = extractGroupKeys(groups);
|
||||
if (keys.length > 0) return Array.from(new Set(keys));
|
||||
} catch {}
|
||||
|
||||
return ['default'];
|
||||
}
|
||||
|
||||
async createApiToken(
|
||||
baseUrl: string,
|
||||
accessToken: string,
|
||||
_platformUserId?: number,
|
||||
options?: CreateApiTokenOptions,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const res = await this.fetchJson<any>(`${baseUrl}/api/token/`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
body: JSON.stringify(this.buildDefaultTokenPayload(options)),
|
||||
});
|
||||
return !!res?.success;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteApiToken(
|
||||
baseUrl: string,
|
||||
accessToken: string,
|
||||
tokenKey: string,
|
||||
): Promise<boolean> {
|
||||
const targetKey = this.normalizeTokenKeyForCompare(tokenKey);
|
||||
if (!targetKey) return false;
|
||||
|
||||
const headers = { Authorization: `Bearer ${accessToken}` };
|
||||
let tokenId: number | null = null;
|
||||
try {
|
||||
const res = await this.fetchJson<any>(`${baseUrl}/api/token/?p=0&size=100`, { headers });
|
||||
const items = (() => {
|
||||
if (Array.isArray(res?.data)) return res.data;
|
||||
if (Array.isArray(res?.data?.items)) return res.data.items;
|
||||
if (Array.isArray(res?.items)) return res.items;
|
||||
return [];
|
||||
})();
|
||||
for (const item of items) {
|
||||
const key = this.normalizeTokenKeyForCompare(item?.key);
|
||||
const id = Number.parseInt(String(item?.id), 10);
|
||||
if (key && key === targetKey && Number.isFinite(id)) {
|
||||
tokenId = id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!tokenId) return true;
|
||||
|
||||
try {
|
||||
const res = await this.fetchJson<any>(`${baseUrl}/api/token/${tokenId}`, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
});
|
||||
if (res?.success) return true;
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const res = await this.fetchJson<any>(`${baseUrl}/api/token/${tokenId}/`, {
|
||||
method: 'DELETE',
|
||||
headers,
|
||||
});
|
||||
return !!res?.success;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { OneApiAdapter } from './oneApi.js';
|
||||
|
||||
export class OneHubAdapter extends OneApiAdapter {
|
||||
readonly platformName = 'one-hub';
|
||||
|
||||
async detect(url: string): Promise<boolean> {
|
||||
const normalized = url.toLowerCase();
|
||||
return normalized.includes('onehub') || normalized.includes('one-hub');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { OneApiAdapter } from './oneApi.js';
|
||||
|
||||
export class Sub2ApiAdapter extends OneApiAdapter {
|
||||
readonly platformName = 'sub2api';
|
||||
|
||||
async detect(url: string): Promise<boolean> {
|
||||
return url.toLowerCase().includes('sub2api');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { BasePlatformAdapter, CheckinResult, BalanceInfo } from './base.js';
|
||||
|
||||
export class VeloeraAdapter extends BasePlatformAdapter {
|
||||
readonly platformName = 'veloera';
|
||||
|
||||
async detect(url: string): Promise<boolean> {
|
||||
try {
|
||||
const res = await this.fetchJson<any>(`${url}/api/status`);
|
||||
return res?.success === true && (
|
||||
res?.data?.system_name?.toLowerCase().includes('veloera') ||
|
||||
res?.data?.version?.includes('veloera')
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async checkin(baseUrl: string, accessToken: string): Promise<CheckinResult> {
|
||||
try {
|
||||
const res = await this.fetchJson<any>(`${baseUrl}/api/user/checkin`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
if (res?.success) {
|
||||
return { success: true, message: res.message || 'Check-in successful', reward: res.data?.reward?.toString() };
|
||||
}
|
||||
return { success: false, message: res?.message || 'Check-in failed' };
|
||||
} catch (err: any) {
|
||||
return { success: false, message: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
async getBalance(baseUrl: string, accessToken: string): Promise<BalanceInfo> {
|
||||
const res = await this.fetchJson<any>(`${baseUrl}/api/user/self`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
const data = res?.data;
|
||||
const quota = (data?.quota || 0) / 1000000;
|
||||
const used = (data?.used_quota || 0) / 1000000;
|
||||
const todayIncome = Number.isFinite(data?.today_income) ? (data.today_income / 1000000) : undefined;
|
||||
const todayQuotaConsumption = Number.isFinite(data?.today_quota_consumption) ? (data.today_quota_consumption / 1000000) : undefined;
|
||||
return { balance: quota - used, used, quota, todayIncome, todayQuotaConsumption };
|
||||
}
|
||||
|
||||
async getModels(baseUrl: string, apiToken: string, _platformUserId?: number): Promise<string[]> {
|
||||
const res = await this.fetchJson<any>(`${baseUrl}/v1/models`, {
|
||||
headers: { Authorization: `Bearer ${apiToken}` },
|
||||
});
|
||||
return (res?.data || []).map((m: any) => m.id).filter(Boolean);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user