@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.
@@ -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
- if (selectPipelineStageBackend('obfuscate', options.env ?? process.env) === 'python') {
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
- await Promise.all(Array.from({ length: workerCount }, async () => {
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
- await mapper(items[index]);
293
- }
294
- }));
295
- }
296
- function createLimiter(concurrency) {
297
- const limit = Math.max(1, Math.trunc(concurrency));
298
- let active = 0;
299
- const queue = [];
300
- return async (task) => {
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
- await Promise.all(speedTasks);
597
- await Promise.all(availabilityTasks);
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 = useStreamingStages
625
- ? availabilityResults
626
- : context.stages?.availability
627
- ? await context.stages.availability(candidateSpeedResults, profile.speed_test ?? {}, runtimePath, profile.availability_targets)
628
- : await checkLinkAvailabilityBatchWithBackend({
629
- results: candidateSpeedResults,
630
- config: profile.speed_test ?? {},
631
- runtime_path: runtimePath,
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(path.join(projectRoot, 'templates', 'vmess_node.js'), 'utf8');
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(path.join(projectRoot, 'templates', 'vmess_node.js'), 'utf8');
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(path.join(projectRoot, 'templates', 'vmess_node.js'), 'utf8');
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
- if (selectPipelineStageBackend('postprocess', options.env ?? process.env) === 'python') {
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 {
@@ -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
- if (selectPipelineStageBackend('render', options.env ?? process.env) === 'python') {
86
- return options.pythonRender ? options.pythonRender(input) : renderWithPython(input, options);
87
- }
15
+ void options;
88
16
  return runRender(input);
89
17
  }