CI/CD: - Fix build_args format in woodpecker CI (comma-separated, not YAML list) - This fixes "unknown" SHA/version showing on remote deployments Backend schema-tolerant fixes (graceful fallbacks when tables missing): - users.ts: Check which columns exist before querying - worker-registry.ts: Return empty result if table doesn't exist - task-service.ts: Add tableExists() helper, handle missing tables/views - proxies.ts: Return totalProxies in test-all response Frontend fixes: - Proxies: Use total from response for accurate progress display - SEO PagesTab: Dim Generate button when no AI provider active 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
257 lines
7.3 KiB
TypeScript
257 lines
7.3 KiB
TypeScript
import { pool } from '../db/pool';
|
|
import { testProxy, saveProxyTestResult } from './proxy';
|
|
|
|
interface ProxyTestJob {
|
|
id: number;
|
|
status: string;
|
|
total_proxies: number;
|
|
tested_proxies: number;
|
|
passed_proxies: number;
|
|
failed_proxies: number;
|
|
mode?: string; // 'all' | 'failed' | 'inactive'
|
|
}
|
|
|
|
// Concurrency settings
|
|
const DEFAULT_CONCURRENCY = 10; // Test 10 proxies at a time
|
|
|
|
// Simple in-memory queue - could be replaced with Bull/Bee-Queue for production
|
|
const activeJobs = new Map<number, { cancelled: boolean }>();
|
|
|
|
// Clean up orphaned jobs on server startup
|
|
export async function cleanupOrphanedJobs(): Promise<void> {
|
|
try {
|
|
const result = await pool.query(`
|
|
UPDATE proxy_test_jobs
|
|
SET status = 'cancelled',
|
|
completed_at = CURRENT_TIMESTAMP,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE status IN ('pending', 'running')
|
|
RETURNING id
|
|
`);
|
|
|
|
if (result.rows.length > 0) {
|
|
console.log(`🧹 Cleaned up ${result.rows.length} orphaned proxy test jobs`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error cleaning up orphaned jobs:', error);
|
|
}
|
|
}
|
|
|
|
export type ProxyTestMode = 'all' | 'failed' | 'inactive';
|
|
|
|
export interface CreateJobResult {
|
|
jobId: number;
|
|
totalProxies: number;
|
|
}
|
|
|
|
export async function createProxyTestJob(mode: ProxyTestMode = 'all', concurrency: number = DEFAULT_CONCURRENCY): Promise<CreateJobResult> {
|
|
// Check for existing running jobs first
|
|
const existingJob = await getActiveProxyTestJob();
|
|
if (existingJob) {
|
|
throw new Error('A proxy test job is already running. Please cancel it first.');
|
|
}
|
|
|
|
// Get count based on mode
|
|
let countQuery: string;
|
|
switch (mode) {
|
|
case 'failed':
|
|
countQuery = `SELECT COUNT(*) as count FROM proxies WHERE test_result = 'failed' OR active = false`;
|
|
break;
|
|
case 'inactive':
|
|
countQuery = `SELECT COUNT(*) as count FROM proxies WHERE active = false`;
|
|
break;
|
|
default:
|
|
countQuery = `SELECT COUNT(*) as count FROM proxies`;
|
|
}
|
|
|
|
const result = await pool.query(countQuery);
|
|
const totalProxies = parseInt(result.rows[0].count);
|
|
|
|
if (totalProxies === 0) {
|
|
throw new Error(`No proxies to test with mode '${mode}'`);
|
|
}
|
|
|
|
const jobResult = await pool.query(`
|
|
INSERT INTO proxy_test_jobs (status, total_proxies)
|
|
VALUES ('pending', $1)
|
|
RETURNING id
|
|
`, [totalProxies]);
|
|
|
|
const jobId = jobResult.rows[0].id;
|
|
|
|
// Start job in background with mode and concurrency
|
|
runProxyTestJob(jobId, mode, concurrency).catch(err => {
|
|
console.error(`❌ Proxy test job ${jobId} failed:`, err);
|
|
});
|
|
|
|
return { jobId, totalProxies };
|
|
}
|
|
|
|
export async function getProxyTestJob(jobId: number): Promise<ProxyTestJob | null> {
|
|
const result = await pool.query(`
|
|
SELECT id, status, total_proxies, tested_proxies, passed_proxies, failed_proxies
|
|
FROM proxy_test_jobs
|
|
WHERE id = $1
|
|
`, [jobId]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return result.rows[0];
|
|
}
|
|
|
|
export async function getActiveProxyTestJob(): Promise<ProxyTestJob | null> {
|
|
const result = await pool.query(`
|
|
SELECT id, status, total_proxies, tested_proxies, passed_proxies, failed_proxies
|
|
FROM proxy_test_jobs
|
|
WHERE status IN ('pending', 'running')
|
|
ORDER BY created_at DESC
|
|
LIMIT 1
|
|
`);
|
|
|
|
if (result.rows.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return result.rows[0];
|
|
}
|
|
|
|
export async function cancelProxyTestJob(jobId: number): Promise<boolean> {
|
|
// Try to cancel in-memory job first
|
|
const jobControl = activeJobs.get(jobId);
|
|
if (jobControl) {
|
|
jobControl.cancelled = true;
|
|
}
|
|
|
|
// Always update database to handle orphaned jobs
|
|
const result = await pool.query(`
|
|
UPDATE proxy_test_jobs
|
|
SET status = 'cancelled',
|
|
completed_at = CURRENT_TIMESTAMP,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = $1 AND status IN ('pending', 'running')
|
|
RETURNING id
|
|
`, [jobId]);
|
|
|
|
return result.rows.length > 0;
|
|
}
|
|
|
|
async function runProxyTestJob(jobId: number, mode: ProxyTestMode = 'all', concurrency: number = DEFAULT_CONCURRENCY): Promise<void> {
|
|
// Register job as active
|
|
activeJobs.set(jobId, { cancelled: false });
|
|
|
|
try {
|
|
// Update status to running
|
|
await pool.query(`
|
|
UPDATE proxy_test_jobs
|
|
SET status = 'running',
|
|
started_at = CURRENT_TIMESTAMP,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = $1
|
|
`, [jobId]);
|
|
|
|
console.log(`🔍 Starting proxy test job ${jobId} (mode: ${mode}, concurrency: ${concurrency})...`);
|
|
|
|
// Get proxies based on mode
|
|
let query: string;
|
|
switch (mode) {
|
|
case 'failed':
|
|
query = `SELECT id, host, port, protocol, username, password FROM proxies WHERE test_result = 'failed' OR active = false ORDER BY id`;
|
|
break;
|
|
case 'inactive':
|
|
query = `SELECT id, host, port, protocol, username, password FROM proxies WHERE active = false ORDER BY id`;
|
|
break;
|
|
default:
|
|
query = `SELECT id, host, port, protocol, username, password FROM proxies ORDER BY id`;
|
|
}
|
|
|
|
const result = await pool.query(query);
|
|
const proxies = result.rows;
|
|
|
|
let tested = 0;
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
// Process proxies in batches for parallel testing
|
|
for (let i = 0; i < proxies.length; i += concurrency) {
|
|
// Check if job was cancelled
|
|
const jobControl = activeJobs.get(jobId);
|
|
if (jobControl?.cancelled) {
|
|
console.log(`⏸️ Proxy test job ${jobId} cancelled`);
|
|
break;
|
|
}
|
|
|
|
const batch = proxies.slice(i, i + concurrency);
|
|
|
|
// Test batch in parallel
|
|
const batchResults = await Promise.all(
|
|
batch.map(async (proxy) => {
|
|
const testResult = await testProxy(
|
|
proxy.host,
|
|
proxy.port,
|
|
proxy.protocol,
|
|
proxy.username,
|
|
proxy.password
|
|
);
|
|
|
|
// Save result
|
|
await saveProxyTestResult(proxy.id, testResult);
|
|
|
|
return testResult.success;
|
|
})
|
|
);
|
|
|
|
// Count results
|
|
for (const success of batchResults) {
|
|
tested++;
|
|
if (success) {
|
|
passed++;
|
|
} else {
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
// Update job progress
|
|
await pool.query(`
|
|
UPDATE proxy_test_jobs
|
|
SET tested_proxies = $1,
|
|
passed_proxies = $2,
|
|
failed_proxies = $3,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = $4
|
|
`, [tested, passed, failed, jobId]);
|
|
|
|
// Log progress
|
|
console.log(`📊 Job ${jobId}: ${tested}/${proxies.length} proxies tested (${passed} passed, ${failed} failed)`);
|
|
}
|
|
|
|
// Mark job as completed
|
|
const jobControl = activeJobs.get(jobId);
|
|
const finalStatus = jobControl?.cancelled ? 'cancelled' : 'completed';
|
|
|
|
await pool.query(`
|
|
UPDATE proxy_test_jobs
|
|
SET status = $1,
|
|
completed_at = CURRENT_TIMESTAMP,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = $2
|
|
`, [finalStatus, jobId]);
|
|
|
|
console.log(`✅ Proxy test job ${jobId} ${finalStatus}: ${tested} tested, ${passed} passed, ${failed} failed`);
|
|
|
|
} catch (error) {
|
|
console.error(`❌ Proxy test job ${jobId} error:`, error);
|
|
await pool.query(`
|
|
UPDATE proxy_test_jobs
|
|
SET status = 'failed',
|
|
completed_at = CURRENT_TIMESTAMP,
|
|
updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = $1
|
|
`, [jobId]);
|
|
} finally {
|
|
// Remove from active jobs
|
|
activeJobs.delete(jobId);
|
|
}
|
|
}
|