mirror of
https://github.com/AdsPower/adspower-browser.git
synced 2026-09-19 03:08:18 +08:00
feat(core): Local API 请求可配置最小间隔(E2E 动态超时)
This commit is contained in:
@@ -1,7 +1,63 @@
|
||||
import axios from 'axios';
|
||||
import type { InternalAxiosRequestConfig } from 'axios';
|
||||
import { PORT, API_KEY, CONFIG } from './config.js';
|
||||
import { LOCAL_API_CONTRACTS } from './localApiContracts.js';
|
||||
|
||||
/** 若 >0:任意两次经 `getApiClient()` 发往 Local API 的请求之间至少间隔该毫秒数(串行化,避免并发绕过)。 */
|
||||
function readLocalApiMinIntervalMs(): number {
|
||||
const raw = process.env.ADSP_LOCAL_API_MIN_INTERVAL_MS?.trim() ?? '';
|
||||
if (!raw) {
|
||||
return 0;
|
||||
}
|
||||
const n = Number(raw);
|
||||
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
||||
}
|
||||
|
||||
let localApiThrottleLock: Promise<void> = Promise.resolve();
|
||||
let localApiLastRequestStartMs = 0;
|
||||
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
async function throttleLocalApiRequest(): Promise<void> {
|
||||
const gapMs = readLocalApiMinIntervalMs();
|
||||
if (gapMs <= 0) {
|
||||
return;
|
||||
}
|
||||
const prev = localApiThrottleLock;
|
||||
let release!: () => void;
|
||||
localApiThrottleLock = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
await prev;
|
||||
try {
|
||||
const now = Date.now();
|
||||
const earliest = localApiLastRequestStartMs === 0 ? now : localApiLastRequestStartMs + gapMs;
|
||||
const waitMs = Math.max(0, earliest - now);
|
||||
if (waitMs > 0) {
|
||||
await sleep(waitMs);
|
||||
}
|
||||
localApiLastRequestStartMs = Date.now();
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
function isLocalApiRequestUrl(url: string | undefined): boolean {
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const u = new URL(url, 'http://127.0.0.1');
|
||||
return u.hostname === '127.0.0.1' || u.hostname === 'localhost';
|
||||
} catch {
|
||||
return url.includes('127.0.0.1') || url.includes('localhost');
|
||||
}
|
||||
}
|
||||
|
||||
export const LOCAL_API_BASE = `http://127.0.0.1:${PORT}`;
|
||||
|
||||
export const getLocalApiBase = () => {
|
||||
@@ -48,7 +104,14 @@ export const apiClient = axios.create({
|
||||
});
|
||||
|
||||
export const getApiClient = () => {
|
||||
return axios.create({
|
||||
headers: CONFIG.apiKey ? { 'Authorization': `Bearer ${CONFIG.apiKey}` } : {}
|
||||
const client = axios.create({
|
||||
headers: CONFIG.apiKey ? { 'Authorization': `Bearer ${CONFIG.apiKey}` } : {},
|
||||
});
|
||||
}
|
||||
client.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
|
||||
if (isLocalApiRequestUrl(config.url)) {
|
||||
await throttleLocalApiRequest();
|
||||
}
|
||||
return config;
|
||||
});
|
||||
return client;
|
||||
};
|
||||
|
||||
+49
-21
@@ -162,29 +162,57 @@ function isE2ERealApiEnabled(): boolean {
|
||||
return process.env.ADSP_MCP_E2E_ENABLED === '1';
|
||||
}
|
||||
|
||||
/** `ADSP_LOCAL_API_MIN_INTERVAL_MS` 开启时,两次 Local API 请求至少间隔该毫秒数,全量 case 会显著变长。 */
|
||||
function realApiPerCaseTimeoutMs(): number {
|
||||
const n = Number(process.env.ADSP_LOCAL_API_MIN_INTERVAL_MS ?? '0');
|
||||
return n > 0 ? 120_000 : 30_000;
|
||||
}
|
||||
|
||||
function realApiBrowserTimeoutMs(): number {
|
||||
const n = Number(process.env.ADSP_LOCAL_API_MIN_INTERVAL_MS ?? '0');
|
||||
return n > 0 ? 360_000 : 180_000;
|
||||
}
|
||||
|
||||
function realApiRunAllTimeoutMs(): number {
|
||||
const n = Number(process.env.ADSP_LOCAL_API_MIN_INTERVAL_MS ?? '0');
|
||||
return n > 0 ? 900_000 : 180_000;
|
||||
}
|
||||
|
||||
describe.skipIf(!isE2ERealApiEnabled())('e2e real Local API (Task 5)', () => {
|
||||
beforeEach(() => {
|
||||
resetOptionalCoverage();
|
||||
});
|
||||
|
||||
it('create-group: group_name discoverable via get-group-list', async () => {
|
||||
const result = await runOneCase('group.create.basic');
|
||||
expect(result.passed).toBe(true);
|
||||
expect(result.details.join('\n')).toMatch(/group_name=/);
|
||||
});
|
||||
it(
|
||||
'create-group: group_name discoverable via get-group-list',
|
||||
async () => {
|
||||
const result = await runOneCase('group.create.basic');
|
||||
expect(result.passed).toBe(true);
|
||||
expect(result.details.join('\n')).toMatch(/group_name=/);
|
||||
},
|
||||
realApiPerCaseTimeoutMs(),
|
||||
);
|
||||
|
||||
it('create-group: each optionalAll field has a passing case (remark)', async () => {
|
||||
await runOneCase('group.create.withRemark');
|
||||
const coverage = getParameterCoverage('create-group');
|
||||
for (const param of coverage.optionalAll) {
|
||||
expect(coverage.passedOptionalParams).toContain(param);
|
||||
}
|
||||
});
|
||||
it(
|
||||
'create-group: each optionalAll field has a passing case (remark)',
|
||||
async () => {
|
||||
await runOneCase('group.create.withRemark');
|
||||
const coverage = getParameterCoverage('create-group');
|
||||
for (const param of coverage.optionalAll) {
|
||||
expect(coverage.passedOptionalParams).toContain(param);
|
||||
}
|
||||
},
|
||||
realApiPerCaseTimeoutMs(),
|
||||
);
|
||||
|
||||
it('proxy: create → list → delete', async () => {
|
||||
const result = await runOneCase('proxy.create.list.delete');
|
||||
expect(result.passed).toBe(true);
|
||||
});
|
||||
it(
|
||||
'proxy: create → list → delete',
|
||||
async () => {
|
||||
const result = await runOneCase('proxy.create.list.delete');
|
||||
expect(result.passed).toBe(true);
|
||||
},
|
||||
realApiPerCaseTimeoutMs(),
|
||||
);
|
||||
|
||||
it(
|
||||
'browser: create → open headless → close → delete',
|
||||
@@ -192,7 +220,7 @@ describe.skipIf(!isE2ERealApiEnabled())('e2e real Local API (Task 5)', () => {
|
||||
const result = await runOneCase('browser.open.headless');
|
||||
expect(result.passed).toBe(true);
|
||||
},
|
||||
180_000,
|
||||
realApiBrowserTimeoutMs(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -212,7 +240,7 @@ describe.skipIf(!isE2ERealApiEnabled())('e2e parameter report (Task 6)', () => {
|
||||
status: expect.any(String),
|
||||
});
|
||||
},
|
||||
180_000,
|
||||
realApiRunAllTimeoutMs(),
|
||||
);
|
||||
|
||||
it(
|
||||
@@ -222,7 +250,7 @@ describe.skipIf(!isE2ERealApiEnabled())('e2e parameter report (Task 6)', () => {
|
||||
const row = report.tools.find((t) => t.name === 'create-group');
|
||||
expect(row?.missingOptionalParameters).toEqual([]);
|
||||
},
|
||||
180_000,
|
||||
realApiRunAllTimeoutMs(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -238,7 +266,7 @@ describe.skipIf(!isE2ERealApiEnabled())('e2e coverage summary gates (Task 8)', (
|
||||
expect(report.summary.casePassRate).toBeGreaterThanOrEqual(0.95);
|
||||
expect(report.summary.parameterPassRate).toBeGreaterThanOrEqual(0.95);
|
||||
},
|
||||
180_000,
|
||||
realApiRunAllTimeoutMs(),
|
||||
);
|
||||
|
||||
it(
|
||||
@@ -254,6 +282,6 @@ describe.skipIf(!isE2ERealApiEnabled())('e2e coverage summary gates (Task 8)', (
|
||||
expect(report.summary.optionalAllCoverage).toBeLessThanOrEqual(1);
|
||||
expect(report.summary.totalTools).toBe(report.tools.length);
|
||||
},
|
||||
180_000,
|
||||
realApiRunAllTimeoutMs(),
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user