@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
|
@@ -116,8 +116,13 @@ export function buildViewModel(state, messages, language) {
|
|
|
116
116
|
const subscriptionUrl = profile.deploy.subscription_url || FALLBACK_PROFILE.deploy.subscription_url;
|
|
117
117
|
const subscriptionCards = buildSubscriptionCards(subscriptionUrl);
|
|
118
118
|
const currentSubscription = subscriptionCards.find((card) => card.title === state.subscriptionFormat) ?? subscriptionCards[0];
|
|
119
|
-
const displayLogs = (state.logEntries ?? []).map((entry) =>
|
|
119
|
+
const displayLogs = (state.logEntries ?? []).map((entry, index) => ({
|
|
120
|
+
logId: entry?.logId ?? `history-${index}`,
|
|
121
|
+
...classifyLogEntry(entry)
|
|
122
|
+
}));
|
|
120
123
|
const logFilter = state.logFilter ?? '全部';
|
|
124
|
+
const filteredLogs = filterLogEntries(displayLogs, logFilter);
|
|
125
|
+
const logWindow = selectLogWindow(filteredLogs, state.logView);
|
|
121
126
|
const stageRows = normalizeStageRows(state.stageStatus, state.runState);
|
|
122
127
|
const currentStage = stageRows.find((row) => row.status === 'running') ?? stageRows.find((row) => row.status === 'success') ?? stageRows[0];
|
|
123
128
|
const artifactDir = state.artifactDir ?? '';
|
|
@@ -140,11 +145,12 @@ export function buildViewModel(state, messages, language) {
|
|
|
140
145
|
counts,
|
|
141
146
|
subscriptionUrl,
|
|
142
147
|
subscriptionFormat: state.subscriptionFormat ?? 'Clash',
|
|
143
|
-
qrDataUrl: state.qrDataUrl ?? '',
|
|
148
|
+
qr: state.qr ?? { status: state.qrDataUrl ? 'success' : 'idle', dataUrl: state.qrDataUrl ?? '', message: '' },
|
|
144
149
|
displayLogs,
|
|
145
150
|
logFilter,
|
|
146
|
-
|
|
147
|
-
|
|
151
|
+
logView: state.logView ?? { follow: true, unseenCount: 0, clearedSnapshot: null },
|
|
152
|
+
logRows: buildLogRows(logWindow, false),
|
|
153
|
+
logGroups: groupLogEntriesByStage(logWindow, false),
|
|
148
154
|
stageRows,
|
|
149
155
|
currentStage,
|
|
150
156
|
artifactDir,
|
|
@@ -194,6 +200,7 @@ export function buildSidebarNav(messages, activePage) {
|
|
|
194
200
|
class="nav-item ${activePage === page ? 'active' : ''}"
|
|
195
201
|
data-page-target="${page}"
|
|
196
202
|
type="button"
|
|
203
|
+
${activePage === page ? 'aria-current="page"' : ''}
|
|
197
204
|
>
|
|
198
205
|
<span class="nav-icon">${NAV_ICONS[page]}</span>
|
|
199
206
|
<span class="nav-copy">${escapeHtml(messages.nav[page])}</span>
|
|
@@ -252,7 +259,7 @@ export function buildSidebarStatus(viewModel, messages, state, language) {
|
|
|
252
259
|
|
|
253
260
|
export function buildPageMarkup(activePage, viewModel, messages, language, subtabs = {}) {
|
|
254
261
|
return `
|
|
255
|
-
<section class="page-shell" data-page-shell="${activePage}">
|
|
262
|
+
<section ${activePage === 'runs' ? 'id="runsWorkspace"' : ''} class="page-shell" data-page-shell="${activePage}">
|
|
256
263
|
${buildPageInner(activePage, viewModel, messages, language, subtabs)}
|
|
257
264
|
</section>
|
|
258
265
|
`;
|
|
@@ -358,11 +365,15 @@ function buildRunsPage(vm, messages) {
|
|
|
358
365
|
const stageDisabled = vm.runControlState.isBusy || !vm.retryStageOptions.length ? 'disabled' : '';
|
|
359
366
|
const retryDisabled = vm.runControlState.isBusy || !vm.selectedRetryArtifactDir || !vm.selectedRetryStage ? 'disabled' : '';
|
|
360
367
|
return `
|
|
361
|
-
<div
|
|
362
|
-
<article class="panel wide-panel run-control-panel">
|
|
368
|
+
<div class="mobile-run-bar" data-mobile-run-bar>
|
|
363
369
|
<button class="btn btn-primary run-big" data-run-action="start" type="button" ${runDisabled}>▶ 开始运行</button>
|
|
364
370
|
<button class="btn btn-secondary run-big" data-run-action="stop" type="button" ${vm.runControlState.stopDisabled ? 'disabled' : ''}>■ 停止运行</button>
|
|
365
|
-
|
|
371
|
+
</div>
|
|
372
|
+
<div class="page-grid runs-grid">
|
|
373
|
+
<article class="panel wide-panel run-control-panel">
|
|
374
|
+
<details class="run-secondary-controls">
|
|
375
|
+
<summary>重试历史、阶段与运行选项</summary>
|
|
376
|
+
<div class="retry-control-card">
|
|
366
377
|
<div class="field compact retry-field retry-artifact-field">
|
|
367
378
|
<span>历史 run</span>
|
|
368
379
|
<select data-run-retry-artifact ${artifactDisabled}>
|
|
@@ -405,8 +416,8 @@ function buildRunsPage(vm, messages) {
|
|
|
405
416
|
: '<div class="empty-state">暂无可重试 run</div>'}
|
|
406
417
|
</div>
|
|
407
418
|
<p class="retry-help">阶段重试会新建 artifact,并从所选阶段继续执行到 verify。</p>
|
|
408
|
-
|
|
409
|
-
|
|
419
|
+
</div>
|
|
420
|
+
<div class="run-options">
|
|
410
421
|
${[
|
|
411
422
|
['跳过部署', 'skipDeploy', false],
|
|
412
423
|
['跳过验证', 'skipVerify', false],
|
|
@@ -414,7 +425,8 @@ function buildRunsPage(vm, messages) {
|
|
|
414
425
|
].map(([label, key, checked]) => `
|
|
415
426
|
<label class="checkbox-chip"><input data-run-option="${escapeHtml(key)}" type="checkbox" ${checked ? 'checked' : ''} />${escapeHtml(label)}</label>
|
|
416
427
|
`).join('')}
|
|
417
|
-
|
|
428
|
+
</div>
|
|
429
|
+
</details>
|
|
418
430
|
</article>
|
|
419
431
|
|
|
420
432
|
${buildRunsStageProgressMarkup(vm)}
|
|
@@ -518,11 +530,11 @@ function buildResultsPage(vm, messages) {
|
|
|
518
530
|
<tbody>
|
|
519
531
|
${vm.nodeRows.length ? vm.nodeRows.map((row, index) => `
|
|
520
532
|
<tr>
|
|
521
|
-
<td>${index + 1}</td>
|
|
522
|
-
<td>${escapeHtml(row.name || '—')}</td>
|
|
523
|
-
<td>${escapeHtml(row.address || '—')}</td>
|
|
524
|
-
<td>${escapeHtml(row.protocol || '—')}</td>
|
|
525
|
-
<td class="mono">${escapeHtml(row.path || '—')}</td>
|
|
533
|
+
<td><span class="node-card-field">序号</span><span>${index + 1}</span></td>
|
|
534
|
+
<td><span class="node-card-field">节点名称</span><span>${escapeHtml(row.name || '—')}</span></td>
|
|
535
|
+
<td><span class="node-card-field">IP地址</span><span>${escapeHtml(row.address || '—')}</span></td>
|
|
536
|
+
<td><span class="node-card-field">协议</span><span>${escapeHtml(row.protocol || '—')}</span></td>
|
|
537
|
+
<td class="mono"><span class="node-card-field">path</span><span>${escapeHtml(row.path || '—')}</span></td>
|
|
526
538
|
</tr>
|
|
527
539
|
`).join('') : '<tr><td colspan="5">暂无节点,运行完成后显示。</td></tr>'}
|
|
528
540
|
</tbody>
|
|
@@ -545,6 +557,7 @@ function buildSubscriptionsPage(vm, messages) {
|
|
|
545
557
|
<button
|
|
546
558
|
class="subtab ${vm.subscriptionFormat === format ? 'active' : ''}"
|
|
547
559
|
data-subscription-format="${escapeHtml(format)}"
|
|
560
|
+
aria-pressed="${vm.subscriptionFormat === format}"
|
|
548
561
|
type="button"
|
|
549
562
|
>${escapeHtml(format)}</button>
|
|
550
563
|
`).join('')}
|
|
@@ -559,7 +572,7 @@ function buildSubscriptionsPage(vm, messages) {
|
|
|
559
572
|
|
|
560
573
|
<article class="panel slim-panel">
|
|
561
574
|
<div class="panel-headline"><h3>订阅二维码</h3></div>
|
|
562
|
-
<div class="qr-block">${renderQr(vm.
|
|
575
|
+
<div class="qr-block">${renderQr(vm.qr)}</div>
|
|
563
576
|
<p class="panel-subcopy center-copy">扫码导入订阅</p>
|
|
564
577
|
</article>
|
|
565
578
|
|
|
@@ -581,17 +594,24 @@ function buildLogsPage(vm, messages) {
|
|
|
581
594
|
<button
|
|
582
595
|
class="subtab ${vm.logFilter === filter ? 'active' : ''}"
|
|
583
596
|
data-log-filter="${escapeHtml(filter)}"
|
|
597
|
+
aria-pressed="${vm.logFilter === filter ? 'true' : 'false'}"
|
|
584
598
|
type="button"
|
|
585
599
|
>${escapeHtml(filter)}</button>
|
|
586
600
|
`).join('')}
|
|
587
601
|
</div>
|
|
588
602
|
<div class="toolbar-right">
|
|
589
|
-
<
|
|
590
|
-
|
|
591
|
-
|
|
603
|
+
<div class="log-utility-actions">
|
|
604
|
+
<button class="btn btn-secondary small" data-action="copy-log" type="button">复制日志</button>
|
|
605
|
+
${vm.runtime === 'web' ? '' : '<button class="btn btn-primary small" data-action="open-log-file" type="button">打开日志文件</button>'}
|
|
606
|
+
</div>
|
|
607
|
+
<div class="log-destructive-actions">
|
|
608
|
+
<button class="btn btn-secondary small" data-action="clear-log" type="button">清空显示</button>
|
|
609
|
+
</div>
|
|
592
610
|
</div>
|
|
593
611
|
</div>
|
|
594
|
-
|
|
612
|
+
${!vm.logView.follow && vm.logView.unseenCount > 0 ? `<button class="btn btn-primary small log-jump-latest" data-log-jump-latest type="button">回到底部 · ${vm.logView.unseenCount} 条新消息</button>` : ''}
|
|
613
|
+
${vm.logView.clearedSnapshot ? '<button class="btn btn-secondary small log-undo-clear" data-log-undo-clear type="button">撤销清空</button>' : ''}
|
|
614
|
+
<div id="logCenterTable" class="terminal-output log-stream" role="log" aria-live="polite">
|
|
595
615
|
${buildLogCenterMarkup(vm)}
|
|
596
616
|
</div>
|
|
597
617
|
</article>
|
|
@@ -672,15 +692,20 @@ function buildSettingsDrawer(vm) {
|
|
|
672
692
|
|
|
673
693
|
const style = vm.modalTransform ? `style="transform: ${escapeHtml(vm.modalTransform)};"` : '';
|
|
674
694
|
|
|
695
|
+
if (!isOpen) {
|
|
696
|
+
return '<div id="settingsDrawer" class="settings-drawer-shell" data-open="false" data-section="" hidden inert></div>';
|
|
697
|
+
}
|
|
698
|
+
|
|
675
699
|
return `
|
|
676
700
|
<div id="settingsDrawer" class="settings-drawer-shell" data-open="${isOpen ? 'true' : 'false'}" data-section="${escapeHtml(section)}">
|
|
677
701
|
<button class="settings-drawer-backdrop" data-drawer-dismiss="backdrop" type="button" aria-label="关闭设置弹窗"></button>
|
|
678
|
-
<aside class="settings-drawer-panel" ${style}>
|
|
702
|
+
<aside data-settings-dialog role="dialog" aria-modal="true" aria-labelledby="settingsDrawerTitle" class="settings-drawer-panel" ${style}>
|
|
679
703
|
<div class="settings-drawer-head">
|
|
680
704
|
<div>
|
|
681
705
|
<span class="settings-card-kicker">编辑配置</span>
|
|
682
706
|
<h3 id="settingsDrawerTitle">${escapeHtml(title)}</h3>
|
|
683
707
|
</div>
|
|
708
|
+
<button class="icon-btn settings-drawer-close" data-drawer-close="cancel" type="button" aria-label="关闭${escapeHtml(title)}">×</button>
|
|
684
709
|
</div>
|
|
685
710
|
<div class="settings-drawer-body">
|
|
686
711
|
${isOpen ? buildSettingsDrawerBody(section, drawer.draft) : ''}
|
|
@@ -725,18 +750,19 @@ function buildSettingsDrawerBody(section, draft) {
|
|
|
725
750
|
<thead><tr><th>启用</th><th>名称</th><th>地址</th><th>密钥</th></tr></thead>
|
|
726
751
|
<tbody>
|
|
727
752
|
${Object.entries(sourceDraft).map(([name, source]) => `
|
|
728
|
-
<tr>
|
|
729
|
-
<td><input type="checkbox" data-drawer-source="${escapeHtml(name)}" data-drawer-key="enabled" ${source.enabled ? 'checked' : ''} /></td>
|
|
730
|
-
<td><strong class="settings-source-name">${escapeHtml(SOURCE_NAMES[name] || name)}</strong></td>
|
|
731
|
-
<td>
|
|
753
|
+
<tr data-settings-field-group>
|
|
754
|
+
<td data-field-label="启用"><input aria-label="${escapeHtml(SOURCE_NAMES[name] || name)}:启用" type="checkbox" data-drawer-source="${escapeHtml(name)}" data-drawer-key="enabled" ${source.enabled ? 'checked' : ''} /></td>
|
|
755
|
+
<td data-field-label="名称"><strong class="settings-source-name">${escapeHtml(SOURCE_NAMES[name] || name)}</strong></td>
|
|
756
|
+
<td data-field-label="地址">
|
|
732
757
|
<textarea
|
|
733
758
|
class="table-textarea mono"
|
|
734
759
|
rows="3"
|
|
760
|
+
aria-label="${escapeHtml(SOURCE_NAMES[name] || name)}:地址"
|
|
735
761
|
data-drawer-source="${escapeHtml(name)}"
|
|
736
762
|
data-drawer-key="url"
|
|
737
763
|
>${escapeHtml(source.url ?? '')}</textarea>
|
|
738
764
|
</td>
|
|
739
|
-
<td><input data-drawer-source="${escapeHtml(name)}" data-drawer-key="key" value="${escapeHtml(source.key ?? '')}" /></td>
|
|
765
|
+
<td data-field-label="密钥"><input aria-label="${escapeHtml(SOURCE_NAMES[name] || name)}:密钥" data-drawer-source="${escapeHtml(name)}" data-drawer-key="key" value="${escapeHtml(source.key ?? '')}" /></td>
|
|
740
766
|
</tr>
|
|
741
767
|
`).join('')}
|
|
742
768
|
</tbody>
|
|
@@ -771,13 +797,13 @@ function buildSettingsDrawerBody(section, draft) {
|
|
|
771
797
|
<thead><tr><th>启用</th><th>名称</th><th>URL</th><th>操作</th></tr></thead>
|
|
772
798
|
<tbody>
|
|
773
799
|
${(draft?.targets ?? []).map((target, index) => `
|
|
774
|
-
<tr>
|
|
775
|
-
<td><input type="checkbox" data-availability-index="${index}" data-availability-key="enabled" ${target.enabled ? 'checked' : ''} /></td>
|
|
776
|
-
<td><input data-availability-index="${index}" data-availability-key="name" value="${escapeHtml(target.name ?? '')}" /></td>
|
|
777
|
-
<td>
|
|
778
|
-
<textarea class="table-textarea mono" rows="3" data-availability-index="${index}" data-availability-key="url">${escapeHtml(target.url ?? '')}</textarea>
|
|
800
|
+
<tr data-settings-field-group>
|
|
801
|
+
<td data-field-label="启用"><input aria-label="${escapeHtml(formatAvailabilityName(target.name))}:启用" type="checkbox" data-availability-index="${index}" data-availability-key="enabled" ${target.enabled ? 'checked' : ''} /></td>
|
|
802
|
+
<td data-field-label="名称"><input aria-label="${escapeHtml(formatAvailabilityName(target.name))}:名称" data-availability-index="${index}" data-availability-key="name" value="${escapeHtml(target.name ?? '')}" /></td>
|
|
803
|
+
<td data-field-label="URL">
|
|
804
|
+
<textarea aria-label="${escapeHtml(formatAvailabilityName(target.name))}:URL" class="table-textarea mono" rows="3" data-availability-index="${index}" data-availability-key="url">${escapeHtml(target.url ?? '')}</textarea>
|
|
779
805
|
</td>
|
|
780
|
-
<td><button class="btn btn-secondary small" data-availability-action="remove" data-availability-index="${index}" type="button">删除</button></td>
|
|
806
|
+
<td data-field-label="操作"><button class="btn btn-secondary small destructive" data-availability-action="remove" data-availability-index="${index}" type="button">删除</button></td>
|
|
781
807
|
</tr>
|
|
782
808
|
`).join('')}
|
|
783
809
|
</tbody>
|
|
@@ -812,6 +838,11 @@ function buildSettingsDrawerBody(section, draft) {
|
|
|
812
838
|
`;
|
|
813
839
|
}
|
|
814
840
|
|
|
841
|
+
function formatAvailabilityName(name) {
|
|
842
|
+
const labels = { gemini: 'Gemini', chatgpt: 'ChatGPT', claude: 'Claude' };
|
|
843
|
+
return labels[String(name ?? '').toLowerCase()] ?? String(name || '网站');
|
|
844
|
+
}
|
|
845
|
+
|
|
815
846
|
function renderDrawerField(label, type, value, path, isCompact = false, extraAttrs = '') {
|
|
816
847
|
return `
|
|
817
848
|
<label class="field ${isCompact ? 'compact' : ''}">
|
|
@@ -825,11 +856,17 @@ function renderBadge(text, tone) {
|
|
|
825
856
|
return `<span class="badge ${tone}">${escapeHtml(text)}</span>`;
|
|
826
857
|
}
|
|
827
858
|
|
|
828
|
-
function renderQr(
|
|
829
|
-
if (
|
|
830
|
-
return
|
|
859
|
+
function renderQr(qr) {
|
|
860
|
+
if (qr.status === 'success' && qr.dataUrl) {
|
|
861
|
+
return `<img class="qr-image" alt="订阅二维码" src="${escapeHtml(qr.dataUrl)}" />`;
|
|
862
|
+
}
|
|
863
|
+
if (qr.status === 'loading' || qr.status === 'idle') {
|
|
864
|
+
return '<div class="qr-loading" role="status">二维码生成中</div>';
|
|
831
865
|
}
|
|
832
|
-
|
|
866
|
+
if (qr.status === 'unavailable') {
|
|
867
|
+
return '<div class="qr-status"><strong>当前环境不支持二维码生成</strong><span>请使用上方“复制链接”导入订阅。</span></div>';
|
|
868
|
+
}
|
|
869
|
+
return `<div class="qr-status" role="alert"><strong>${escapeHtml(qr.message || '二维码生成失败')}</strong><button class="btn btn-secondary small" data-action="retry-qr" type="button">重试生成</button></div>`;
|
|
833
870
|
}
|
|
834
871
|
|
|
835
872
|
function buildShortcutDescriptors(messages) {
|
|
@@ -899,11 +936,6 @@ function buildSourceMetricRows(profile, sourceCounts = {}, countKey = 'raw_links
|
|
|
899
936
|
...Object.keys(profile.sources ?? {}),
|
|
900
937
|
...Object.keys(sourceCounts ?? {})
|
|
901
938
|
]);
|
|
902
|
-
const hasRequestedValues = countKey === 'raw_links'
|
|
903
|
-
|| Object.values(sourceCounts ?? {}).some((counts) => counts && countKey in counts);
|
|
904
|
-
if (!hasRequestedValues) {
|
|
905
|
-
return [];
|
|
906
|
-
}
|
|
907
939
|
return Array.from(sourceNames)
|
|
908
940
|
.sort((left, right) => {
|
|
909
941
|
const leftIndex = SOURCE_DISPLAY_ORDER.indexOf(left);
|
|
@@ -913,7 +945,9 @@ function buildSourceMetricRows(profile, sourceCounts = {}, countKey = 'raw_links
|
|
|
913
945
|
.map((name) => ({
|
|
914
946
|
name,
|
|
915
947
|
label: SOURCE_NAMES[name] || name,
|
|
916
|
-
count:
|
|
948
|
+
count: countKey !== 'raw_links' && !Object.hasOwn(sourceCounts[name] ?? {}, countKey)
|
|
949
|
+
? '—'
|
|
950
|
+
: Number(sourceCounts[name]?.[countKey] ?? 0)
|
|
917
951
|
}));
|
|
918
952
|
}
|
|
919
953
|
|
|
@@ -938,7 +972,9 @@ export function extractSourceUrlFromCurl(value = '') {
|
|
|
938
972
|
export function buildRegionStats(nodeRows = []) {
|
|
939
973
|
const counts = new Map();
|
|
940
974
|
for (const row of nodeRows) {
|
|
941
|
-
const
|
|
975
|
+
const previewRegion = String(row?.regionCode ?? '').trim().toUpperCase();
|
|
976
|
+
const inferredRegion = previewRegion || inferNodeRegion(row?.name);
|
|
977
|
+
const region = inferredRegion === 'ZZ' || inferredRegion === 'OTHER' || inferredRegion === '其他' ? 'US' : inferredRegion;
|
|
942
978
|
counts.set(region, (counts.get(region) ?? 0) + 1);
|
|
943
979
|
}
|
|
944
980
|
return Array.from(counts.entries()).map(([region, count]) => ({ region, count }));
|
|
@@ -1029,7 +1065,7 @@ export function applySourceIterationDraft(sources = {}, draft = {}) {
|
|
|
1029
1065
|
function inferNodeRegion(name = '') {
|
|
1030
1066
|
const normalized = String(name).replace(/^[^\p{L}\p{N}]+/u, '').trim();
|
|
1031
1067
|
const match = normalized.match(/^([A-Z]{2})(?:\b|[\s_-])/);
|
|
1032
|
-
return match ? match[1] : '
|
|
1068
|
+
return match ? match[1] : 'US';
|
|
1033
1069
|
}
|
|
1034
1070
|
|
|
1035
1071
|
function coercePositiveInteger(value, fallback) {
|
|
@@ -1048,18 +1084,27 @@ function coerceAreaValue(value, fallback) {
|
|
|
1048
1084
|
return Math.trunc(parsed);
|
|
1049
1085
|
}
|
|
1050
1086
|
|
|
1051
|
-
function buildLogRows(displayLogs) {
|
|
1052
|
-
return displayLogs.slice(-28).map((entry) => ({
|
|
1087
|
+
function buildLogRows(displayLogs, trimWindow = true) {
|
|
1088
|
+
return (trimWindow ? displayLogs.slice(-28) : displayLogs).map((entry) => ({
|
|
1053
1089
|
...entry,
|
|
1054
1090
|
levelClass: entry.level === 'error' ? 'danger' : entry.level === 'warning' ? 'warning' : 'success'
|
|
1055
1091
|
}));
|
|
1056
1092
|
}
|
|
1057
1093
|
|
|
1094
|
+
function selectLogWindow(entries, logView) {
|
|
1095
|
+
if (logView?.follow !== false) return entries.slice(-28);
|
|
1096
|
+
const anchorIndex = entries.findIndex((entry) => entry.logId === logView.anchorId);
|
|
1097
|
+
const start = anchorIndex >= 0
|
|
1098
|
+
? Math.max(0, Math.min(anchorIndex - 8, Math.max(0, entries.length - 120)))
|
|
1099
|
+
: Math.max(0, entries.length - 120);
|
|
1100
|
+
return entries.slice(start, start + 120);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1058
1103
|
function renderLogRows(rows) {
|
|
1059
1104
|
if (!rows.length) {
|
|
1060
1105
|
return '<div class="empty-state log-empty-state">暂无可显示日志</div>';
|
|
1061
1106
|
}
|
|
1062
|
-
return rows.map((row) => `<div class="log-line ${row.levelClass}">${escapeHtml(row.line)}</div>`).join('');
|
|
1107
|
+
return rows.map((row) => `<div class="log-line ${row.levelClass}" data-log-id="${escapeHtml(row.logId)}">${escapeHtml(row.line)}</div>`).join('');
|
|
1063
1108
|
}
|
|
1064
1109
|
|
|
1065
1110
|
function renderLogGroups(groups) {
|
|
@@ -1069,7 +1114,7 @@ function renderLogGroups(groups) {
|
|
|
1069
1114
|
return groups.map((group) => `
|
|
1070
1115
|
<section class="log-group">
|
|
1071
1116
|
<div class="log-group-title">${escapeHtml(group.label)}</div>
|
|
1072
|
-
${group.rows.map((row) => `<div class="log-line ${row.levelClass}">${escapeHtml(row.line)}</div>`).join('')}
|
|
1117
|
+
${group.rows.map((row) => `<div class="log-line ${row.levelClass}" data-log-id="${escapeHtml(row.logId)}">${escapeHtml(row.line)}</div>`).join('')}
|
|
1073
1118
|
</section>
|
|
1074
1119
|
`).join('');
|
|
1075
1120
|
}
|
|
@@ -1103,9 +1148,9 @@ export function filterLogEntries(entries, filter = '全部') {
|
|
|
1103
1148
|
return entries;
|
|
1104
1149
|
}
|
|
1105
1150
|
|
|
1106
|
-
export function groupLogEntriesByStage(entries) {
|
|
1151
|
+
export function groupLogEntriesByStage(entries, trimWindow = true) {
|
|
1107
1152
|
const groups = new Map();
|
|
1108
|
-
for (const row of buildLogRows(entries)) {
|
|
1153
|
+
for (const row of buildLogRows(entries, trimWindow)) {
|
|
1109
1154
|
const label = row.stage || '其他';
|
|
1110
1155
|
if (!groups.has(label)) {
|
|
1111
1156
|
groups.set(label, { label, rows: [] });
|