@swimmingliu/autovpn 1.6.9 → 1.8.0
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 +5 -0
- package/dist/backend/node-backend.js +7 -22
- package/dist/jobs/commands.js +38 -7
- package/dist/jobs/read.js +9 -22
- package/dist/pipeline/availability.js +38 -36
- package/dist/pipeline/country-codes.js +12 -0
- package/dist/pipeline/geoip.js +232 -0
- package/dist/pipeline/network-retry.js +54 -0
- package/dist/pipeline/orchestrator.js +1027 -844
- package/dist/pipeline/postprocess.js +2 -2
- package/dist/pipeline/proxy-runtime.js +130 -28
- package/dist/pipeline/run-store.js +654 -0
- package/dist/pipeline/speedtest.js +44 -66
- package/dist/pipeline/streaming-coordinator.js +154 -0
- package/dist/web/renderer/app.js +275 -52
- package/dist/web/renderer/assets/fonts/AutoVPNVisualTest-NotoSansSC.woff2 +0 -0
- package/dist/web/renderer/assets/fonts/OFL.txt +92 -0
- package/dist/web/renderer/assets/fonts/README.md +5 -0
- package/dist/web/renderer/i18n.js +1 -1
- package/dist/web/renderer/index.html +1 -1
- package/dist/web/renderer/state.js +8 -0
- package/dist/web/renderer/styles.css +410 -19
- package/dist/web/renderer/views.js +96 -51
- package/package.json +1 -1
package/dist/web/renderer/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getMessages, formatMessage } from './i18n.js';
|
|
2
|
-
import { resolveRunControlState } from './state.js';
|
|
2
|
+
import { createLogViewState, createQrState, resolveRunControlState } from './state.js';
|
|
3
3
|
import {
|
|
4
4
|
addAvailabilityTargetDraft,
|
|
5
5
|
applyAvailabilityTargetDraft,
|
|
@@ -115,6 +115,7 @@ const state = {
|
|
|
115
115
|
runState: 'idle',
|
|
116
116
|
runResult: 'idle',
|
|
117
117
|
logEntries: [],
|
|
118
|
+
logView: createLogViewState(),
|
|
118
119
|
extractDedupedFingerprints: new Set(),
|
|
119
120
|
artifactDir: '',
|
|
120
121
|
retryArtifacts: [],
|
|
@@ -125,12 +126,16 @@ const state = {
|
|
|
125
126
|
outputFiles: [],
|
|
126
127
|
nodeRows: [],
|
|
127
128
|
toast: null,
|
|
128
|
-
|
|
129
|
+
qr: createQrState(),
|
|
129
130
|
runStartedAt: null,
|
|
130
131
|
lastUpdateAt: null,
|
|
131
132
|
modalTransform: ''
|
|
132
133
|
};
|
|
133
134
|
|
|
135
|
+
let settingsDrawerOpener = null;
|
|
136
|
+
let runDetailsMobileBreakpoint = null;
|
|
137
|
+
let nextLogSequence = 0;
|
|
138
|
+
|
|
134
139
|
const elements = {
|
|
135
140
|
sidebarTitle: document.querySelector('#sidebarTitle'),
|
|
136
141
|
sidebarVersion: document.querySelector('#sidebarVersion'),
|
|
@@ -144,6 +149,7 @@ const elements = {
|
|
|
144
149
|
};
|
|
145
150
|
|
|
146
151
|
let toastTimer = null;
|
|
152
|
+
let qrRequestId = 0;
|
|
147
153
|
|
|
148
154
|
async function bootstrap() {
|
|
149
155
|
state.language = 'zh-CN';
|
|
@@ -216,6 +222,9 @@ function bindActions() {
|
|
|
216
222
|
document.addEventListener('click', handleDocumentClick);
|
|
217
223
|
document.addEventListener('input', handleDocumentInput);
|
|
218
224
|
document.addEventListener('change', handleDocumentInput);
|
|
225
|
+
document.addEventListener('keydown', handleSettingsDrawerKeydown);
|
|
226
|
+
document.addEventListener('scroll', handleLogScroll, true);
|
|
227
|
+
window.addEventListener('resize', syncRunDetailsPresentation);
|
|
219
228
|
|
|
220
229
|
document.addEventListener('mousedown', (e) => {
|
|
221
230
|
const header = e.target.closest('.settings-drawer-head');
|
|
@@ -268,18 +277,30 @@ function renderAll() {
|
|
|
268
277
|
state.language,
|
|
269
278
|
state.subtabs
|
|
270
279
|
);
|
|
280
|
+
syncRunDetailsPresentation();
|
|
281
|
+
updateChromeState(messages);
|
|
271
282
|
renderToast();
|
|
272
283
|
}
|
|
273
284
|
|
|
285
|
+
function syncRunDetailsPresentation() {
|
|
286
|
+
const details = document.querySelector('details.run-secondary-controls');
|
|
287
|
+
if (!details) return;
|
|
288
|
+
const mobile = window.matchMedia('(max-width: 720px)').matches;
|
|
289
|
+
const isNewDetails = details.dataset.breakpointInitialized !== 'true';
|
|
290
|
+
if (isNewDetails || mobile !== runDetailsMobileBreakpoint) {
|
|
291
|
+
details.open = !mobile;
|
|
292
|
+
details.dataset.breakpointInitialized = 'true';
|
|
293
|
+
}
|
|
294
|
+
runDetailsMobileBreakpoint = mobile;
|
|
295
|
+
}
|
|
296
|
+
|
|
274
297
|
function renderChrome(messages, viewModel) {
|
|
275
|
-
const controlState = resolveRunControlState(state.runState);
|
|
276
298
|
elements.sidebarTitle.textContent = messages.sidebarTitle;
|
|
277
299
|
elements.sidebarVersion.textContent = messages.sidebarVersion;
|
|
278
300
|
elements.pageTitle.textContent = messages.pageTitles[state.activePage];
|
|
279
301
|
elements.pageSubtitle.textContent = messages.pageSubtitles[state.activePage];
|
|
280
|
-
elements.runStateBadge.textContent = messages.runStateLabels[state.runState] ?? messages.runStateLabels.idle;
|
|
281
|
-
elements.runStateBadge.className = `badge ${runTone()}`;
|
|
282
302
|
elements.sidebarNav.innerHTML = buildSidebarNav(messages, state.activePage);
|
|
303
|
+
const controlState = resolveRunControlState(state.runState);
|
|
283
304
|
elements.pageActions.innerHTML = buildTopbarActions(state.activePage, viewModel, messages, {
|
|
284
305
|
runDisabled: controlState.runDisabled,
|
|
285
306
|
stopDisabled: controlState.stopDisabled,
|
|
@@ -287,6 +308,25 @@ function renderChrome(messages, viewModel) {
|
|
|
287
308
|
stopLabel: messages.stopButton,
|
|
288
309
|
saveLabel: messages.saveButton
|
|
289
310
|
});
|
|
311
|
+
updateChromeState(messages);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function updateChromeState(messages = getMessages(state.language)) {
|
|
315
|
+
const controlState = resolveRunControlState(state.runState);
|
|
316
|
+
elements.runStateBadge.textContent = messages.runStateLabels[state.runState] ?? messages.runStateLabels.idle;
|
|
317
|
+
elements.runStateBadge.className = `badge ${runTone()}`;
|
|
318
|
+
for (const button of document.querySelectorAll('[data-run-action="start"]')) {
|
|
319
|
+
button.disabled = controlState.runDisabled;
|
|
320
|
+
button.setAttribute('aria-busy', String(controlState.isBusy));
|
|
321
|
+
button.textContent = resolveRunButtonLabel(messages);
|
|
322
|
+
}
|
|
323
|
+
for (const button of document.querySelectorAll('[data-run-action="stop"]')) {
|
|
324
|
+
button.disabled = controlState.stopDisabled;
|
|
325
|
+
button.setAttribute('aria-busy', String(state.runState === 'stopping'));
|
|
326
|
+
}
|
|
327
|
+
for (const button of document.querySelectorAll('[data-action="retry-stage"]')) {
|
|
328
|
+
button.disabled = controlState.isBusy || !state.selectedRetryArtifactDir || !state.selectedRetryStage;
|
|
329
|
+
}
|
|
290
330
|
}
|
|
291
331
|
|
|
292
332
|
function resolveRunButtonLabel(messages) {
|
|
@@ -316,6 +356,26 @@ function runTone() {
|
|
|
316
356
|
}
|
|
317
357
|
|
|
318
358
|
async function handleDocumentClick(event) {
|
|
359
|
+
const jumpLatest = event.target.closest('[data-log-jump-latest]');
|
|
360
|
+
if (jumpLatest) {
|
|
361
|
+
state.logView.follow = true;
|
|
362
|
+
state.logView.unseenCount = 0;
|
|
363
|
+
renderAll();
|
|
364
|
+
scrollLogToLatest();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const undoClear = event.target.closest('[data-log-undo-clear]');
|
|
369
|
+
if (undoClear && state.logView.clearedSnapshot) {
|
|
370
|
+
state.logEntries = state.logView.clearedSnapshot;
|
|
371
|
+
state.logView.clearedSnapshot = null;
|
|
372
|
+
state.logView.follow = true;
|
|
373
|
+
state.logView.unseenCount = 0;
|
|
374
|
+
state.toast = null;
|
|
375
|
+
renderAll();
|
|
376
|
+
scrollLogToLatest();
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
319
379
|
const navButton = event.target.closest('[data-page-target]');
|
|
320
380
|
if (navButton) {
|
|
321
381
|
state.activePage = navButton.dataset.pageTarget;
|
|
@@ -347,7 +407,7 @@ async function handleDocumentClick(event) {
|
|
|
347
407
|
|
|
348
408
|
const copyButton = event.target.closest('[data-copy-text]');
|
|
349
409
|
if (copyButton) {
|
|
350
|
-
copyText(copyButton.dataset.copyText);
|
|
410
|
+
copyText(copyButton.dataset.copyText, { control: copyButton, successLog: '已复制订阅链接' });
|
|
351
411
|
return;
|
|
352
412
|
}
|
|
353
413
|
|
|
@@ -389,10 +449,15 @@ async function handleDocumentClick(event) {
|
|
|
389
449
|
successToast: formatMessage(messages.copiedNodesToastMessage, { count: nodeLinks.length }),
|
|
390
450
|
successLog: formatMessage(messages.copiedNodesLogMessage, { count: nodeLinks.length }),
|
|
391
451
|
failureToast: formatMessage(messages.copyFailedToastMessage, { error: '{error}' }),
|
|
392
|
-
failureLog: formatMessage(messages.copyFailedLogMessage, { error: '{error}' })
|
|
452
|
+
failureLog: formatMessage(messages.copyFailedLogMessage, { error: '{error}' }),
|
|
453
|
+
control: action
|
|
393
454
|
});
|
|
394
455
|
return;
|
|
395
456
|
}
|
|
457
|
+
if (action?.dataset.action === 'retry-qr') {
|
|
458
|
+
refreshQrCode();
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
396
461
|
if (action?.dataset.action === 'retry-stage') {
|
|
397
462
|
retryStage();
|
|
398
463
|
return;
|
|
@@ -402,8 +467,12 @@ async function handleDocumentClick(event) {
|
|
|
402
467
|
return;
|
|
403
468
|
}
|
|
404
469
|
if (action?.dataset.action === 'clear-log') {
|
|
470
|
+
state.logView.clearedSnapshot = [...state.logEntries];
|
|
405
471
|
state.logEntries = [];
|
|
472
|
+
state.logView.follow = true;
|
|
473
|
+
state.logView.unseenCount = 0;
|
|
406
474
|
renderAll();
|
|
475
|
+
showToast({ tone: 'neutral', message: '日志已清空', durationMs: 5000, action: 'undo-clear' });
|
|
407
476
|
return;
|
|
408
477
|
}
|
|
409
478
|
if (action?.dataset.action === 'open-log-file') {
|
|
@@ -413,7 +482,7 @@ async function handleDocumentClick(event) {
|
|
|
413
482
|
|
|
414
483
|
const settingsCard = event.target.closest('[data-settings-card]');
|
|
415
484
|
if (settingsCard) {
|
|
416
|
-
openSettingsDrawer(settingsCard.dataset.settingsCard);
|
|
485
|
+
openSettingsDrawer(settingsCard.dataset.settingsCard, settingsCard);
|
|
417
486
|
return;
|
|
418
487
|
}
|
|
419
488
|
|
|
@@ -424,16 +493,12 @@ async function handleDocumentClick(event) {
|
|
|
424
493
|
}
|
|
425
494
|
|
|
426
495
|
if (event.target.closest('[data-drawer-dismiss="backdrop"]')) {
|
|
427
|
-
|
|
428
|
-
state.modalTransform = '';
|
|
429
|
-
renderAll();
|
|
496
|
+
closeSettingsDrawer({ restoreFocus: true });
|
|
430
497
|
return;
|
|
431
498
|
}
|
|
432
499
|
|
|
433
500
|
if (event.target.closest('[data-drawer-close="cancel"]')) {
|
|
434
|
-
|
|
435
|
-
state.modalTransform = '';
|
|
436
|
-
renderAll();
|
|
501
|
+
closeSettingsDrawer({ restoreFocus: true });
|
|
437
502
|
return;
|
|
438
503
|
}
|
|
439
504
|
|
|
@@ -661,16 +726,20 @@ async function writeClipboardText(text) {
|
|
|
661
726
|
throw new Error('clipboard_unavailable');
|
|
662
727
|
}
|
|
663
728
|
|
|
664
|
-
function showToast({ tone = 'neutral', message, durationMs = 2400 }) {
|
|
729
|
+
function showToast({ tone = 'neutral', message, durationMs = 2400, action = null }) {
|
|
665
730
|
if (!message) {
|
|
666
731
|
return;
|
|
667
732
|
}
|
|
668
|
-
state.toast = { tone, message };
|
|
733
|
+
state.toast = { tone, message, action };
|
|
669
734
|
renderToast();
|
|
670
735
|
if (toastTimer) {
|
|
671
736
|
clearTimeout(toastTimer);
|
|
672
737
|
}
|
|
673
738
|
toastTimer = window.setTimeout(() => {
|
|
739
|
+
if (state.toast?.action === 'undo-clear') {
|
|
740
|
+
state.logView.clearedSnapshot = null;
|
|
741
|
+
renderActiveRuntimeSections(buildViewModel(state, getMessages(state.language), state.language));
|
|
742
|
+
}
|
|
674
743
|
state.toast = null;
|
|
675
744
|
renderToast();
|
|
676
745
|
toastTimer = null;
|
|
@@ -688,6 +757,7 @@ function renderToast() {
|
|
|
688
757
|
elements.toastRoot.innerHTML = `
|
|
689
758
|
<div class="toast ${escapeHtml(state.toast.tone || 'neutral')}" data-toast data-toast-tone="${escapeHtml(state.toast.tone || 'neutral')}">
|
|
690
759
|
${escapeHtml(state.toast.message)}
|
|
760
|
+
${state.toast.action === 'undo-clear' ? '<button class="btn small" data-log-undo-clear type="button">撤销</button>' : ''}
|
|
691
761
|
</div>
|
|
692
762
|
`;
|
|
693
763
|
}
|
|
@@ -700,19 +770,27 @@ async function copyText(value, options = {}) {
|
|
|
700
770
|
return;
|
|
701
771
|
}
|
|
702
772
|
|
|
773
|
+
const control = options.control;
|
|
774
|
+
if (control?.getAttribute('aria-busy') === 'true') return;
|
|
775
|
+
if (control) {
|
|
776
|
+
control.disabled = true;
|
|
777
|
+
control.setAttribute('aria-busy', 'true');
|
|
778
|
+
}
|
|
703
779
|
try {
|
|
704
780
|
await writeClipboardText(text);
|
|
705
781
|
showToast({ tone: 'success', message: options.successToast ?? messages.copiedToastMessage });
|
|
706
|
-
appendLog(options.successLog ??
|
|
782
|
+
appendLog(options.successLog ?? '已复制内容');
|
|
707
783
|
} catch (error) {
|
|
708
|
-
const detail = resolveErrorMessage(error);
|
|
709
784
|
showToast({
|
|
710
785
|
tone: 'danger',
|
|
711
|
-
message:
|
|
786
|
+
message: options.safeFailureToast ?? '复制失败,请重试'
|
|
712
787
|
});
|
|
713
|
-
appendLog(
|
|
714
|
-
|
|
715
|
-
)
|
|
788
|
+
appendLog(options.safeFailureLog ?? '复制操作失败');
|
|
789
|
+
} finally {
|
|
790
|
+
if (control?.isConnected) {
|
|
791
|
+
control.disabled = false;
|
|
792
|
+
control.setAttribute('aria-busy', 'false');
|
|
793
|
+
}
|
|
716
794
|
}
|
|
717
795
|
}
|
|
718
796
|
|
|
@@ -758,19 +836,27 @@ async function openCurrentLogFile() {
|
|
|
758
836
|
}
|
|
759
837
|
|
|
760
838
|
async function refreshQrCode() {
|
|
839
|
+
const requestId = ++qrRequestId;
|
|
761
840
|
const subscriptionUrl = resolveActiveSubscriptionUrl();
|
|
762
841
|
if (!subscriptionUrl || !window.vpnAutomation?.generateQr) {
|
|
763
|
-
state.
|
|
842
|
+
state.qr = createQrState('unavailable', '', '当前环境不支持二维码生成');
|
|
843
|
+
renderAll();
|
|
764
844
|
return;
|
|
765
845
|
}
|
|
766
846
|
|
|
847
|
+
state.qr = createQrState('loading');
|
|
848
|
+
renderAll();
|
|
767
849
|
try {
|
|
768
850
|
const result = await window.vpnAutomation.generateQr(subscriptionUrl);
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
851
|
+
if (requestId !== qrRequestId || subscriptionUrl !== resolveActiveSubscriptionUrl()) return;
|
|
852
|
+
state.qr = result?.dataUrl
|
|
853
|
+
? createQrState('success', result.dataUrl)
|
|
854
|
+
: createQrState('unavailable', '', '二维码服务未返回图片');
|
|
855
|
+
} catch (error) {
|
|
856
|
+
if (requestId !== qrRequestId || subscriptionUrl !== resolveActiveSubscriptionUrl()) return;
|
|
857
|
+
state.qr = createQrState('error', '', resolveErrorMessage(error));
|
|
773
858
|
}
|
|
859
|
+
renderAll();
|
|
774
860
|
}
|
|
775
861
|
|
|
776
862
|
function resolveActiveSubscriptionUrl() {
|
|
@@ -786,7 +872,7 @@ function resolveActiveSubscriptionUrl() {
|
|
|
786
872
|
return `${baseUrl}?format=${encodeURIComponent(format.toLowerCase().replaceAll(' ', '-'))}`;
|
|
787
873
|
}
|
|
788
874
|
|
|
789
|
-
function openSettingsDrawer(section) {
|
|
875
|
+
function openSettingsDrawer(section, opener = document.activeElement) {
|
|
790
876
|
if (!state.profile) {
|
|
791
877
|
return;
|
|
792
878
|
}
|
|
@@ -797,8 +883,52 @@ function openSettingsDrawer(section) {
|
|
|
797
883
|
}
|
|
798
884
|
|
|
799
885
|
state.modalTransform = '';
|
|
886
|
+
settingsDrawerOpener = opener?.matches?.('[data-settings-card]')
|
|
887
|
+
? `[data-settings-card="${CSS.escape(opener.dataset.settingsCard)}"]`
|
|
888
|
+
: opener?.id
|
|
889
|
+
? `#${CSS.escape(opener.id)}`
|
|
890
|
+
: null;
|
|
800
891
|
state.settingsDrawer = { section, draft };
|
|
801
892
|
renderAll();
|
|
893
|
+
queueMicrotask(() => {
|
|
894
|
+
const dialog = document.querySelector('[data-settings-dialog]');
|
|
895
|
+
const focusTarget = dialog?.querySelector('input:not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled])');
|
|
896
|
+
focusTarget?.focus();
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function closeSettingsDrawer({ restoreFocus = true } = {}) {
|
|
901
|
+
const openerSelector = settingsDrawerOpener;
|
|
902
|
+
state.settingsDrawer = null;
|
|
903
|
+
state.modalTransform = '';
|
|
904
|
+
settingsDrawerOpener = null;
|
|
905
|
+
renderAll();
|
|
906
|
+
if (restoreFocus && openerSelector) {
|
|
907
|
+
queueMicrotask(() => document.querySelector(openerSelector)?.focus());
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function handleSettingsDrawerKeydown(event) {
|
|
912
|
+
const dialog = document.querySelector('[data-settings-dialog]');
|
|
913
|
+
if (!dialog) return;
|
|
914
|
+
if (event.key === 'Escape') {
|
|
915
|
+
event.preventDefault();
|
|
916
|
+
closeSettingsDrawer({ restoreFocus: true });
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (event.key !== 'Tab') return;
|
|
920
|
+
const focusable = [...dialog.querySelectorAll('button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])')]
|
|
921
|
+
.filter((element) => !element.hidden && element.getClientRects().length > 0);
|
|
922
|
+
if (!focusable.length) return;
|
|
923
|
+
const first = focusable[0];
|
|
924
|
+
const last = focusable.at(-1);
|
|
925
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
926
|
+
event.preventDefault();
|
|
927
|
+
last.focus();
|
|
928
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
929
|
+
event.preventDefault();
|
|
930
|
+
first.focus();
|
|
931
|
+
}
|
|
802
932
|
}
|
|
803
933
|
|
|
804
934
|
function buildSettingsDraft(section) {
|
|
@@ -833,8 +963,6 @@ async function saveSettingsDrawer() {
|
|
|
833
963
|
if (section !== 'about') {
|
|
834
964
|
state.profile[section] = resolveSettingsDraftPayload(section, draft);
|
|
835
965
|
}
|
|
836
|
-
state.settingsDrawer = null;
|
|
837
|
-
state.modalTransform = '';
|
|
838
966
|
touchUpdate();
|
|
839
967
|
if (section === 'deploy') {
|
|
840
968
|
await refreshQrCode();
|
|
@@ -851,9 +979,10 @@ async function saveSettingsDrawer() {
|
|
|
851
979
|
`[settings] deploy saved project=${state.profile.deploy.project_name} url=${state.profile.deploy.pages_project_url}`
|
|
852
980
|
);
|
|
853
981
|
}
|
|
982
|
+
closeSettingsDrawer({ restoreFocus: true });
|
|
854
983
|
return;
|
|
855
984
|
}
|
|
856
|
-
|
|
985
|
+
closeSettingsDrawer({ restoreFocus: true });
|
|
857
986
|
}
|
|
858
987
|
|
|
859
988
|
async function saveProfile({ silent = false } = {}) {
|
|
@@ -957,7 +1086,7 @@ async function runPipeline() {
|
|
|
957
1086
|
state.selectedRetryStage = '';
|
|
958
1087
|
state.runStartedAt = Date.now();
|
|
959
1088
|
touchUpdate();
|
|
960
|
-
|
|
1089
|
+
renderRuntimeOnly();
|
|
961
1090
|
appendLog(messages.pipelineStarted);
|
|
962
1091
|
|
|
963
1092
|
if (runOptions.saveBeforeRun) {
|
|
@@ -968,7 +1097,7 @@ async function runPipeline() {
|
|
|
968
1097
|
state.runState = 'idle';
|
|
969
1098
|
state.runResult = 'demo';
|
|
970
1099
|
touchUpdate();
|
|
971
|
-
|
|
1100
|
+
renderRuntimeOnly();
|
|
972
1101
|
appendLog(formatMessage(messages.pipelineFinished, { code: 'demo' }));
|
|
973
1102
|
return;
|
|
974
1103
|
}
|
|
@@ -983,7 +1112,7 @@ async function runPipeline() {
|
|
|
983
1112
|
state.runResult = 'failed';
|
|
984
1113
|
state.runStartedAt = null;
|
|
985
1114
|
touchUpdate();
|
|
986
|
-
|
|
1115
|
+
renderRuntimeOnly();
|
|
987
1116
|
appendLog(formatMessage(messages.pipelineFailed, { error: error.message }));
|
|
988
1117
|
}
|
|
989
1118
|
}
|
|
@@ -1006,7 +1135,7 @@ async function retryStage() {
|
|
|
1006
1135
|
state.nodeRows = [];
|
|
1007
1136
|
state.runStartedAt = Date.now();
|
|
1008
1137
|
touchUpdate();
|
|
1009
|
-
|
|
1138
|
+
renderRuntimeOnly();
|
|
1010
1139
|
appendLog(`[retry] artifact=${state.selectedRetryArtifactDir} stage=${state.selectedRetryStage}`);
|
|
1011
1140
|
|
|
1012
1141
|
const runOptions = collectRunOptions();
|
|
@@ -1019,7 +1148,7 @@ async function retryStage() {
|
|
|
1019
1148
|
state.runResult = 'failed';
|
|
1020
1149
|
state.runStartedAt = null;
|
|
1021
1150
|
touchUpdate();
|
|
1022
|
-
|
|
1151
|
+
renderRuntimeOnly();
|
|
1023
1152
|
appendLog(formatMessage(messages.pipelineFailed, { error: 'retry bridge unavailable' }));
|
|
1024
1153
|
return;
|
|
1025
1154
|
}
|
|
@@ -1038,7 +1167,7 @@ async function retryStage() {
|
|
|
1038
1167
|
state.runResult = 'failed';
|
|
1039
1168
|
state.runStartedAt = null;
|
|
1040
1169
|
touchUpdate();
|
|
1041
|
-
|
|
1170
|
+
renderRuntimeOnly();
|
|
1042
1171
|
appendLog(formatMessage(messages.pipelineFailed, { error: error.message }));
|
|
1043
1172
|
}
|
|
1044
1173
|
}
|
|
@@ -1085,7 +1214,7 @@ async function stopPipeline() {
|
|
|
1085
1214
|
|
|
1086
1215
|
state.runState = 'stopping';
|
|
1087
1216
|
touchUpdate();
|
|
1088
|
-
|
|
1217
|
+
renderRuntimeOnly();
|
|
1089
1218
|
appendLog(messages.pipelineStopping);
|
|
1090
1219
|
|
|
1091
1220
|
try {
|
|
@@ -1101,7 +1230,7 @@ async function stopPipeline() {
|
|
|
1101
1230
|
state.runState = 'idle';
|
|
1102
1231
|
state.runStartedAt = null;
|
|
1103
1232
|
touchUpdate();
|
|
1104
|
-
|
|
1233
|
+
renderRuntimeOnly();
|
|
1105
1234
|
appendLog(messages.stopUnavailable);
|
|
1106
1235
|
}
|
|
1107
1236
|
return;
|
|
@@ -1122,7 +1251,7 @@ function finishRun(result = {}) {
|
|
|
1122
1251
|
state.runState = 'idle';
|
|
1123
1252
|
state.runResult = 'stopped';
|
|
1124
1253
|
touchUpdate();
|
|
1125
|
-
|
|
1254
|
+
renderRuntimeOnly();
|
|
1126
1255
|
appendLog(messages.pipelineStopped);
|
|
1127
1256
|
void hydrateRetryArtifacts();
|
|
1128
1257
|
return;
|
|
@@ -1132,7 +1261,7 @@ function finishRun(result = {}) {
|
|
|
1132
1261
|
state.runState = 'success';
|
|
1133
1262
|
state.runResult = 'success';
|
|
1134
1263
|
touchUpdate();
|
|
1135
|
-
|
|
1264
|
+
renderRuntimeOnly();
|
|
1136
1265
|
appendLog(formatMessage(messages.pipelineFinished, { code: result.code ?? 0 }));
|
|
1137
1266
|
void hydrateRetryArtifacts();
|
|
1138
1267
|
return;
|
|
@@ -1141,7 +1270,7 @@ function finishRun(result = {}) {
|
|
|
1141
1270
|
state.runState = 'failed';
|
|
1142
1271
|
state.runResult = 'failed';
|
|
1143
1272
|
touchUpdate();
|
|
1144
|
-
|
|
1273
|
+
renderRuntimeOnly();
|
|
1145
1274
|
if (result.error) {
|
|
1146
1275
|
appendLog(formatMessage(messages.pipelineFailed, { error: result.error }));
|
|
1147
1276
|
} else {
|
|
@@ -1165,7 +1294,7 @@ function handlePipelineEvent(event, options = {}) {
|
|
|
1165
1294
|
state.runResult = 'success';
|
|
1166
1295
|
}
|
|
1167
1296
|
touchUpdate();
|
|
1168
|
-
|
|
1297
|
+
renderRuntimeOnly();
|
|
1169
1298
|
}
|
|
1170
1299
|
return;
|
|
1171
1300
|
}
|
|
@@ -1187,7 +1316,7 @@ function handlePipelineEvent(event, options = {}) {
|
|
|
1187
1316
|
}
|
|
1188
1317
|
|
|
1189
1318
|
if (event.type === 'stage') {
|
|
1190
|
-
state.stageStatus[event.stage] = event.status;
|
|
1319
|
+
state.stageStatus[event.stage] = normalizeStageStatus(event.status);
|
|
1191
1320
|
appendLog(`[stage] ${event.stage} ${event.status}`, {
|
|
1192
1321
|
kind: 'stage',
|
|
1193
1322
|
stage: event.stage,
|
|
@@ -1199,7 +1328,7 @@ function handlePipelineEvent(event, options = {}) {
|
|
|
1199
1328
|
}
|
|
1200
1329
|
|
|
1201
1330
|
if (event.type === 'summary') {
|
|
1202
|
-
state.stageStatus = event.stage_status
|
|
1331
|
+
state.stageStatus = normalizeStageStatuses(event.stage_status);
|
|
1203
1332
|
state.counts = normalizeCounts(event.counts ?? {});
|
|
1204
1333
|
state.sourceCounts = normalizeSourceCounts(event.source_counts ?? state.sourceCounts);
|
|
1205
1334
|
state.retryContext = event.retry_context ?? state.retryContext ?? {};
|
|
@@ -1332,12 +1461,38 @@ function handlePipelineEvent(event, options = {}) {
|
|
|
1332
1461
|
}
|
|
1333
1462
|
|
|
1334
1463
|
if (event.type === 'run_started') {
|
|
1335
|
-
|
|
1464
|
+
const nextArtifactDir = String(event.artifact_dir ?? '');
|
|
1465
|
+
if (nextArtifactDir !== state.artifactDir) {
|
|
1466
|
+
resetRunScopedProgress();
|
|
1467
|
+
state.runState = 'running';
|
|
1468
|
+
state.runResult = 'running';
|
|
1469
|
+
state.runStartedAt = Date.now();
|
|
1470
|
+
}
|
|
1471
|
+
state.artifactDir = nextArtifactDir;
|
|
1336
1472
|
touchUpdate();
|
|
1337
1473
|
renderAll();
|
|
1338
1474
|
}
|
|
1339
1475
|
}
|
|
1340
1476
|
|
|
1477
|
+
function normalizeStageStatus(status) {
|
|
1478
|
+
return status === 'skipped' ? '已跳过' : status;
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function normalizeStageStatuses(stageStatuses = {}) {
|
|
1482
|
+
return Object.fromEntries(
|
|
1483
|
+
Object.entries(stageStatuses ?? {}).map(([stage, status]) => [stage, normalizeStageStatus(status)])
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function resetRunScopedProgress() {
|
|
1488
|
+
state.stageStatus = {};
|
|
1489
|
+
state.counts = {};
|
|
1490
|
+
state.sourceCounts = {};
|
|
1491
|
+
state.extractDedupedFingerprints = new Set();
|
|
1492
|
+
state.outputFiles = [];
|
|
1493
|
+
state.nodeRows = [];
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1341
1496
|
function normalizeCounts(counts) {
|
|
1342
1497
|
return {
|
|
1343
1498
|
...counts,
|
|
@@ -1355,7 +1510,9 @@ function normalizeSourceCounts(sourceCounts = {}) {
|
|
|
1355
1510
|
{
|
|
1356
1511
|
...counts,
|
|
1357
1512
|
raw_links: Number(counts?.raw_links ?? 0),
|
|
1358
|
-
|
|
1513
|
+
...(counts && Object.hasOwn(counts, 'deduped_links')
|
|
1514
|
+
? { deduped_links: Number(counts.deduped_links) }
|
|
1515
|
+
: {})
|
|
1359
1516
|
}
|
|
1360
1517
|
])
|
|
1361
1518
|
);
|
|
@@ -1368,8 +1525,16 @@ function updateExtractMetrics(event) {
|
|
|
1368
1525
|
}
|
|
1369
1526
|
|
|
1370
1527
|
const previous = state.sourceCounts[sourceName] ?? {};
|
|
1371
|
-
const
|
|
1372
|
-
const
|
|
1528
|
+
const previousRawLinks = validNonNegativeNumber(previous.raw_links) ?? 0;
|
|
1529
|
+
const previousDedupedLinks = validNonNegativeNumber(previous.deduped_links) ?? 0;
|
|
1530
|
+
const rawLinks = Math.max(previousRawLinks, validNonNegativeNumber(event.total_links) ?? previousRawLinks);
|
|
1531
|
+
const eventDedupedLinks = event.deduped_links == null
|
|
1532
|
+
? rawLinks
|
|
1533
|
+
: validNonNegativeNumber(event.deduped_links) ?? previousDedupedLinks;
|
|
1534
|
+
const sourceDedupedLinks = Math.max(
|
|
1535
|
+
previousDedupedLinks,
|
|
1536
|
+
eventDedupedLinks
|
|
1537
|
+
);
|
|
1373
1538
|
if (Array.isArray(event.new_item_fingerprints)) {
|
|
1374
1539
|
for (const fingerprint of event.new_item_fingerprints) {
|
|
1375
1540
|
if (fingerprint) {
|
|
@@ -1395,6 +1560,11 @@ function updateExtractMetrics(event) {
|
|
|
1395
1560
|
}
|
|
1396
1561
|
}
|
|
1397
1562
|
|
|
1563
|
+
function validNonNegativeNumber(value) {
|
|
1564
|
+
const number = Number(value);
|
|
1565
|
+
return Number.isFinite(number) && number >= 0 ? number : null;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1398
1568
|
async function hydrateArtifactPreview() {
|
|
1399
1569
|
if (!state.artifactDir || !window.vpnAutomation?.previewArtifact) {
|
|
1400
1570
|
state.outputFiles = [];
|
|
@@ -1430,7 +1600,7 @@ function hydrateArtifactState(result) {
|
|
|
1430
1600
|
state.runResult = 'failed';
|
|
1431
1601
|
}
|
|
1432
1602
|
if (result.stage_status) {
|
|
1433
|
-
state.stageStatus = result.stage_status;
|
|
1603
|
+
state.stageStatus = normalizeStageStatuses(result.stage_status);
|
|
1434
1604
|
}
|
|
1435
1605
|
}
|
|
1436
1606
|
|
|
@@ -1491,7 +1661,23 @@ function resolveDefaultRetryStage(artifact) {
|
|
|
1491
1661
|
}
|
|
1492
1662
|
|
|
1493
1663
|
function appendLog(message, overrides = {}) {
|
|
1494
|
-
|
|
1664
|
+
const logCenter = document.querySelector('#logCenterTable');
|
|
1665
|
+
if (logCenter) {
|
|
1666
|
+
state.logView.follow = logCenter.scrollHeight - logCenter.scrollTop - logCenter.clientHeight <= 32;
|
|
1667
|
+
if (!state.logView.follow) {
|
|
1668
|
+
const streamTop = logCenter.getBoundingClientRect().top;
|
|
1669
|
+
state.logView.anchorId = [...logCenter.querySelectorAll('.log-line')]
|
|
1670
|
+
.find((line) => line.getBoundingClientRect().bottom >= streamTop)
|
|
1671
|
+
?.dataset.logId ?? state.logView.anchorId;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
if (!state.logView.follow) {
|
|
1675
|
+
state.logView.unseenCount += 1;
|
|
1676
|
+
}
|
|
1677
|
+
state.logEntries.push({
|
|
1678
|
+
...classifyLogEntry(message, overrides),
|
|
1679
|
+
logId: `live-${++nextLogSequence}`
|
|
1680
|
+
});
|
|
1495
1681
|
touchUpdate();
|
|
1496
1682
|
renderRuntimeOnly({ chrome: false });
|
|
1497
1683
|
}
|
|
@@ -1500,7 +1686,7 @@ function renderRuntimeOnly({ chrome = true } = {}) {
|
|
|
1500
1686
|
const messages = getMessages(state.language);
|
|
1501
1687
|
const viewModel = buildViewModel(state, messages, state.language);
|
|
1502
1688
|
if (chrome) {
|
|
1503
|
-
|
|
1689
|
+
updateChromeState(messages);
|
|
1504
1690
|
}
|
|
1505
1691
|
renderActiveRuntimeSections(viewModel);
|
|
1506
1692
|
renderToast();
|
|
@@ -1509,7 +1695,29 @@ function renderRuntimeOnly({ chrome = true } = {}) {
|
|
|
1509
1695
|
function renderActiveRuntimeSections(viewModel) {
|
|
1510
1696
|
const logCenter = document.querySelector('#logCenterTable');
|
|
1511
1697
|
if (logCenter) {
|
|
1698
|
+
const previousScrollTop = logCenter.scrollTop;
|
|
1699
|
+
const streamTop = logCenter.getBoundingClientRect().top;
|
|
1700
|
+
const anchor = [...logCenter.querySelectorAll('.log-line')]
|
|
1701
|
+
.find((line) => line.getBoundingClientRect().bottom >= streamTop);
|
|
1702
|
+
const anchorId = anchor?.dataset.logId;
|
|
1703
|
+
const anchorViewportTop = anchor?.getBoundingClientRect().top;
|
|
1512
1704
|
logCenter.innerHTML = buildLogCenterMarkup(viewModel);
|
|
1705
|
+
const workspace = logCenter.closest('#logsWorkspace');
|
|
1706
|
+
workspace?.querySelectorAll('[data-log-jump-latest], [data-log-undo-clear]').forEach((node) => node.remove());
|
|
1707
|
+
if (!state.logView.follow && state.logView.unseenCount > 0) {
|
|
1708
|
+
logCenter.insertAdjacentHTML('beforebegin', `<button class="btn btn-primary small log-jump-latest" data-log-jump-latest type="button">回到底部 · ${state.logView.unseenCount} 条新消息</button>`);
|
|
1709
|
+
}
|
|
1710
|
+
if (state.logView.clearedSnapshot) {
|
|
1711
|
+
logCenter.insertAdjacentHTML('beforebegin', '<button class="btn btn-secondary small log-undo-clear" data-log-undo-clear type="button">撤销清空</button>');
|
|
1712
|
+
}
|
|
1713
|
+
if (state.logView.follow) {
|
|
1714
|
+
logCenter.scrollTop = logCenter.scrollHeight;
|
|
1715
|
+
} else if (anchorId) {
|
|
1716
|
+
const restoredAnchor = logCenter.querySelector(`[data-log-id="${CSS.escape(anchorId)}"]`);
|
|
1717
|
+
logCenter.scrollTop += (restoredAnchor?.getBoundingClientRect().top ?? anchorViewportTop) - anchorViewportTop;
|
|
1718
|
+
} else {
|
|
1719
|
+
logCenter.scrollTop = previousScrollTop;
|
|
1720
|
+
}
|
|
1513
1721
|
}
|
|
1514
1722
|
|
|
1515
1723
|
const stageProgress = document.querySelector('#runsStageProgress');
|
|
@@ -1528,6 +1736,21 @@ function renderActiveRuntimeSections(viewModel) {
|
|
|
1528
1736
|
}
|
|
1529
1737
|
}
|
|
1530
1738
|
|
|
1739
|
+
function handleLogScroll(event) {
|
|
1740
|
+
if (event.target?.id !== 'logCenterTable') return;
|
|
1741
|
+
const distance = event.target.scrollHeight - event.target.scrollTop - event.target.clientHeight;
|
|
1742
|
+
state.logView.follow = distance <= 32;
|
|
1743
|
+
if (state.logView.follow) {
|
|
1744
|
+
state.logView.unseenCount = 0;
|
|
1745
|
+
event.target.closest('#logsWorkspace')?.querySelectorAll('[data-log-jump-latest]').forEach((node) => node.remove());
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
function scrollLogToLatest() {
|
|
1750
|
+
const logCenter = document.querySelector('#logCenterTable');
|
|
1751
|
+
if (logCenter) logCenter.scrollTop = logCenter.scrollHeight;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1531
1754
|
function resolveVisibleLogEntries() {
|
|
1532
1755
|
return filterLogEntries((state.logEntries ?? []).map((entry) => classifyLogEntry(entry)), state.logFilter);
|
|
1533
1756
|
}
|