@swimmingliu/autovpn 1.6.1 → 1.6.3
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/dist/backend/node-backend.js +1 -1
- package/dist/backend/select-backend.js +1 -2
- package/dist/cli/main.js +23 -14
- package/dist/cli/native-commands.js +12 -12
- package/dist/doctor/checks.js +16 -1
- package/dist/jobs/commands.js +8 -19
- package/dist/pipeline/availability.js +25 -12
- package/dist/pipeline/dedupe.js +3 -5
- package/dist/pipeline/deploy.js +3 -5
- package/dist/pipeline/extract.js +35 -7
- package/dist/pipeline/obfuscate.js +3 -5
- package/dist/pipeline/orchestrator.js +267 -39
- package/dist/pipeline/postprocess.js +3 -5
- package/dist/pipeline/proxy-runtime.js +68 -6
- package/dist/pipeline/render.js +3 -5
- package/dist/pipeline/speedtest.js +72 -36
- package/dist/runtime/managed-tools.js +9 -2
- package/dist/server/http.js +11 -8
- package/dist/server/runtime.js +40 -3
- package/dist/web/renderer/app.js +62 -4
- package/dist/web/renderer/i18n.js +1 -1
- package/dist/web/renderer/styles.css +321 -1
- package/dist/web/renderer/views.js +8 -2
- package/package.json +1 -1
|
@@ -71,6 +71,24 @@ function emitEvent(callback, eventType, payload) {
|
|
|
71
71
|
callback(eventType, payload);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
+
async function mapWithConcurrency(items, concurrency, worker, onComplete) {
|
|
75
|
+
const limit = Math.max(1, Math.trunc(Number(concurrency) || 1));
|
|
76
|
+
const results = new Array(items.length);
|
|
77
|
+
let nextIndex = 0;
|
|
78
|
+
let completed = 0;
|
|
79
|
+
async function runWorker() {
|
|
80
|
+
while (nextIndex < items.length) {
|
|
81
|
+
const index = nextIndex;
|
|
82
|
+
nextIndex += 1;
|
|
83
|
+
const result = await worker(items[index], index);
|
|
84
|
+
results[index] = result;
|
|
85
|
+
completed += 1;
|
|
86
|
+
onComplete?.(result, index, completed);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => runWorker()));
|
|
90
|
+
return results;
|
|
91
|
+
}
|
|
74
92
|
function defaultNow() {
|
|
75
93
|
return performance.now();
|
|
76
94
|
}
|
|
@@ -84,26 +102,52 @@ async function fetchWithTimeout(fetchImpl, url, timeoutMs) {
|
|
|
84
102
|
clearTimeout(timer);
|
|
85
103
|
}
|
|
86
104
|
}
|
|
87
|
-
async function
|
|
105
|
+
async function withBodyTimeout(operation, timeoutMs) {
|
|
106
|
+
let timer;
|
|
107
|
+
try {
|
|
108
|
+
return await Promise.race([
|
|
109
|
+
operation(),
|
|
110
|
+
new Promise((_resolve, reject) => {
|
|
111
|
+
timer = setTimeout(() => reject(new Error(`response body timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
112
|
+
})
|
|
113
|
+
]);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
if (timer) {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function readResponseBytes(response, maxBytes, timeoutMs) {
|
|
88
122
|
if (response.body?.getReader) {
|
|
89
123
|
const reader = response.body.getReader();
|
|
90
|
-
let
|
|
124
|
+
let timedOut = false;
|
|
91
125
|
try {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
126
|
+
return await withBodyTimeout(async () => {
|
|
127
|
+
let total = 0;
|
|
128
|
+
while (total < maxBytes) {
|
|
129
|
+
const { done, value } = await reader.read();
|
|
130
|
+
if (done) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
total += Math.min(value.byteLength, maxBytes - total);
|
|
96
134
|
}
|
|
97
|
-
|
|
98
|
-
}
|
|
135
|
+
return total;
|
|
136
|
+
}, timeoutMs);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
timedOut = error instanceof Error && error.message.includes('response body timed out');
|
|
140
|
+
throw error;
|
|
99
141
|
}
|
|
100
142
|
finally {
|
|
143
|
+
if (timedOut) {
|
|
144
|
+
await reader.cancel().catch(() => { });
|
|
145
|
+
}
|
|
101
146
|
reader.releaseLock();
|
|
102
147
|
}
|
|
103
|
-
return total;
|
|
104
148
|
}
|
|
105
149
|
if (response.arrayBuffer) {
|
|
106
|
-
return Math.min((await response.arrayBuffer()).byteLength, maxBytes);
|
|
150
|
+
return await withBodyTimeout(async () => Math.min((await response.arrayBuffer()).byteLength, maxBytes), timeoutMs);
|
|
107
151
|
}
|
|
108
152
|
return 0;
|
|
109
153
|
}
|
|
@@ -301,26 +345,23 @@ async function probeLinksDirect(links, config, options) {
|
|
|
301
345
|
}
|
|
302
346
|
const now = options.now ?? defaultNow;
|
|
303
347
|
const timeoutMs = Math.max(1, Number(config.timeout_seconds)) * 1000;
|
|
304
|
-
|
|
305
|
-
for (const link of links) {
|
|
348
|
+
return mapWithConcurrency(links, config.concurrency, async (link) => {
|
|
306
349
|
const started = now();
|
|
307
350
|
try {
|
|
308
351
|
const response = await fetchWithTimeout(fetchImpl, config.probe_url, timeoutMs);
|
|
309
352
|
const elapsed = Math.max(now() - started, 1);
|
|
310
353
|
requireOkResponse(response, new Set([200, 204]));
|
|
311
|
-
|
|
354
|
+
return { link, reachable: true, latency_ms: Math.max(Math.round(elapsed), 1), error: '' };
|
|
312
355
|
}
|
|
313
356
|
catch (error) {
|
|
314
|
-
|
|
357
|
+
return { link, reachable: false, latency_ms: 0, error: error instanceof Error ? error.message : String(error) };
|
|
315
358
|
}
|
|
316
|
-
}
|
|
317
|
-
return results;
|
|
359
|
+
});
|
|
318
360
|
}
|
|
319
361
|
async function probeLinksMihomo(links, config, runtimePath, options) {
|
|
320
362
|
const openRuntime = options.openMihomoRuntime ?? defaultOpenMihomoRuntime;
|
|
321
363
|
const probeDelay = options.probeMihomoProxyDelay ?? defaultProbeMihomoProxyDelay;
|
|
322
|
-
|
|
323
|
-
for (const link of links) {
|
|
364
|
+
return mapWithConcurrency(links, config.concurrency, async (link) => {
|
|
324
365
|
let runtime;
|
|
325
366
|
try {
|
|
326
367
|
runtime = await openRuntime(link, {
|
|
@@ -329,16 +370,15 @@ async function probeLinksMihomo(links, config, runtimePath, options) {
|
|
|
329
370
|
env: options.env
|
|
330
371
|
});
|
|
331
372
|
const latencyMs = await probeDelay(runtime.controllerUrl, runtime.proxyName, config.probe_url, config.timeout_seconds);
|
|
332
|
-
|
|
373
|
+
return { link, reachable: true, latency_ms: latencyMs, error: '' };
|
|
333
374
|
}
|
|
334
375
|
catch (error) {
|
|
335
|
-
|
|
376
|
+
return { link, reachable: false, latency_ms: 0, error: error instanceof Error ? error.message : String(error) };
|
|
336
377
|
}
|
|
337
378
|
finally {
|
|
338
379
|
await runtime?.close();
|
|
339
380
|
}
|
|
340
|
-
}
|
|
341
|
-
return results;
|
|
381
|
+
});
|
|
342
382
|
}
|
|
343
383
|
async function testLinkDirect(link, config, options) {
|
|
344
384
|
const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
@@ -354,7 +394,7 @@ async function testLinkDirect(link, config, options) {
|
|
|
354
394
|
try {
|
|
355
395
|
const response = await fetchWithTimeout(fetchImpl, url, timeoutMs);
|
|
356
396
|
requireOkResponse(response, new Set([200]));
|
|
357
|
-
const total = await readResponseBytes(response, Math.max(1, Number(config.max_download_bytes)));
|
|
397
|
+
const total = await readResponseBytes(response, Math.max(1, Number(config.max_download_bytes)), timeoutMs);
|
|
358
398
|
const elapsedSeconds = Math.max((now() - started) / 1000, 0.001);
|
|
359
399
|
speedValues.push(total / elapsedSeconds / 1024 / 1024);
|
|
360
400
|
}
|
|
@@ -440,7 +480,7 @@ async function speedtestInNode(input, options) {
|
|
|
440
480
|
const config = normalizeSpeedTestConfig(input.config);
|
|
441
481
|
const runtimePath = input.runtime_path ?? '';
|
|
442
482
|
const requestedRuntime = String((options.env ?? process.env).AUTOVPN_SPEEDTEST_RUNTIME ?? '').trim().toLowerCase();
|
|
443
|
-
const useMihomoRuntime = requestedRuntime
|
|
483
|
+
const useMihomoRuntime = requestedRuntime !== 'direct';
|
|
444
484
|
const probeLinks = options.probeLinks ?? ((links) => (useMihomoRuntime
|
|
445
485
|
? probeLinksMihomo(links, config, runtimePath, options)
|
|
446
486
|
: probeLinksDirect(links, config, options)));
|
|
@@ -474,13 +514,10 @@ async function speedtestInNode(input, options) {
|
|
|
474
514
|
latency_ms: probe.latency_ms,
|
|
475
515
|
error: probe.error ?? ''
|
|
476
516
|
}));
|
|
477
|
-
|
|
478
|
-
const result = await testLink(candidateLinks[index], config, { runtime_path: runtimePath });
|
|
517
|
+
const testedResults = await mapWithConcurrency(candidateLinks, config.concurrency, async (link) => (testLink(link, config, { runtime_path: runtimePath })), (result, _index, completed) => {
|
|
479
518
|
if (result.reachable && result.latency_ms <= 0) {
|
|
480
519
|
result.latency_ms = probeByLink.get(result.link)?.latency_ms ?? 0;
|
|
481
520
|
}
|
|
482
|
-
results.push(result);
|
|
483
|
-
const completed = index + 1;
|
|
484
521
|
options.progressCallback?.(`[speedtest] ${completed}/${candidateSet.size} reachable=${result.reachable} speed=${result.average_download_mb_s}MB/s`);
|
|
485
522
|
emitEvent(options.eventCallback, 'speedtest_result', {
|
|
486
523
|
completed,
|
|
@@ -492,15 +529,14 @@ async function speedtestInNode(input, options) {
|
|
|
492
529
|
passed_threshold: result.reachable && result.average_download_mb_s >= config.min_download_mb_s,
|
|
493
530
|
error: result.error ?? ''
|
|
494
531
|
});
|
|
495
|
-
}
|
|
532
|
+
});
|
|
533
|
+
results.push(...testedResults);
|
|
496
534
|
return results;
|
|
497
535
|
}
|
|
498
536
|
export function selectPipelineStageBackend(stage, env = process.env) {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const selected = stageOverride || pipelineOverride || 'node';
|
|
503
|
-
return selected === 'python' ? 'python' : 'node';
|
|
537
|
+
void stage;
|
|
538
|
+
void env;
|
|
539
|
+
return 'node';
|
|
504
540
|
}
|
|
505
541
|
async function defaultResolvePythonCli(env) {
|
|
506
542
|
// @ts-expect-error Phase 1 runner remains plain ESM JavaScript.
|
|
@@ -561,7 +597,7 @@ export async function probeSpeedtestLinksInNode(input, options = {}) {
|
|
|
561
597
|
const config = normalizeSpeedTestConfig(input.config);
|
|
562
598
|
const runtimePath = input.runtime_path ?? '';
|
|
563
599
|
const requestedRuntime = String((options.env ?? process.env).AUTOVPN_SPEEDTEST_RUNTIME ?? '').trim().toLowerCase();
|
|
564
|
-
const useMihomoRuntime = requestedRuntime
|
|
600
|
+
const useMihomoRuntime = requestedRuntime !== 'direct';
|
|
565
601
|
const probeLinks = options.probeLinks ?? ((links) => (useMihomoRuntime
|
|
566
602
|
? probeLinksMihomo(links, config, runtimePath, options)
|
|
567
603
|
: probeLinksDirect(links, config, options)));
|
|
@@ -571,7 +607,7 @@ export async function testSpeedtestLinkInNode(input, options = {}) {
|
|
|
571
607
|
const config = normalizeSpeedTestConfig(input.config);
|
|
572
608
|
const runtimePath = input.runtime_path ?? '';
|
|
573
609
|
const requestedRuntime = String((options.env ?? process.env).AUTOVPN_SPEEDTEST_RUNTIME ?? '').trim().toLowerCase();
|
|
574
|
-
const useMihomoRuntime = requestedRuntime
|
|
610
|
+
const useMihomoRuntime = requestedRuntime !== 'direct';
|
|
575
611
|
const testLink = options.testLink ?? ((link) => (useMihomoRuntime
|
|
576
612
|
? testLinkMihomo(link, config, runtimePath, options)
|
|
577
613
|
: testLinkDirect(link, config, options)));
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { access, mkdir } from 'node:fs/promises';
|
|
4
4
|
import { constants } from 'node:fs';
|
|
5
5
|
import { resolveUserRuntimeRoot } from './paths.js';
|
|
6
|
+
const DEFAULT_MANAGED_NPM_REGISTRY = 'https://registry.npmmirror.com';
|
|
6
7
|
export class ManagedToolError extends Error {
|
|
7
8
|
code;
|
|
8
9
|
constructor(message, code = 'MANAGED_TOOL_ERROR') {
|
|
@@ -98,9 +99,12 @@ async function installManagedTool(packageName, version, installDir, runCommand)
|
|
|
98
99
|
await mkdir(installDir, { recursive: true });
|
|
99
100
|
let result;
|
|
100
101
|
try {
|
|
101
|
-
result = await runCommand(['npm', 'install', '--no-save', '--no-audit', '--no-fund', `${packageName}@${version}`], {
|
|
102
|
+
result = await runCommand(['npm', '--prefix', installDir, 'install', '--no-save', '--no-audit', '--no-fund', `${packageName}@${version}`], {
|
|
102
103
|
cwd: installDir,
|
|
103
|
-
env: {
|
|
104
|
+
env: {
|
|
105
|
+
NPM_CONFIG_YES: 'true',
|
|
106
|
+
NPM_CONFIG_REGISTRY: resolveManagedNpmRegistry()
|
|
107
|
+
}
|
|
104
108
|
});
|
|
105
109
|
}
|
|
106
110
|
catch (error) {
|
|
@@ -110,6 +114,9 @@ async function installManagedTool(packageName, version, installDir, runCommand)
|
|
|
110
114
|
throw new ManagedToolError(`npm install failed for ${packageName}@${version}: ${safeCommandMessage(result)}`, 'MANAGED_TOOL_INSTALL_FAILED');
|
|
111
115
|
}
|
|
112
116
|
}
|
|
117
|
+
function resolveManagedNpmRegistry(env = process.env) {
|
|
118
|
+
return String(env.NPM_CONFIG_REGISTRY || env.npm_config_registry || DEFAULT_MANAGED_NPM_REGISTRY);
|
|
119
|
+
}
|
|
113
120
|
async function verifyToolVersion(binaryPath, cwd, runCommand, source) {
|
|
114
121
|
let result;
|
|
115
122
|
try {
|
package/dist/server/http.js
CHANGED
|
@@ -241,14 +241,17 @@ export async function createAutoVpnServer(options) {
|
|
|
241
241
|
const originHost = options.host === '0.0.0.0' ? '127.0.0.1' : options.host;
|
|
242
242
|
return {
|
|
243
243
|
origin: `http://${originHost}:${port}`,
|
|
244
|
-
close:
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
244
|
+
close: async () => {
|
|
245
|
+
await options.runtime.close?.();
|
|
246
|
+
await new Promise((resolve, reject) => {
|
|
247
|
+
server.close((error) => {
|
|
248
|
+
if (error) {
|
|
249
|
+
reject(error);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
resolve();
|
|
253
|
+
});
|
|
251
254
|
});
|
|
252
|
-
}
|
|
255
|
+
}
|
|
253
256
|
};
|
|
254
257
|
}
|
package/dist/server/runtime.js
CHANGED
|
@@ -136,6 +136,18 @@ function latestJob(projectRoot, env) {
|
|
|
136
136
|
return undefined;
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
|
+
function latestJobEvents(job, maxEvents = 1000) {
|
|
140
|
+
const eventLog = String(job?.event_log ?? '');
|
|
141
|
+
if (!eventLog || !fs.existsSync(eventLog))
|
|
142
|
+
return [];
|
|
143
|
+
try {
|
|
144
|
+
const lines = fs.readFileSync(eventLog, 'utf8').split(/\r?\n/).filter(Boolean);
|
|
145
|
+
return lines.slice(-maxEvents).map((line) => parseJsonLine(line)).filter(Boolean);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
139
151
|
function terminalRunStateFromLatestJob(job, nowMs, ttlMs) {
|
|
140
152
|
const status = String(job?.status ?? '');
|
|
141
153
|
if (status === 'running' || status === 'stopping')
|
|
@@ -163,6 +175,16 @@ function terminalRunStateFromArtifact(artifact, nowMs, ttlMs) {
|
|
|
163
175
|
return undefined;
|
|
164
176
|
return status;
|
|
165
177
|
}
|
|
178
|
+
function runStateFromJobStatus(job) {
|
|
179
|
+
const status = String(job?.status ?? '');
|
|
180
|
+
if (status === 'failed')
|
|
181
|
+
return 'failed';
|
|
182
|
+
if (status === 'success')
|
|
183
|
+
return 'success';
|
|
184
|
+
if (status === 'stopped')
|
|
185
|
+
return 'idle';
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
166
188
|
function jobsRoot(projectRoot, env) {
|
|
167
189
|
return path.join(path.dirname(resolveProfilePath(projectRoot, env)), 'jobs');
|
|
168
190
|
}
|
|
@@ -303,8 +325,11 @@ export function createServerRuntime(options) {
|
|
|
303
325
|
}
|
|
304
326
|
}
|
|
305
327
|
if (!cancelled && runState === 'running') {
|
|
306
|
-
|
|
307
|
-
|
|
328
|
+
const reconciledState = runStateFromJobStatus(latestJob(options.projectRoot, options.env ?? process.env));
|
|
329
|
+
if (reconciledState) {
|
|
330
|
+
runState = reconciledState;
|
|
331
|
+
publish({ type: 'server_state', run_state: runState });
|
|
332
|
+
}
|
|
308
333
|
}
|
|
309
334
|
}
|
|
310
335
|
catch (error) {
|
|
@@ -344,12 +369,14 @@ export function createServerRuntime(options) {
|
|
|
344
369
|
const artifact = normalizeLatestArtifact(options.projectRoot, options.env ?? process.env);
|
|
345
370
|
restoreRunState(artifact);
|
|
346
371
|
const retries = artifactList(options.projectRoot, options.env ?? process.env);
|
|
372
|
+
const job = latestJob(options.projectRoot, options.env ?? process.env);
|
|
347
373
|
return {
|
|
348
374
|
profile: sanitizeProfileForServer(profilePayload(options.projectRoot, options.env)),
|
|
349
375
|
runState,
|
|
350
376
|
artifact,
|
|
351
377
|
retryArtifacts: Array.isArray(retries.items) ? retries.items : [],
|
|
352
|
-
deployment: (artifact?.deployment ?? {})
|
|
378
|
+
deployment: (artifact?.deployment ?? {}),
|
|
379
|
+
logEvents: latestJobEvents(job)
|
|
353
380
|
};
|
|
354
381
|
},
|
|
355
382
|
async saveProfile(profile) {
|
|
@@ -436,6 +463,16 @@ export function createServerRuntime(options) {
|
|
|
436
463
|
subscribe(handler) {
|
|
437
464
|
subscribers.add(handler);
|
|
438
465
|
return () => subscribers.delete(handler);
|
|
466
|
+
},
|
|
467
|
+
async close() {
|
|
468
|
+
unsubscribeLogs?.();
|
|
469
|
+
if (!activeJobId) {
|
|
470
|
+
const artifact = normalizeLatestArtifact(options.projectRoot, options.env ?? process.env);
|
|
471
|
+
restoreRunState(artifact);
|
|
472
|
+
}
|
|
473
|
+
if ((runState === 'running' || runState === 'stopping') && activeJobId) {
|
|
474
|
+
await this.stopRun?.();
|
|
475
|
+
}
|
|
439
476
|
}
|
|
440
477
|
};
|
|
441
478
|
}
|
package/dist/web/renderer/app.js
CHANGED
|
@@ -111,9 +111,11 @@ const state = {
|
|
|
111
111
|
logFilter: '全部',
|
|
112
112
|
settingsDrawer: null,
|
|
113
113
|
isDemo: false,
|
|
114
|
+
runtime: document.body.dataset.runtime === 'web' ? 'web' : 'electron',
|
|
114
115
|
runState: 'idle',
|
|
115
116
|
runResult: 'idle',
|
|
116
117
|
logEntries: [],
|
|
118
|
+
extractDedupedFingerprints: new Set(),
|
|
117
119
|
artifactDir: '',
|
|
118
120
|
retryArtifacts: [],
|
|
119
121
|
selectedRetryArtifactDir: '',
|
|
@@ -163,6 +165,7 @@ async function bootstrap() {
|
|
|
163
165
|
? await window.vpnAutomation.loadState()
|
|
164
166
|
: { profile: await window.vpnAutomation.loadProfile() };
|
|
165
167
|
hydrateInitialRuntimeState(loadedState);
|
|
168
|
+
hydrateHistoricalEvents(loadedState?.logEvents);
|
|
166
169
|
touchUpdate();
|
|
167
170
|
await refreshQrCode();
|
|
168
171
|
if (!loadedState?.retryArtifacts) {
|
|
@@ -196,6 +199,19 @@ function hydrateInitialRuntimeState(loadedState = {}) {
|
|
|
196
199
|
}
|
|
197
200
|
}
|
|
198
201
|
|
|
202
|
+
function hydrateHistoricalEvents(events = []) {
|
|
203
|
+
if (!Array.isArray(events) || events.length === 0) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const previousRunState = state.runState;
|
|
207
|
+
const previousRunResult = state.runResult;
|
|
208
|
+
for (const event of events) {
|
|
209
|
+
handlePipelineEvent(event, { historical: true });
|
|
210
|
+
}
|
|
211
|
+
state.runState = previousRunState;
|
|
212
|
+
state.runResult = previousRunState === 'idle' ? previousRunResult : previousRunState;
|
|
213
|
+
}
|
|
214
|
+
|
|
199
215
|
function bindActions() {
|
|
200
216
|
document.addEventListener('click', handleDocumentClick);
|
|
201
217
|
document.addEventListener('input', handleDocumentInput);
|
|
@@ -299,7 +315,7 @@ function runTone() {
|
|
|
299
315
|
return 'neutral';
|
|
300
316
|
}
|
|
301
317
|
|
|
302
|
-
function handleDocumentClick(event) {
|
|
318
|
+
async function handleDocumentClick(event) {
|
|
303
319
|
const navButton = event.target.closest('[data-page-target]');
|
|
304
320
|
if (navButton) {
|
|
305
321
|
state.activePage = navButton.dataset.pageTarget;
|
|
@@ -422,7 +438,7 @@ function handleDocumentClick(event) {
|
|
|
422
438
|
}
|
|
423
439
|
|
|
424
440
|
if (event.target.closest('[data-drawer-save="save"]')) {
|
|
425
|
-
saveSettingsDrawer();
|
|
441
|
+
await saveSettingsDrawer();
|
|
426
442
|
}
|
|
427
443
|
}
|
|
428
444
|
|
|
@@ -548,6 +564,7 @@ function buildDeployDraft(deploy = {}) {
|
|
|
548
564
|
function sanitizeDeployDraft(draft = {}) {
|
|
549
565
|
const sanitizedDraft = structuredClone(draft);
|
|
550
566
|
delete sanitizedDraft.__autoLinkedPagesProjectUrl;
|
|
567
|
+
sanitizedDraft.min_final_links = Math.max(0, Math.trunc(Number(sanitizedDraft.min_final_links ?? 10) || 0));
|
|
551
568
|
return sanitizedDraft;
|
|
552
569
|
}
|
|
553
570
|
|
|
@@ -932,6 +949,7 @@ async function runPipeline() {
|
|
|
932
949
|
state.stageStatus = {};
|
|
933
950
|
state.counts = {};
|
|
934
951
|
state.sourceCounts = {};
|
|
952
|
+
state.extractDedupedFingerprints = new Set();
|
|
935
953
|
state.logEntries = [];
|
|
936
954
|
state.artifactDir = '';
|
|
937
955
|
state.outputFiles = [];
|
|
@@ -981,6 +999,7 @@ async function retryStage() {
|
|
|
981
999
|
state.stageStatus = {};
|
|
982
1000
|
state.counts = {};
|
|
983
1001
|
state.sourceCounts = {};
|
|
1002
|
+
state.extractDedupedFingerprints = new Set();
|
|
984
1003
|
state.logEntries = [];
|
|
985
1004
|
state.artifactDir = '';
|
|
986
1005
|
state.outputFiles = [];
|
|
@@ -1131,7 +1150,8 @@ function finishRun(result = {}) {
|
|
|
1131
1150
|
void hydrateRetryArtifacts();
|
|
1132
1151
|
}
|
|
1133
1152
|
|
|
1134
|
-
function handlePipelineEvent(event) {
|
|
1153
|
+
function handlePipelineEvent(event, options = {}) {
|
|
1154
|
+
const historical = Boolean(options.historical);
|
|
1135
1155
|
if (event.type === 'server_state') {
|
|
1136
1156
|
const nextRunState = String(event.run_state ?? '');
|
|
1137
1157
|
if (['idle', 'running', 'stopping', 'failed', 'success'].includes(nextRunState)) {
|
|
@@ -1151,6 +1171,10 @@ function handlePipelineEvent(event) {
|
|
|
1151
1171
|
}
|
|
1152
1172
|
|
|
1153
1173
|
if (event.type === 'run_failed') {
|
|
1174
|
+
if (historical) {
|
|
1175
|
+
appendLog(`[run_failed] ${event.error ?? ''}`);
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1154
1178
|
if (state.runState !== 'idle' || state.runResult !== 'failed') {
|
|
1155
1179
|
finishRun({ ok: false, error: event.error });
|
|
1156
1180
|
}
|
|
@@ -1196,16 +1220,34 @@ function handlePipelineEvent(event) {
|
|
|
1196
1220
|
appendLog(`[summary] artifacts: ${event.artifact_dir}`);
|
|
1197
1221
|
const runStatus = String(event.run_status ?? '');
|
|
1198
1222
|
if (runStatus === 'failed') {
|
|
1223
|
+
if (historical) {
|
|
1224
|
+
state.runResult = 'failed';
|
|
1225
|
+
touchUpdate();
|
|
1226
|
+
renderAll();
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1199
1229
|
finishRun({ ok: false, error: event.error });
|
|
1200
1230
|
hydrateArtifactPreview();
|
|
1201
1231
|
return;
|
|
1202
1232
|
}
|
|
1203
1233
|
if (runStatus === 'success') {
|
|
1234
|
+
if (historical) {
|
|
1235
|
+
state.runResult = 'success';
|
|
1236
|
+
touchUpdate();
|
|
1237
|
+
renderAll();
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1204
1240
|
finishRun({ ok: true, code: 0 });
|
|
1205
1241
|
hydrateArtifactPreview();
|
|
1206
1242
|
return;
|
|
1207
1243
|
}
|
|
1208
1244
|
if (runStatus === 'stopped') {
|
|
1245
|
+
if (historical) {
|
|
1246
|
+
state.runResult = 'idle';
|
|
1247
|
+
touchUpdate();
|
|
1248
|
+
renderAll();
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1209
1251
|
finishRun({ stopped: true });
|
|
1210
1252
|
hydrateArtifactPreview();
|
|
1211
1253
|
return;
|
|
@@ -1327,15 +1369,30 @@ function updateExtractMetrics(event) {
|
|
|
1327
1369
|
|
|
1328
1370
|
const previous = state.sourceCounts[sourceName] ?? {};
|
|
1329
1371
|
const rawLinks = Number(event.total_links ?? previous.raw_links ?? 0);
|
|
1372
|
+
const sourceDedupedLinks = Number(event.deduped_links ?? previous.deduped_links ?? rawLinks);
|
|
1373
|
+
if (Array.isArray(event.new_item_fingerprints)) {
|
|
1374
|
+
for (const fingerprint of event.new_item_fingerprints) {
|
|
1375
|
+
if (fingerprint) {
|
|
1376
|
+
state.extractDedupedFingerprints.add(String(fingerprint));
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1330
1380
|
state.sourceCounts = {
|
|
1331
1381
|
...state.sourceCounts,
|
|
1332
1382
|
[sourceName]: {
|
|
1333
1383
|
...previous,
|
|
1334
|
-
raw_links: rawLinks
|
|
1384
|
+
raw_links: rawLinks,
|
|
1385
|
+
deduped_links: sourceDedupedLinks
|
|
1335
1386
|
}
|
|
1336
1387
|
};
|
|
1337
1388
|
state.counts.raw_links = Object.values(state.sourceCounts)
|
|
1338
1389
|
.reduce((total, item) => total + Number(item?.raw_links ?? 0), 0);
|
|
1390
|
+
if (state.extractDedupedFingerprints.size > 0) {
|
|
1391
|
+
state.counts.deduped_links = state.extractDedupedFingerprints.size;
|
|
1392
|
+
} else {
|
|
1393
|
+
state.counts.deduped_links = Object.values(state.sourceCounts)
|
|
1394
|
+
.reduce((total, item) => total + Number(item?.deduped_links ?? 0), 0);
|
|
1395
|
+
}
|
|
1339
1396
|
}
|
|
1340
1397
|
|
|
1341
1398
|
async function hydrateArtifactPreview() {
|
|
@@ -1362,6 +1419,7 @@ function hydrateArtifactState(result) {
|
|
|
1362
1419
|
state.artifactDir = result.artifact_dir;
|
|
1363
1420
|
state.counts = normalizeCounts(result.counts ?? {});
|
|
1364
1421
|
state.sourceCounts = normalizeSourceCounts(result.source_counts ?? {});
|
|
1422
|
+
state.extractDedupedFingerprints = new Set();
|
|
1365
1423
|
state.outputFiles = result.outputFiles ?? [];
|
|
1366
1424
|
state.nodeRows = result.nodeRows ?? [];
|
|
1367
1425
|
state.retryContext = result.retry_context ?? {};
|