@swimmingliu/autovpn 1.6.5 → 1.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -54
- package/dist/backend/select-backend.js +0 -3
- package/dist/cli/commands/index.js +4 -0
- package/dist/cli/main.js +7 -25
- package/dist/cli/native-commands.js +1 -0
- package/dist/doctor/checks.js +17 -2
- package/dist/jobs/commands.js +13 -0
- package/dist/jobs/process.js +92 -10
- package/dist/pipeline/availability.js +0 -96
- package/dist/pipeline/dedupe.js +1 -69
- package/dist/pipeline/deploy.js +165 -301
- package/dist/pipeline/extract.js +0 -80
- package/dist/pipeline/obfuscate.js +1 -80
- package/dist/pipeline/orchestrator.js +111 -130
- package/dist/pipeline/postprocess.js +1 -85
- package/dist/pipeline/proxy-runtime.js +5 -0
- package/dist/pipeline/render.js +1 -73
- package/dist/pipeline/speedtest.js +89 -100
- package/dist/runtime/paths.js +11 -2
- package/dist/runtime/templates.js +37 -0
- package/dist/templates/share-worker/vpn.js +4813 -0
- package/dist/templates/vmess_node.js +27 -0
- package/dist/web/renderer/i18n.js +1 -1
- package/package.json +3 -4
- package/dist/backend/python-backend.js +0 -242
- package/lib/cache.mjs +0 -14
- package/lib/errors.mjs +0 -11
- package/lib/install-python-cli.mjs +0 -63
- package/lib/runner.mjs +0 -113
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { spawn as defaultSpawn } from 'node:child_process';
|
|
3
|
-
import { mergeProjectEnv } from '../runtime/env.js';
|
|
4
1
|
const DEFAULT_WORKER_BUILD_CONFIG = {
|
|
5
2
|
environment_name: 'production',
|
|
6
3
|
entry_filename: '_worker.js',
|
|
@@ -15,26 +12,6 @@ const DEFAULT_WORKER_BUILD_CONFIG = {
|
|
|
15
12
|
enable_identifier_randomization: true,
|
|
16
13
|
emit_sidecar_modules: true
|
|
17
14
|
};
|
|
18
|
-
const PYTHON_OBFUSCATE_HELPER = `
|
|
19
|
-
import json
|
|
20
|
-
import sys
|
|
21
|
-
from vpn_automation.config.models import WorkerBuildConfig
|
|
22
|
-
from vpn_automation.pipeline.worker_build import build_worker_artifacts
|
|
23
|
-
|
|
24
|
-
payload = json.load(sys.stdin)
|
|
25
|
-
config = WorkerBuildConfig(**(payload.get("config") or {}))
|
|
26
|
-
artifacts = build_worker_artifacts(payload["rendered_source"], config, payload["secret_query"])
|
|
27
|
-
json.dump(
|
|
28
|
-
{
|
|
29
|
-
"transformed_source": artifacts.transformed_source,
|
|
30
|
-
"modules": artifacts.modules,
|
|
31
|
-
"manifest": artifacts.manifest,
|
|
32
|
-
},
|
|
33
|
-
sys.stdout,
|
|
34
|
-
ensure_ascii=False,
|
|
35
|
-
)
|
|
36
|
-
sys.stdout.write("\\n")
|
|
37
|
-
`;
|
|
38
15
|
function jsonStringLiteral(value) {
|
|
39
16
|
return JSON.stringify(value);
|
|
40
17
|
}
|
|
@@ -139,63 +116,7 @@ export function buildWorkerArtifacts(renderedSource, configInput = {}, secretQue
|
|
|
139
116
|
}
|
|
140
117
|
};
|
|
141
118
|
}
|
|
142
|
-
export function selectPipelineStageBackend(stage, env = process.env) {
|
|
143
|
-
void stage;
|
|
144
|
-
void env;
|
|
145
|
-
return 'node';
|
|
146
|
-
}
|
|
147
|
-
async function defaultResolvePythonCli(env) {
|
|
148
|
-
// @ts-expect-error Phase 1 runner remains plain ESM JavaScript.
|
|
149
|
-
const runner = await import('../../lib/runner.mjs');
|
|
150
|
-
return runner.resolveOrInstallPythonCli({ env });
|
|
151
|
-
}
|
|
152
|
-
function pythonCommandFor(resolved) {
|
|
153
|
-
const command = resolved.command;
|
|
154
|
-
const name = path.basename(command).toLowerCase();
|
|
155
|
-
if (['autovpn', 'autovpn.exe'].includes(name)) {
|
|
156
|
-
const executable = process.platform === 'win32' ? 'python.exe' : 'python';
|
|
157
|
-
return path.join(path.dirname(command), executable);
|
|
158
|
-
}
|
|
159
|
-
return process.platform === 'win32' ? 'python.exe' : 'python3';
|
|
160
|
-
}
|
|
161
|
-
async function obfuscateWithPython(input, options) {
|
|
162
|
-
const env = mergeProjectEnv(options.cwd ?? process.cwd(), options.env ?? process.env);
|
|
163
|
-
const resolved = options.resolvePythonCli ? await options.resolvePythonCli() : await defaultResolvePythonCli(env);
|
|
164
|
-
const child = (options.spawn ?? defaultSpawn)(pythonCommandFor(resolved), ['-c', PYTHON_OBFUSCATE_HELPER], {
|
|
165
|
-
cwd: options.cwd ?? process.cwd(),
|
|
166
|
-
env,
|
|
167
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
168
|
-
});
|
|
169
|
-
let stdout = '';
|
|
170
|
-
let stderr = '';
|
|
171
|
-
child.stdout?.on('data', (chunk) => {
|
|
172
|
-
stdout += String(chunk);
|
|
173
|
-
});
|
|
174
|
-
child.stderr?.on('data', (chunk) => {
|
|
175
|
-
stderr += String(chunk);
|
|
176
|
-
});
|
|
177
|
-
const completion = new Promise((resolve, reject) => {
|
|
178
|
-
child.on('error', reject);
|
|
179
|
-
child.on('close', (code) => {
|
|
180
|
-
if (code !== 0) {
|
|
181
|
-
reject(new Error(`Python obfuscate backend failed with exit code ${code}: ${stderr.trim()}`));
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
try {
|
|
185
|
-
resolve(JSON.parse(stdout));
|
|
186
|
-
}
|
|
187
|
-
catch (error) {
|
|
188
|
-
reject(new Error(`Python obfuscate backend returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`));
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
child.stdin?.write(JSON.stringify(input));
|
|
193
|
-
child.stdin?.end();
|
|
194
|
-
return completion;
|
|
195
|
-
}
|
|
196
119
|
export async function buildWorkerArtifactsWithBackend(input, options = {}) {
|
|
197
|
-
|
|
198
|
-
return options.pythonObfuscate ? options.pythonObfuscate(input) : obfuscateWithPython(input, options);
|
|
199
|
-
}
|
|
120
|
+
void options;
|
|
200
121
|
return buildWorkerArtifacts(input.rendered_source, input.config, input.secret_query);
|
|
201
122
|
}
|
|
@@ -5,6 +5,7 @@ import { parse } from '@iarna/toml';
|
|
|
5
5
|
import { mergeProjectEnv } from '../runtime/env.js';
|
|
6
6
|
import { resolveArtifactsRoot, resolveProfilePath } from '../runtime/paths.js';
|
|
7
7
|
import { redactText } from '../runtime/redaction.js';
|
|
8
|
+
import { resolveWorkerTemplatePath } from '../runtime/templates.js';
|
|
8
9
|
import { fetchSourceLinksWithBackend } from './extract.js';
|
|
9
10
|
import { canonicalVmessKey, dedupeVmessLinksWithBackend, parseVmessLink } from './dedupe.js';
|
|
10
11
|
import { normalizeSpeedTestConfig, probeSpeedtestLinksInNode, selectSpeedtestCandidates, speedtestLinksWithBackend, testSpeedtestLinkInNode } from './speedtest.js';
|
|
@@ -284,32 +285,28 @@ async function restoreResumeSpeedResults(artifactDir, eventLog, passedLinks) {
|
|
|
284
285
|
}
|
|
285
286
|
async function forEachWithConcurrency(items, concurrency, mapper) {
|
|
286
287
|
let nextIndex = 0;
|
|
288
|
+
let failed = false;
|
|
289
|
+
let firstError;
|
|
287
290
|
const workerCount = Math.max(1, Math.min(Math.max(1, Math.floor(concurrency)), items.length));
|
|
288
|
-
|
|
289
|
-
while (nextIndex < items.length) {
|
|
291
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
292
|
+
while (nextIndex < items.length && !failed) {
|
|
290
293
|
const index = nextIndex;
|
|
291
294
|
nextIndex += 1;
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (active >= limit) {
|
|
302
|
-
await new Promise((resolve) => queue.push(resolve));
|
|
303
|
-
}
|
|
304
|
-
active += 1;
|
|
305
|
-
try {
|
|
306
|
-
return await task();
|
|
307
|
-
}
|
|
308
|
-
finally {
|
|
309
|
-
active -= 1;
|
|
310
|
-
queue.shift()?.();
|
|
295
|
+
try {
|
|
296
|
+
await mapper(items[index]);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
if (!failed) {
|
|
300
|
+
failed = true;
|
|
301
|
+
firstError = error;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
311
304
|
}
|
|
312
|
-
};
|
|
305
|
+
});
|
|
306
|
+
await Promise.allSettled(workers);
|
|
307
|
+
if (failed) {
|
|
308
|
+
throw firstError;
|
|
309
|
+
}
|
|
313
310
|
}
|
|
314
311
|
async function speedtestResumeStateFromEventLog(eventLog) {
|
|
315
312
|
const probes = new Map();
|
|
@@ -417,98 +414,6 @@ export async function runNodePipeline(options, context = {}) {
|
|
|
417
414
|
let availabilityResults = [];
|
|
418
415
|
let availableLinks = [];
|
|
419
416
|
const streamedDeduped = new Set();
|
|
420
|
-
const speedTasks = [];
|
|
421
|
-
const availabilityTasks = [];
|
|
422
|
-
const limitSpeedtest = createLimiter(Number(speedConfig.concurrency ?? 1));
|
|
423
|
-
const limitAvailability = createLimiter(Number(speedConfig.concurrency ?? 1));
|
|
424
|
-
let speedCompleted = 0;
|
|
425
|
-
let availabilityCompleted = 0;
|
|
426
|
-
let speedArtifactFlush = Promise.resolve();
|
|
427
|
-
let availabilityArtifactFlush = Promise.resolve();
|
|
428
|
-
const flushStreamingSpeedArtifacts = async () => {
|
|
429
|
-
const resultSnapshot = [...speedResults];
|
|
430
|
-
const linkSnapshot = [...passedSpeedLinks];
|
|
431
|
-
speedArtifactFlush = speedArtifactFlush.catch(() => undefined).then(async () => {
|
|
432
|
-
summary.counts.speedtest_links = linkSnapshot.length;
|
|
433
|
-
await writeLines(artifactDir, 'vpn_node_speedtest.txt', linkSnapshot);
|
|
434
|
-
await writeJson(artifactDir, 'vpn_node_speedtest_report.json', resultSnapshot);
|
|
435
|
-
await writeReport();
|
|
436
|
-
});
|
|
437
|
-
await speedArtifactFlush;
|
|
438
|
-
};
|
|
439
|
-
const flushStreamingAvailabilityArtifacts = async () => {
|
|
440
|
-
const resultSnapshot = [...availabilityResults];
|
|
441
|
-
const linkSnapshot = resultSnapshot.filter((result) => result.all_passed).map((result) => result.link);
|
|
442
|
-
availabilityArtifactFlush = availabilityArtifactFlush.catch(() => undefined).then(async () => {
|
|
443
|
-
summary.counts.availability_links = linkSnapshot.length;
|
|
444
|
-
await writeLines(artifactDir, 'vpn_node_availability.txt', linkSnapshot);
|
|
445
|
-
await writeJson(artifactDir, 'vpn_node_availability_report.json', resultSnapshot);
|
|
446
|
-
await writeReport();
|
|
447
|
-
});
|
|
448
|
-
await availabilityArtifactFlush;
|
|
449
|
-
};
|
|
450
|
-
const runStreamingAvailability = (speedResult) => {
|
|
451
|
-
const task = limitAvailability(async () => {
|
|
452
|
-
const results = context.stages?.availability
|
|
453
|
-
? await context.stages.availability([speedResult], profile.speed_test ?? {}, runtimePath, profile.availability_targets)
|
|
454
|
-
: await checkLinkAvailabilityBatchWithBackend({
|
|
455
|
-
results: [speedResult],
|
|
456
|
-
config: profile.speed_test ?? {},
|
|
457
|
-
runtime_path: runtimePath,
|
|
458
|
-
targets: profile.availability_targets
|
|
459
|
-
}, { cwd: projectRoot, env: runtimeStageEnv });
|
|
460
|
-
for (const result of results) {
|
|
461
|
-
availabilityResults.push(result);
|
|
462
|
-
availabilityCompleted += 1;
|
|
463
|
-
await flushStreamingAvailabilityArtifacts();
|
|
464
|
-
emit('availability_link_result', {
|
|
465
|
-
completed: availabilityCompleted,
|
|
466
|
-
total: passedSpeedLinks.length,
|
|
467
|
-
link: result.link,
|
|
468
|
-
all_passed: result.all_passed,
|
|
469
|
-
provider_results: result.provider_results
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
});
|
|
473
|
-
availabilityTasks.push(task);
|
|
474
|
-
task.catch(() => undefined);
|
|
475
|
-
};
|
|
476
|
-
const runStreamingSpeedtest = (link) => {
|
|
477
|
-
const task = limitSpeedtest(async () => {
|
|
478
|
-
const result = context.stages?.speedtestLink
|
|
479
|
-
? await context.stages.speedtestLink(link, profile.speed_test ?? {}, runtimePath)
|
|
480
|
-
: await testSpeedtestLinkInNode({ link, config: profile.speed_test, runtime_path: runtimePath }, {
|
|
481
|
-
cwd: projectRoot,
|
|
482
|
-
env: runtimeStageEnv
|
|
483
|
-
});
|
|
484
|
-
speedResults.push(result);
|
|
485
|
-
speedCompleted += 1;
|
|
486
|
-
const passed = result.reachable && result.average_download_mb_s >= speedConfig.min_download_mb_s;
|
|
487
|
-
emit('speedtest_result', {
|
|
488
|
-
completed: speedCompleted,
|
|
489
|
-
total: dedupedLinks.length,
|
|
490
|
-
link: result.link,
|
|
491
|
-
reachable: result.reachable,
|
|
492
|
-
average_download_mb_s: result.average_download_mb_s,
|
|
493
|
-
latency_ms: result.latency_ms,
|
|
494
|
-
passed_threshold: passed,
|
|
495
|
-
error: result.error ?? ''
|
|
496
|
-
});
|
|
497
|
-
emit('log', {
|
|
498
|
-
message: `[speedtest] ${speedCompleted}/${dedupedLinks.length} reachable=${result.reachable} speed=${result.average_download_mb_s}MB/s`
|
|
499
|
-
});
|
|
500
|
-
if (passed) {
|
|
501
|
-
passedSpeedLinks.push(result.link);
|
|
502
|
-
await flushStreamingSpeedArtifacts();
|
|
503
|
-
runStreamingAvailability(result);
|
|
504
|
-
}
|
|
505
|
-
else if (useStreamingStages) {
|
|
506
|
-
await flushStreamingSpeedArtifacts();
|
|
507
|
-
}
|
|
508
|
-
});
|
|
509
|
-
speedTasks.push(task);
|
|
510
|
-
task.catch(() => undefined);
|
|
511
|
-
};
|
|
512
417
|
const onExtractedLinks = async (links) => {
|
|
513
418
|
for (const link of links) {
|
|
514
419
|
rawLinks.push(link);
|
|
@@ -518,7 +423,6 @@ export async function runNodePipeline(options, context = {}) {
|
|
|
518
423
|
}
|
|
519
424
|
streamedDeduped.add(key);
|
|
520
425
|
dedupedLinks.push(link);
|
|
521
|
-
runStreamingSpeedtest(link);
|
|
522
426
|
}
|
|
523
427
|
};
|
|
524
428
|
await setStage('extract', 'running');
|
|
@@ -593,8 +497,81 @@ export async function runNodePipeline(options, context = {}) {
|
|
|
593
497
|
emit('log', {
|
|
594
498
|
message: `[speedtest] runtime_core=${requestedRuntime === 'direct' ? 'direct' : 'mihomo'} probe_url=${speedConfig.probe_url}`
|
|
595
499
|
});
|
|
596
|
-
|
|
597
|
-
|
|
500
|
+
const probes = context.stages?.speedtestProbe
|
|
501
|
+
? await context.stages.speedtestProbe(dedupedLinks, profile.speed_test ?? {}, runtimePath)
|
|
502
|
+
: await probeSpeedtestLinksInNode({ links: dedupedLinks, config: profile.speed_test, runtime_path: runtimePath }, {
|
|
503
|
+
cwd: projectRoot,
|
|
504
|
+
env: runtimeStageEnv
|
|
505
|
+
});
|
|
506
|
+
for (let index = 0; index < probes.length; index += 1) {
|
|
507
|
+
const result = probes[index];
|
|
508
|
+
emit('log', { message: `[speedtest:probe] ${index + 1}/${dedupedLinks.length} reachable=${result.reachable} latency=${result.latency_ms}ms` });
|
|
509
|
+
emit('speedtest_probe_result', {
|
|
510
|
+
completed: index + 1,
|
|
511
|
+
total: dedupedLinks.length,
|
|
512
|
+
link: result.link,
|
|
513
|
+
reachable: result.reachable,
|
|
514
|
+
latency_ms: result.latency_ms,
|
|
515
|
+
error: result.error ?? ''
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
const candidateLinks = selectSpeedtestCandidates(probes, speedConfig.max_download_candidates);
|
|
519
|
+
const reachableCount = probes.filter((probe) => probe.reachable).length;
|
|
520
|
+
emit('log', { message: `[speedtest] selected ${candidateLinks.length}/${reachableCount} reachable links for full download test` });
|
|
521
|
+
emit('speedtest_selected', {
|
|
522
|
+
total_links: dedupedLinks.length,
|
|
523
|
+
reachable_count: reachableCount,
|
|
524
|
+
candidate_count: candidateLinks.length
|
|
525
|
+
});
|
|
526
|
+
const probeByLink = new Map(probes.map((probe) => [probe.link, probe]));
|
|
527
|
+
const resultByLink = new Map();
|
|
528
|
+
for (const probe of probes) {
|
|
529
|
+
if (!probe.reachable) {
|
|
530
|
+
resultByLink.set(probe.link, {
|
|
531
|
+
link: probe.link,
|
|
532
|
+
reachable: false,
|
|
533
|
+
average_download_mb_s: 0,
|
|
534
|
+
latency_ms: probe.latency_ms,
|
|
535
|
+
error: probe.error ?? ''
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
let speedCompleted = 0;
|
|
540
|
+
await forEachWithConcurrency(candidateLinks, speedConfig.concurrency, async (link) => {
|
|
541
|
+
const result = context.stages?.speedtestLink
|
|
542
|
+
? await context.stages.speedtestLink(link, profile.speed_test ?? {}, runtimePath)
|
|
543
|
+
: await testSpeedtestLinkInNode({ link, config: profile.speed_test, runtime_path: runtimePath }, {
|
|
544
|
+
cwd: projectRoot,
|
|
545
|
+
env: runtimeStageEnv
|
|
546
|
+
});
|
|
547
|
+
if (result.reachable && result.latency_ms <= 0) {
|
|
548
|
+
result.latency_ms = probeByLink.get(result.link)?.latency_ms ?? 0;
|
|
549
|
+
}
|
|
550
|
+
resultByLink.set(result.link, result);
|
|
551
|
+
speedCompleted += 1;
|
|
552
|
+
const passed = result.reachable && result.average_download_mb_s >= speedConfig.min_download_mb_s;
|
|
553
|
+
emit('speedtest_result', {
|
|
554
|
+
completed: speedCompleted,
|
|
555
|
+
total: candidateLinks.length,
|
|
556
|
+
link: result.link,
|
|
557
|
+
reachable: result.reachable,
|
|
558
|
+
average_download_mb_s: result.average_download_mb_s,
|
|
559
|
+
latency_ms: result.latency_ms,
|
|
560
|
+
passed_threshold: passed,
|
|
561
|
+
error: result.error ?? ''
|
|
562
|
+
});
|
|
563
|
+
emit('log', {
|
|
564
|
+
message: `[speedtest] ${speedCompleted}/${candidateLinks.length} reachable=${result.reachable} speed=${result.average_download_mb_s}MB/s`
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
speedResults = [
|
|
568
|
+
...probes.filter((probe) => !probe.reachable).map((probe) => resultByLink.get(probe.link)),
|
|
569
|
+
...candidateLinks.map((link) => resultByLink.get(link)).filter((result) => Boolean(result))
|
|
570
|
+
];
|
|
571
|
+
passedSpeedLinks = candidateLinks.filter((link) => {
|
|
572
|
+
const result = resultByLink.get(link);
|
|
573
|
+
return Boolean(result?.reachable && result.average_download_mb_s >= speedConfig.min_download_mb_s);
|
|
574
|
+
});
|
|
598
575
|
}
|
|
599
576
|
else {
|
|
600
577
|
speedResults = context.stages?.speedtest
|
|
@@ -621,20 +598,22 @@ export async function runNodePipeline(options, context = {}) {
|
|
|
621
598
|
}
|
|
622
599
|
const speedResultByLink = new Map(speedResults.map((result) => [result.link, result]));
|
|
623
600
|
const candidateSpeedResults = passedSpeedLinks.map((link) => speedResultByLink.get(link)).filter((result) => Boolean(result));
|
|
624
|
-
availabilityResults =
|
|
625
|
-
?
|
|
626
|
-
:
|
|
627
|
-
|
|
628
|
-
:
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
targets: profile.availability_targets
|
|
633
|
-
}, { cwd: projectRoot, env: runtimeStageEnv });
|
|
601
|
+
availabilityResults = context.stages?.availability
|
|
602
|
+
? await context.stages.availability(candidateSpeedResults, profile.speed_test ?? {}, runtimePath, profile.availability_targets)
|
|
603
|
+
: await checkLinkAvailabilityBatchWithBackend({
|
|
604
|
+
results: candidateSpeedResults,
|
|
605
|
+
config: profile.speed_test ?? {},
|
|
606
|
+
runtime_path: runtimePath,
|
|
607
|
+
targets: profile.availability_targets
|
|
608
|
+
}, { cwd: projectRoot, env: runtimeStageEnv });
|
|
634
609
|
availableLinks = availabilityResults.filter((result) => result.all_passed).map((result) => result.link);
|
|
635
610
|
summary.counts.availability_links = availableLinks.length;
|
|
636
611
|
await writeLines(artifactDir, 'vpn_node_availability.txt', availableLinks);
|
|
637
612
|
await writeJson(artifactDir, 'vpn_node_availability_report.json', availabilityResults);
|
|
613
|
+
if (passedSpeedLinks.length > 0 && availableLinks.length === 0) {
|
|
614
|
+
await setStage('availability', 'failed');
|
|
615
|
+
throw new Error('No links passed availability');
|
|
616
|
+
}
|
|
638
617
|
await setStage('availability', 'success', !useStreamingStages);
|
|
639
618
|
await setStage('postprocess', 'running');
|
|
640
619
|
const countryLookup = context.stages?.countryLookup ?? defaultCountryFor;
|
|
@@ -650,7 +629,7 @@ export async function runNodePipeline(options, context = {}) {
|
|
|
650
629
|
await writeLines(artifactDir, 'vpn_node_emoji.txt', postprocessed.links);
|
|
651
630
|
await setStage('postprocess', 'success');
|
|
652
631
|
await setStage('render', 'running');
|
|
653
|
-
const template = await readFile(
|
|
632
|
+
const template = await readFile(resolveWorkerTemplatePath(projectRoot), 'utf8');
|
|
654
633
|
const rendered = await renderMainDataWithBackend({ template, links: postprocessed.links }, { cwd: projectRoot, env });
|
|
655
634
|
await setStage('render', 'success');
|
|
656
635
|
await setStage('obfuscate', 'running');
|
|
@@ -819,6 +798,8 @@ export async function retryNodePipelineStage(options, context = {}) {
|
|
|
819
798
|
await writeReport();
|
|
820
799
|
throw new Error('No links passed availability');
|
|
821
800
|
}
|
|
801
|
+
const availableLinkSet = new Set(availableLinks);
|
|
802
|
+
availabilityResults = availabilityResults.filter((result) => availableLinkSet.has(result.link));
|
|
822
803
|
await setStage('availability', 'success');
|
|
823
804
|
}
|
|
824
805
|
else if (isStageAtOrAfter(stage, 'postprocess')) {
|
|
@@ -861,7 +842,7 @@ export async function retryNodePipelineStage(options, context = {}) {
|
|
|
861
842
|
throw new Error('No postprocess output available to retry render');
|
|
862
843
|
}
|
|
863
844
|
await setStage('render', 'running');
|
|
864
|
-
const template = await readFile(
|
|
845
|
+
const template = await readFile(resolveWorkerTemplatePath(projectRoot), 'utf8');
|
|
865
846
|
const rendered = await renderMainDataWithBackend({ template, links: finalLinks }, { cwd: projectRoot, env });
|
|
866
847
|
await writeFile(path.join(retryArtifactDir, 'vmess_node.js'), rendered.rendered_source, 'utf8');
|
|
867
848
|
await setStage('render', 'success');
|
|
@@ -1185,7 +1166,7 @@ export async function resumeNodePipeline(options, context = {}) {
|
|
|
1185
1166
|
}
|
|
1186
1167
|
await setStage('postprocess', 'success');
|
|
1187
1168
|
await setStage('render', 'running');
|
|
1188
|
-
const template = await readFile(
|
|
1169
|
+
const template = await readFile(resolveWorkerTemplatePath(projectRoot), 'utf8');
|
|
1189
1170
|
const rendered = await renderMainDataWithBackend({ template, links: postprocessed.links }, { cwd: projectRoot, env });
|
|
1190
1171
|
await writeFile(path.join(artifactDir, 'vmess_node.js'), rendered.rendered_source, 'utf8');
|
|
1191
1172
|
await setStage('render', 'success');
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { spawn as defaultSpawn } from 'node:child_process';
|
|
3
|
-
import { mergeProjectEnv } from '../runtime/env.js';
|
|
4
1
|
const EMOJI_MAP = {
|
|
5
2
|
AE: '🇦🇪',
|
|
6
3
|
AR: '🇦🇷',
|
|
@@ -46,30 +43,6 @@ const DEFAULT_FILTERS = {
|
|
|
46
43
|
per_country_limit: {}
|
|
47
44
|
};
|
|
48
45
|
const LEADING_EMOJI_AND_COUNTRY_PATTERN = /^(?:\S+\s+)?([A-Za-z]{2})\s+(.*)$/;
|
|
49
|
-
const PYTHON_POSTPROCESS_HELPER = `
|
|
50
|
-
import json
|
|
51
|
-
import sys
|
|
52
|
-
from vpn_automation.config.models import FilterConfig
|
|
53
|
-
from vpn_automation.pipeline.postprocess import decorate_link_with_country, select_links_by_country_limit
|
|
54
|
-
|
|
55
|
-
payload = json.load(sys.stdin)
|
|
56
|
-
filters = payload.get("filters")
|
|
57
|
-
ranked_links = payload.get("ranked_links") or []
|
|
58
|
-
filter_config = FilterConfig() if filters is None else FilterConfig(
|
|
59
|
-
excluded_country_codes=list(filters.get("excluded_country_codes") or []),
|
|
60
|
-
per_country_limit=dict(filters.get("per_country_limit") or {}),
|
|
61
|
-
)
|
|
62
|
-
selected = select_links_by_country_limit(
|
|
63
|
-
[(item["link"], {}, item.get("country_code") or item.get("countryCode") or "") for item in ranked_links],
|
|
64
|
-
filter_config,
|
|
65
|
-
)
|
|
66
|
-
country_by_link = {
|
|
67
|
-
item["link"]: item.get("country_code") or item.get("countryCode") or ""
|
|
68
|
-
for item in ranked_links
|
|
69
|
-
}
|
|
70
|
-
json.dump({"links": [decorate_link_with_country(link, country_by_link[link]) for link in selected]}, sys.stdout, ensure_ascii=False)
|
|
71
|
-
sys.stdout.write("\\n")
|
|
72
|
-
`;
|
|
73
46
|
export function parseVmessLink(link) {
|
|
74
47
|
const encoded = link.replace(/^vmess:\/\//, '');
|
|
75
48
|
const padded = encoded + '='.repeat((4 - (encoded.length % 4)) % 4);
|
|
@@ -149,64 +122,7 @@ export function runPostprocess(input) {
|
|
|
149
122
|
links: selected.map((link) => decorateLinkWithCountry(link, countries.get(link) ?? ''))
|
|
150
123
|
};
|
|
151
124
|
}
|
|
152
|
-
export function selectPipelineStageBackend(stage, env = process.env) {
|
|
153
|
-
void stage;
|
|
154
|
-
void env;
|
|
155
|
-
return 'node';
|
|
156
|
-
}
|
|
157
|
-
async function defaultResolvePythonCli(env) {
|
|
158
|
-
// @ts-expect-error Phase 1 runner remains plain ESM JavaScript.
|
|
159
|
-
const runner = await import('../../lib/runner.mjs');
|
|
160
|
-
return runner.resolveOrInstallPythonCli({ env });
|
|
161
|
-
}
|
|
162
|
-
function pythonCommandFor(resolved) {
|
|
163
|
-
const command = resolved.command;
|
|
164
|
-
const name = path.basename(command).toLowerCase();
|
|
165
|
-
if (['autovpn', 'autovpn.exe'].includes(name)) {
|
|
166
|
-
const executable = process.platform === 'win32' ? 'python.exe' : 'python';
|
|
167
|
-
return path.join(path.dirname(command), executable);
|
|
168
|
-
}
|
|
169
|
-
return process.platform === 'win32' ? 'python.exe' : 'python3';
|
|
170
|
-
}
|
|
171
|
-
async function postprocessWithPython(input, options) {
|
|
172
|
-
const env = mergeProjectEnv(options.cwd ?? process.cwd(), options.env ?? process.env);
|
|
173
|
-
const resolved = options.resolvePythonCli ? await options.resolvePythonCli() : await defaultResolvePythonCli(env);
|
|
174
|
-
const helperInput = { ...input, filters: resolveFilters(input.filters) };
|
|
175
|
-
const child = (options.spawn ?? defaultSpawn)(pythonCommandFor(resolved), ['-c', PYTHON_POSTPROCESS_HELPER], {
|
|
176
|
-
cwd: options.cwd ?? process.cwd(),
|
|
177
|
-
env,
|
|
178
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
179
|
-
});
|
|
180
|
-
let stdout = '';
|
|
181
|
-
let stderr = '';
|
|
182
|
-
child.stdout?.on('data', (chunk) => {
|
|
183
|
-
stdout += String(chunk);
|
|
184
|
-
});
|
|
185
|
-
child.stderr?.on('data', (chunk) => {
|
|
186
|
-
stderr += String(chunk);
|
|
187
|
-
});
|
|
188
|
-
const completion = new Promise((resolve, reject) => {
|
|
189
|
-
child.on('error', reject);
|
|
190
|
-
child.on('close', (code) => {
|
|
191
|
-
if (code !== 0) {
|
|
192
|
-
reject(new Error(`Python postprocess backend failed with exit code ${code}: ${stderr.trim()}`));
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
try {
|
|
196
|
-
resolve(JSON.parse(stdout));
|
|
197
|
-
}
|
|
198
|
-
catch (error) {
|
|
199
|
-
reject(new Error(`Python postprocess backend returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`));
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
child.stdin?.write(JSON.stringify(helperInput));
|
|
204
|
-
child.stdin?.end();
|
|
205
|
-
return completion;
|
|
206
|
-
}
|
|
207
125
|
export async function postprocessLinksWithBackend(input, options = {}) {
|
|
208
|
-
|
|
209
|
-
return options.pythonPostprocess ? options.pythonPostprocess(input) : postprocessWithPython(input, options);
|
|
210
|
-
}
|
|
126
|
+
void options;
|
|
211
127
|
return runPostprocess(input);
|
|
212
128
|
}
|
|
@@ -274,6 +274,11 @@ export async function openMihomoRuntime(link, options = {}) {
|
|
|
274
274
|
child.off('error', onStartupError);
|
|
275
275
|
child.on('error', () => { });
|
|
276
276
|
await close();
|
|
277
|
+
if (error instanceof Error
|
|
278
|
+
&& error.name !== 'AbortError'
|
|
279
|
+
&& /^proxy port \d+ did not open in time$/.test(error.message)) {
|
|
280
|
+
error.code = 'AUTOVPN_INTERNAL_TIMEOUT';
|
|
281
|
+
}
|
|
277
282
|
throw error;
|
|
278
283
|
}
|
|
279
284
|
return {
|
package/dist/pipeline/render.js
CHANGED
|
@@ -1,20 +1,4 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { spawn as defaultSpawn } from 'node:child_process';
|
|
3
|
-
import { mergeProjectEnv } from '../runtime/env.js';
|
|
4
1
|
export const MAIN_DATA_PLACEHOLDER = '__MAIN_DATA__';
|
|
5
|
-
const PYTHON_RENDER_HELPER = `
|
|
6
|
-
import json
|
|
7
|
-
import sys
|
|
8
|
-
from vpn_automation.pipeline.render import replace_main_data
|
|
9
|
-
|
|
10
|
-
payload = json.load(sys.stdin)
|
|
11
|
-
json.dump(
|
|
12
|
-
{"rendered_source": replace_main_data(payload["template"], list(payload.get("links") or []))},
|
|
13
|
-
sys.stdout,
|
|
14
|
-
ensure_ascii=False,
|
|
15
|
-
)
|
|
16
|
-
sys.stdout.write("\\n")
|
|
17
|
-
`;
|
|
18
2
|
export function replaceMainData(template, links) {
|
|
19
3
|
const occurrences = template.split(MAIN_DATA_PLACEHOLDER).length - 1;
|
|
20
4
|
if (occurrences !== 1) {
|
|
@@ -27,63 +11,7 @@ export function runRender(input) {
|
|
|
27
11
|
rendered_source: replaceMainData(input.template, input.links ?? [])
|
|
28
12
|
};
|
|
29
13
|
}
|
|
30
|
-
export function selectPipelineStageBackend(stage, env = process.env) {
|
|
31
|
-
void stage;
|
|
32
|
-
void env;
|
|
33
|
-
return 'node';
|
|
34
|
-
}
|
|
35
|
-
async function defaultResolvePythonCli(env) {
|
|
36
|
-
// @ts-expect-error Phase 1 runner remains plain ESM JavaScript.
|
|
37
|
-
const runner = await import('../../lib/runner.mjs');
|
|
38
|
-
return runner.resolveOrInstallPythonCli({ env });
|
|
39
|
-
}
|
|
40
|
-
function pythonCommandFor(resolved) {
|
|
41
|
-
const command = resolved.command;
|
|
42
|
-
const name = path.basename(command).toLowerCase();
|
|
43
|
-
if (['autovpn', 'autovpn.exe'].includes(name)) {
|
|
44
|
-
const executable = process.platform === 'win32' ? 'python.exe' : 'python';
|
|
45
|
-
return path.join(path.dirname(command), executable);
|
|
46
|
-
}
|
|
47
|
-
return process.platform === 'win32' ? 'python.exe' : 'python3';
|
|
48
|
-
}
|
|
49
|
-
async function renderWithPython(input, options) {
|
|
50
|
-
const env = mergeProjectEnv(options.cwd ?? process.cwd(), options.env ?? process.env);
|
|
51
|
-
const resolved = options.resolvePythonCli ? await options.resolvePythonCli() : await defaultResolvePythonCli(env);
|
|
52
|
-
const child = (options.spawn ?? defaultSpawn)(pythonCommandFor(resolved), ['-c', PYTHON_RENDER_HELPER], {
|
|
53
|
-
cwd: options.cwd ?? process.cwd(),
|
|
54
|
-
env,
|
|
55
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
56
|
-
});
|
|
57
|
-
let stdout = '';
|
|
58
|
-
let stderr = '';
|
|
59
|
-
child.stdout?.on('data', (chunk) => {
|
|
60
|
-
stdout += String(chunk);
|
|
61
|
-
});
|
|
62
|
-
child.stderr?.on('data', (chunk) => {
|
|
63
|
-
stderr += String(chunk);
|
|
64
|
-
});
|
|
65
|
-
const completion = new Promise((resolve, reject) => {
|
|
66
|
-
child.on('error', reject);
|
|
67
|
-
child.on('close', (code) => {
|
|
68
|
-
if (code !== 0) {
|
|
69
|
-
reject(new Error(`Python render backend failed with exit code ${code}: ${stderr.trim()}`));
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
try {
|
|
73
|
-
resolve(JSON.parse(stdout));
|
|
74
|
-
}
|
|
75
|
-
catch (error) {
|
|
76
|
-
reject(new Error(`Python render backend returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`));
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
child.stdin?.write(JSON.stringify(input));
|
|
81
|
-
child.stdin?.end();
|
|
82
|
-
return completion;
|
|
83
|
-
}
|
|
84
14
|
export async function renderMainDataWithBackend(input, options = {}) {
|
|
85
|
-
|
|
86
|
-
return options.pythonRender ? options.pythonRender(input) : renderWithPython(input, options);
|
|
87
|
-
}
|
|
15
|
+
void options;
|
|
88
16
|
return runRender(input);
|
|
89
17
|
}
|