@swimmingliu/autovpn 2.0.3 → 2.1.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/dist/cli/accounts-commands.js +123 -7
- package/dist/cli/output.js +1 -1
- package/dist/config/profile.js +40 -9
- package/dist/pipeline/availability.js +201 -150
- package/dist/pipeline/concurrency.js +68 -0
- package/dist/pipeline/credential-source.js +490 -99
- package/dist/pipeline/egress.js +372 -0
- package/dist/pipeline/extract.js +108 -69
- package/dist/pipeline/http-transport.js +232 -0
- package/dist/pipeline/orchestrator.js +34 -8
- package/dist/pipeline/proxy-runtime.js +35 -2
- package/dist/pipeline/speedtest.js +187 -144
- package/dist/pool/account-store.js +46 -4
- package/dist/server/http.js +11 -0
- package/dist/server/runtime.js +27 -1
- package/dist/server/web-adapter.js +12 -0
- package/dist/web/renderer/app.js +115 -10
- package/dist/web/renderer/i18n.js +1 -1
- package/dist/web/renderer/styles.css +37 -2
- package/dist/web/renderer/tables.js +2 -2
- package/dist/web/renderer/views.js +156 -20
- package/dist/web/renderer/workspace.css +5 -0
- package/package.json +1 -1
package/dist/web/renderer/app.js
CHANGED
|
@@ -9,8 +9,10 @@ import { renderAnalytics, clearAnalytics } from './analytics.bundle.js';
|
|
|
9
9
|
import {
|
|
10
10
|
addAvailabilityTargetDraft,
|
|
11
11
|
applyAvailabilityTargetDraft,
|
|
12
|
+
applyNetworkDraft,
|
|
12
13
|
applySourceIterationDraft,
|
|
13
14
|
buildAvailabilityTargetDraft,
|
|
15
|
+
buildNetworkDraft,
|
|
14
16
|
buildPageMarkup,
|
|
15
17
|
buildDashboardMetricsMarkup,
|
|
16
18
|
buildDashboardStateMarkup,
|
|
@@ -25,6 +27,7 @@ import {
|
|
|
25
27
|
buildAccountPoolRecords,
|
|
26
28
|
removeAvailabilityTargetDraft,
|
|
27
29
|
resolveDashboardRunSummary,
|
|
30
|
+
SOURCE_ROUND_DEFAULTS,
|
|
28
31
|
validateSourceDraft,
|
|
29
32
|
classifyLogEntry,
|
|
30
33
|
escapeHtml,
|
|
@@ -36,10 +39,10 @@ initializeAppearance();
|
|
|
36
39
|
|
|
37
40
|
const demoProfile = {
|
|
38
41
|
sources: {
|
|
39
|
-
leiting: { mode: 'credential', official_site: 'https://www.68123836.com', enabled: true,
|
|
40
|
-
heidong: { mode: 'credential', official_site: 'https://www.67986457.com', enabled: true,
|
|
41
|
-
mifeng: { mode: 'credential', official_site: 'https://www.86933221.com', enabled: true,
|
|
42
|
-
xuanfeng: { mode: 'credential', official_site: 'https://www.16863988.com', enabled: true,
|
|
42
|
+
leiting: { mode: 'credential', official_site: 'https://www.68123836.com', enabled: true, ...SOURCE_ROUND_DEFAULTS },
|
|
43
|
+
heidong: { mode: 'credential', official_site: 'https://www.67986457.com', enabled: true, ...SOURCE_ROUND_DEFAULTS },
|
|
44
|
+
mifeng: { mode: 'credential', official_site: 'https://www.86933221.com', enabled: true, ...SOURCE_ROUND_DEFAULTS },
|
|
45
|
+
xuanfeng: { mode: 'credential', official_site: 'https://www.16863988.com', enabled: true, ...SOURCE_ROUND_DEFAULTS }
|
|
43
46
|
},
|
|
44
47
|
speed_test: {
|
|
45
48
|
min_download_mb_s: 1.0,
|
|
@@ -114,7 +117,9 @@ const state = {
|
|
|
114
117
|
statusFilter: 'all',
|
|
115
118
|
brandFilter: 'all',
|
|
116
119
|
sortBy: 'brand',
|
|
117
|
-
busy: false
|
|
120
|
+
busy: false,
|
|
121
|
+
canSetExpiry: typeof window.vpnAutomation?.setAccountExpiry === 'function',
|
|
122
|
+
expiryEditorId: null
|
|
118
123
|
},
|
|
119
124
|
isDemo: false,
|
|
120
125
|
runtime: document.body.dataset.runtime === 'web' ? 'web' : 'electron',
|
|
@@ -198,7 +203,10 @@ function hydrateInitialRuntimeState(loadedState = {}) {
|
|
|
198
203
|
const loadedProfile = loadedState.profile ?? {};
|
|
199
204
|
state.activity = Array.isArray(loadedState.activity) ? loadedState.activity : [];
|
|
200
205
|
state.activityUnavailable = Number(loadedState.activityUnavailable ?? 0);
|
|
201
|
-
|
|
206
|
+
// An older profile has no [network] section. Seeding the displayed defaults
|
|
207
|
+
// here means saving any other drawer writes the section the pipeline reads
|
|
208
|
+
// instead of leaving it absent, and the network drawer opens on real values.
|
|
209
|
+
state.profile = { ...loadedProfile, network: buildNetworkDraft(loadedProfile.network) };
|
|
202
210
|
state.savedProfile = structuredClone(loadedProfile);
|
|
203
211
|
const nextRunState = String(loadedState.runState ?? '');
|
|
204
212
|
if (['idle', 'running', 'stopping', 'failed', 'success'].includes(nextRunState)) {
|
|
@@ -642,6 +650,10 @@ function handleDocumentInput(event) {
|
|
|
642
650
|
const floor = SOURCE_MINIMUM_ONE_KEYS.has(key) ? 1 : 0;
|
|
643
651
|
sourceDraft[sourceName][key] = Math.max(floor, Math.trunc(parsed));
|
|
644
652
|
}
|
|
653
|
+
// The area controls have a hard ceiling the backend also clamps to.
|
|
654
|
+
if (SOURCE_MAXIMUM_KEYS.has(key)) {
|
|
655
|
+
sourceDraft[sourceName][key] = Math.min(SOURCE_MAXIMUM_KEYS.get(key), sourceDraft[sourceName][key]);
|
|
656
|
+
}
|
|
645
657
|
}
|
|
646
658
|
return;
|
|
647
659
|
}
|
|
@@ -793,6 +805,8 @@ const SOURCE_NUMERIC_KEYS = new Set([
|
|
|
793
805
|
'max_iterations',
|
|
794
806
|
'min_iterations',
|
|
795
807
|
'plateau_limit',
|
|
808
|
+
'areas_per_round',
|
|
809
|
+
'area_concurrency',
|
|
796
810
|
'max_runtime_seconds',
|
|
797
811
|
'max_areas',
|
|
798
812
|
'area_min',
|
|
@@ -802,7 +816,15 @@ const SOURCE_NUMERIC_KEYS = new Set([
|
|
|
802
816
|
/** Of those, the ones the runner clamps to at least one. */
|
|
803
817
|
const SOURCE_MINIMUM_ONE_KEYS = new Set([
|
|
804
818
|
'max_iterations',
|
|
805
|
-
'plateau_limit'
|
|
819
|
+
'plateau_limit',
|
|
820
|
+
'areas_per_round',
|
|
821
|
+
'area_concurrency'
|
|
822
|
+
]);
|
|
823
|
+
|
|
824
|
+
/** Numeric source fields with a hard upper bound, matching the backend clamp. */
|
|
825
|
+
const SOURCE_MAXIMUM_KEYS = new Map([
|
|
826
|
+
['areas_per_round', 64],
|
|
827
|
+
['area_concurrency', 16]
|
|
806
828
|
]);
|
|
807
829
|
|
|
808
830
|
function resolveErrorMessage(error) {
|
|
@@ -1037,6 +1059,7 @@ function buildSettingsDraft(section) {
|
|
|
1037
1059
|
if (section === 'speed_test') return buildSpeedTestDraft(state.profile.speed_test);
|
|
1038
1060
|
if (section === 'availability_targets') return buildAvailabilityTargetDraft(state.profile.availability_targets);
|
|
1039
1061
|
if (section === 'deploy') return buildDeployDraft(state.profile.deploy);
|
|
1062
|
+
if (section === 'network') return buildNetworkDraft(state.profile.network);
|
|
1040
1063
|
if (section === 'paths') return structuredClone(state.profile.paths);
|
|
1041
1064
|
if (section === 'about') return { version: getMessages(state.language).sidebarVersion };
|
|
1042
1065
|
return null;
|
|
@@ -1091,6 +1114,9 @@ async function saveSettingsDrawer() {
|
|
|
1091
1114
|
|
|
1092
1115
|
async function hydrateAccountPool() {
|
|
1093
1116
|
state.accountPool.status = 'loading';
|
|
1117
|
+
// Re-detect each hydration: the preload bridge is fixed for the page, but a
|
|
1118
|
+
// test harness or a future H5 adapter may attach it after bootstrap.
|
|
1119
|
+
state.accountPool.canSetExpiry = typeof window.vpnAutomation?.setAccountExpiry === 'function';
|
|
1094
1120
|
touchUpdate();
|
|
1095
1121
|
renderAccountPoolPanelOnly();
|
|
1096
1122
|
if (!window.vpnAutomation?.loadAccounts) {
|
|
@@ -1154,7 +1180,54 @@ async function handleAccountPoolAction(button) {
|
|
|
1154
1180
|
}
|
|
1155
1181
|
if (action === 'toggle') {
|
|
1156
1182
|
await toggleAccountStatus(Number(button.dataset.accountId));
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (action === 'expiry-edit') {
|
|
1186
|
+
state.accountPool.expiryEditorId = Number(button.dataset.accountId);
|
|
1187
|
+
renderAll();
|
|
1188
|
+
queueMicrotask(() => document.querySelector(`[data-account-expiry-input="${CSS.escape(String(button.dataset.accountId))}"]`)?.focus());
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
if (action === 'expiry-cancel') {
|
|
1192
|
+
state.accountPool.expiryEditorId = null;
|
|
1193
|
+
renderAll();
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
if (action === 'expiry-save') {
|
|
1197
|
+
const id = Number(button.dataset.accountId);
|
|
1198
|
+
const input = document.querySelector(`[data-account-expiry-input="${CSS.escape(String(id))}"]`);
|
|
1199
|
+
await setAccountExpiry(id, input?.value ?? '');
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
async function setAccountExpiry(id, expiresAt) {
|
|
1204
|
+
const account = (state.accountPool.accounts ?? []).find((item) => item.id === id);
|
|
1205
|
+
// Feature-detected: the browser build has no preload bridge, so this path is
|
|
1206
|
+
// never reachable there and the row hides the affordance.
|
|
1207
|
+
if (!account || typeof window.vpnAutomation?.setAccountExpiry !== 'function') {
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
const value = String(expiresAt ?? '').trim();
|
|
1211
|
+
if (value && !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
1212
|
+
showToast({ tone: 'danger', message: '请选择有效的到期日期' });
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
state.accountPool.busy = true;
|
|
1216
|
+
renderAccountPoolPanelOnly();
|
|
1217
|
+
try {
|
|
1218
|
+
// Preload bridge signature: setAccountExpiry(id, 'YYYY-MM-DD'); '' clears it.
|
|
1219
|
+
const result = await window.vpnAutomation.setAccountExpiry(id, value);
|
|
1220
|
+
if (!result?.ok) {
|
|
1221
|
+
throw new Error(result?.error || '操作失败');
|
|
1222
|
+
}
|
|
1223
|
+
state.accountPool.expiryEditorId = null;
|
|
1224
|
+
showToast({ tone: 'success', message: value ? `${account.account_masked} 到期日已设为 ${value}` : `${account.account_masked} 到期日已清除` });
|
|
1225
|
+
} catch (error) {
|
|
1226
|
+
showToast({ tone: 'danger', message: resolveErrorMessage(error) });
|
|
1227
|
+
} finally {
|
|
1228
|
+
state.accountPool.busy = false;
|
|
1157
1229
|
}
|
|
1230
|
+
await hydrateAccountPool();
|
|
1158
1231
|
}
|
|
1159
1232
|
|
|
1160
1233
|
async function downloadAccountsTemplate() {
|
|
@@ -1291,6 +1364,9 @@ function resolveSettingsDraftPayload(section, draft) {
|
|
|
1291
1364
|
if (section === 'deploy') {
|
|
1292
1365
|
return sanitizeDeployDraft(draft);
|
|
1293
1366
|
}
|
|
1367
|
+
if (section === 'network') {
|
|
1368
|
+
return applyNetworkDraft(draft);
|
|
1369
|
+
}
|
|
1294
1370
|
return structuredClone(draft);
|
|
1295
1371
|
}
|
|
1296
1372
|
|
|
@@ -1693,6 +1769,24 @@ function handlePipelineEventDetails(event, options = {}) {
|
|
|
1693
1769
|
return;
|
|
1694
1770
|
}
|
|
1695
1771
|
|
|
1772
|
+
if (event.type === 'extract_platform_started') {
|
|
1773
|
+
appendLog(`[extract] ${event.source_name} ${platformTag(event.platform)}开始提取,最多 ${event.requested_iterations ?? 0} 次,最少 ${event.min_iterations ?? 0} 次`, {
|
|
1774
|
+
kind: 'stage',
|
|
1775
|
+
stage: 'extract',
|
|
1776
|
+
level: 'info'
|
|
1777
|
+
});
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
if (event.type === 'extract_platform_completed') {
|
|
1782
|
+
appendLog(`[extract] ${event.source_name} ${platformTag(event.platform)}完成,成功 ${event.successful_iterations ?? 0} 次,失败 ${event.failed_iterations ?? 0} 次,原始节点 ${event.raw_links ?? 0} 个`, {
|
|
1783
|
+
kind: 'stage',
|
|
1784
|
+
stage: 'extract',
|
|
1785
|
+
level: 'info'
|
|
1786
|
+
});
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1696
1790
|
if (event.type === 'extract_source_started') {
|
|
1697
1791
|
appendLog(`[extract] ${event.source_name} 开始提取,最多 ${event.requested_iterations ?? 0} 次,最少 ${event.min_iterations ?? 0} 次`, {
|
|
1698
1792
|
kind: 'stage',
|
|
@@ -1705,7 +1799,7 @@ function handlePipelineEventDetails(event, options = {}) {
|
|
|
1705
1799
|
if (event.type === 'extract_request_result') {
|
|
1706
1800
|
const status = event.success ? '成功' : '失败';
|
|
1707
1801
|
const retry = event.will_retry ? ',将重试' : '';
|
|
1708
|
-
appendLog(`[extract] ${event.source_name} #${event.iteration} ${event.via} ${status}${retry}`, {
|
|
1802
|
+
appendLog(`[extract] ${event.source_name} ${platformTag(event.platform)}#${event.iteration} ${event.via} ${status}${retry}`, {
|
|
1709
1803
|
kind: 'stage',
|
|
1710
1804
|
stage: 'extract',
|
|
1711
1805
|
level: event.success ? 'info' : 'warning'
|
|
@@ -1714,7 +1808,7 @@ function handlePipelineEventDetails(event, options = {}) {
|
|
|
1714
1808
|
}
|
|
1715
1809
|
|
|
1716
1810
|
if (event.type === 'extract_decrypt_result' && !event.success) {
|
|
1717
|
-
appendLog(`[extract] ${event.source_name} #${event.iteration} 解密失败`, {
|
|
1811
|
+
appendLog(`[extract] ${event.source_name} ${platformTag(event.platform)}#${event.iteration} 解密失败`, {
|
|
1718
1812
|
kind: 'stage',
|
|
1719
1813
|
stage: 'extract',
|
|
1720
1814
|
level: 'error'
|
|
@@ -1734,7 +1828,10 @@ function handlePipelineEventDetails(event, options = {}) {
|
|
|
1734
1828
|
|
|
1735
1829
|
if (event.type === 'extract_iteration') {
|
|
1736
1830
|
if (!event.progress_snapshot && !(historical && state.progressSnapshot)) updateExtractMetrics(event);
|
|
1737
|
-
|
|
1831
|
+
// Android and iOS run the same round numbers independently, so a bare `#N`
|
|
1832
|
+
// shows two identical lines. The tag only appears when the event carries a
|
|
1833
|
+
// platform, which keeps older backends and the URL-mode stream unchanged.
|
|
1834
|
+
appendLog(`[extract] ${event.source_name} ${platformTag(event.platform)}#${event.iteration ?? 0} 新增 ${event.new_items ?? 0} 个,本次解析 ${event.extracted_links ?? 0} 个,累计 ${event.total_links ?? 0} 个`, {
|
|
1738
1835
|
kind: 'stage',
|
|
1739
1836
|
stage: 'extract',
|
|
1740
1837
|
level: 'info'
|
|
@@ -1976,6 +2073,14 @@ function resolveDefaultRetryStage(artifact) {
|
|
|
1976
2073
|
return failedStage || retryableStages.at(-1) || '';
|
|
1977
2074
|
}
|
|
1978
2075
|
|
|
2076
|
+
// Both channels emit the same round numbers, so the log needs the channel on
|
|
2077
|
+
// the line. `[android] ` keeps the rest of the format untouched; an event with
|
|
2078
|
+
// no platform (older backend, URL-mode extract) renders exactly as before.
|
|
2079
|
+
function platformTag(platform) {
|
|
2080
|
+
const value = String(platform ?? '').trim().toLowerCase();
|
|
2081
|
+
return value === 'android' || value === 'ios' ? `[${value}] ` : '';
|
|
2082
|
+
}
|
|
2083
|
+
|
|
1979
2084
|
function appendLog(message, overrides = {}) {
|
|
1980
2085
|
const logCenter = document.querySelector('#logCenterTable');
|
|
1981
2086
|
if (logCenter) {
|
|
@@ -1390,6 +1390,8 @@ body[data-page="logs"] #logCenterTable {
|
|
|
1390
1390
|
|
|
1391
1391
|
.source-credential-rounds {
|
|
1392
1392
|
display: grid;
|
|
1393
|
+
/* Four controls in a 232px column: two per line, so the labels stay readable
|
|
1394
|
+
next to a 36px number input. */
|
|
1393
1395
|
grid-template-columns: repeat(2, minmax(72px, 1fr));
|
|
1394
1396
|
gap: 8px;
|
|
1395
1397
|
}
|
|
@@ -1478,7 +1480,7 @@ body[data-page="logs"] #logCenterTable {
|
|
|
1478
1480
|
}
|
|
1479
1481
|
|
|
1480
1482
|
.settings-col-rounds {
|
|
1481
|
-
width:
|
|
1483
|
+
width: 232px;
|
|
1482
1484
|
}
|
|
1483
1485
|
|
|
1484
1486
|
.availability-col-url {
|
|
@@ -1704,13 +1706,40 @@ body[data-page="logs"] #logCenterTable {
|
|
|
1704
1706
|
}
|
|
1705
1707
|
|
|
1706
1708
|
.account-col-expiry {
|
|
1707
|
-
width:
|
|
1709
|
+
width: 178px;
|
|
1708
1710
|
}
|
|
1709
1711
|
|
|
1710
1712
|
.account-col-usage {
|
|
1711
1713
|
width: 132px;
|
|
1712
1714
|
}
|
|
1713
1715
|
|
|
1716
|
+
.account-expiry-value {
|
|
1717
|
+
display: block;
|
|
1718
|
+
font-size: 13px;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
.account-expiry-value.unknown {
|
|
1722
|
+
color: var(--text-soft);
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
.account-expiry-edit {
|
|
1726
|
+
margin-top: 4px;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
.account-expiry-editor {
|
|
1730
|
+
display: flex;
|
|
1731
|
+
flex-wrap: wrap;
|
|
1732
|
+
align-items: center;
|
|
1733
|
+
gap: 6px;
|
|
1734
|
+
margin-top: 6px;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
.account-expiry-editor input {
|
|
1738
|
+
min-width: 132px;
|
|
1739
|
+
padding-inline: 8px;
|
|
1740
|
+
font-size: 12px;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1714
1743
|
.account-col-actions {
|
|
1715
1744
|
width: 92px;
|
|
1716
1745
|
}
|
|
@@ -2841,6 +2870,12 @@ body[data-page="logs"] #logCenterTable {
|
|
|
2841
2870
|
min-height: 44px;
|
|
2842
2871
|
}
|
|
2843
2872
|
|
|
2873
|
+
/* The expiry editor sits in that same action cell at phone widths, so its
|
|
2874
|
+
date input has to meet the same tap-target floor. */
|
|
2875
|
+
.account-expiry-editor input {
|
|
2876
|
+
min-height: 44px;
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2844
2879
|
.app-frame {
|
|
2845
2880
|
display: block;
|
|
2846
2881
|
height: auto;
|
|
@@ -8,8 +8,8 @@ const TABLES = {
|
|
|
8
8
|
],
|
|
9
9
|
accounts: [
|
|
10
10
|
['account', '账号', 240, 160], ['brand', '加速器', 110, 90],
|
|
11
|
-
['status', '状态',
|
|
12
|
-
['usage', '使用',
|
|
11
|
+
['status', '状态', 150, 120], ['expiry', '到期', 180, 130],
|
|
12
|
+
['usage', '使用', 150, 120], ['actions', '操作', 96, 80]
|
|
13
13
|
]
|
|
14
14
|
};
|
|
15
15
|
const initialized = new WeakSet();
|
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
import { buildStageModel, PAGE_INDEX, PAGE_ORDER, resolveRunControlState } from './state.js';
|
|
2
2
|
import { icon } from './icons.js';
|
|
3
3
|
|
|
4
|
+
// Defaults for a source row that has never stored a value. A round is one
|
|
5
|
+
// cursor step over that brand's fixed area list (one region by default), so the
|
|
6
|
+
// count is per platform and per region: 4/2 would fetch only a handful of nodes.
|
|
7
|
+
export const SOURCE_ROUND_DEFAULTS = {
|
|
8
|
+
max_iterations: 200,
|
|
9
|
+
plateau_limit: 50,
|
|
10
|
+
areas_per_round: 8,
|
|
11
|
+
area_concurrency: 4
|
|
12
|
+
};
|
|
13
|
+
export const SOURCE_ROUND_LIMITS = {
|
|
14
|
+
max_iterations: [1, Number.POSITIVE_INFINITY],
|
|
15
|
+
plateau_limit: [1, Number.POSITIVE_INFINITY],
|
|
16
|
+
areas_per_round: [1, 64],
|
|
17
|
+
area_concurrency: [1, 16]
|
|
18
|
+
};
|
|
19
|
+
export const NETWORK_DEFAULTS = {
|
|
20
|
+
proxy_mode: 'proxy',
|
|
21
|
+
proxy_url: 'http://127.0.0.1:7897'
|
|
22
|
+
};
|
|
23
|
+
|
|
4
24
|
const FALLBACK_PROFILE = {
|
|
5
25
|
sources: {
|
|
6
|
-
leiting: { mode: 'credential', official_site: 'https://www.68123836.com', enabled: true,
|
|
7
|
-
heidong: { mode: 'credential', official_site: 'https://www.67986457.com', enabled: true,
|
|
8
|
-
mifeng: { mode: 'credential', official_site: 'https://www.86933221.com', enabled: true,
|
|
9
|
-
xuanfeng: { mode: 'credential', official_site: 'https://www.16863988.com', enabled: true,
|
|
26
|
+
leiting: { mode: 'credential', official_site: 'https://www.68123836.com', enabled: true, ...SOURCE_ROUND_DEFAULTS },
|
|
27
|
+
heidong: { mode: 'credential', official_site: 'https://www.67986457.com', enabled: true, ...SOURCE_ROUND_DEFAULTS },
|
|
28
|
+
mifeng: { mode: 'credential', official_site: 'https://www.86933221.com', enabled: true, ...SOURCE_ROUND_DEFAULTS },
|
|
29
|
+
xuanfeng: { mode: 'credential', official_site: 'https://www.16863988.com', enabled: true, ...SOURCE_ROUND_DEFAULTS }
|
|
10
30
|
},
|
|
11
31
|
speed_test: {
|
|
12
32
|
min_download_mb_s: 1,
|
|
@@ -569,6 +589,7 @@ function buildSettingsPage(vm, messages) {
|
|
|
569
589
|
${buildSettingsCard('speed_test', '测速配置', `最低 ${vm.profile.speed_test.min_download_mb_s} MB/s · 并发 ${vm.profile.speed_test.concurrency}`)}
|
|
570
590
|
${buildSettingsCard('availability_targets', 'AI可达性检测', `已启用 ${enabledAvailabilityTargetCount(vm.profile.availability_targets)} / ${Object.keys(vm.profile.availability_targets ?? {}).length} 个网站`)}
|
|
571
591
|
${buildSettingsCard('deploy', '部署配置', `项目 ${vm.profile.deploy.project_name || '未设置'}`)}
|
|
592
|
+
${buildSettingsCard('network', '网络出口', networkSummary(vm.profile.network))}
|
|
572
593
|
</div>
|
|
573
594
|
</article>
|
|
574
595
|
|
|
@@ -633,12 +654,24 @@ export function buildAccountPoolRecords(pool = EMPTY_ACCOUNT_POOL) {
|
|
|
633
654
|
${rows.map((account) => {
|
|
634
655
|
const meta = ACCOUNT_STATUS_META[account.status] ?? { label: account.status, tone: 'neutral' };
|
|
635
656
|
const retired = account.status !== 'active';
|
|
657
|
+
const expiry = String(account.expires_at ?? '').trim();
|
|
658
|
+
const editingExpiry = pool.canSetExpiry && Number(pool.expiryEditorId) === Number(account.id);
|
|
636
659
|
return `
|
|
637
660
|
<tr data-account-row="${account.id}">
|
|
638
661
|
<td data-field-label="账号"><code class="account-mask">${escapeHtml(account.account_masked)}</code>${account.note ? `<span class="account-note">${escapeHtml(account.note)}</span>` : ''}</td>
|
|
639
662
|
<td data-field-label="加速器">${escapeHtml(SOURCE_NAMES[account.brand] ?? account.brand)}</td>
|
|
640
663
|
<td data-field-label="状态">${renderBadge(meta.label, meta.tone)}${account.status_reason ? `<span class="account-note">${escapeHtml(account.status_reason)}</span>` : ''}</td>
|
|
641
|
-
<td data-field-label="到期"
|
|
664
|
+
<td data-field-label="到期">
|
|
665
|
+
<span class="account-expiry-value ${expiry ? '' : 'unknown'}">${escapeHtml(expiry || '未知')}</span>
|
|
666
|
+
${pool.canSetExpiry ? (editingExpiry
|
|
667
|
+
? `<div class="account-expiry-editor">
|
|
668
|
+
<input type="date" aria-label="${escapeHtml(account.account_masked)}:到期日期" data-account-expiry-input="${account.id}" value="${escapeHtml(expiry.slice(0, 10))}" ${pool.busy ? 'disabled' : ''} />
|
|
669
|
+
<button class="btn btn-primary small" data-account-action="expiry-save" data-account-id="${account.id}" type="button" ${pool.busy ? 'disabled' : ''}>保存</button>
|
|
670
|
+
<button class="btn btn-secondary small" data-account-action="expiry-cancel" data-account-id="${account.id}" type="button" ${pool.busy ? 'disabled' : ''}>取消</button>
|
|
671
|
+
</div>`
|
|
672
|
+
: `<button class="btn btn-secondary small account-expiry-edit" data-account-action="expiry-edit" data-account-id="${account.id}" type="button" ${pool.busy ? 'disabled' : ''}>${expiry ? '改期' : '设置到期'}</button>`)
|
|
673
|
+
: ''}
|
|
674
|
+
</td>
|
|
642
675
|
<td data-field-label="使用"><span class="account-usage">${Number(account.use_count ?? 0)} 次</span><span class="account-note">${Number(account.success_count ?? 0)} 成功 / ${Number(account.fail_count ?? 0)} 失败</span></td>
|
|
643
676
|
<td data-field-label="操作"><button class="btn btn-secondary small ${retired ? '' : 'destructive'}" data-account-action="toggle" data-account-id="${account.id}" type="button" ${pool.busy ? 'disabled' : ''}>${retired ? '启用' : '停用'}</button></td>
|
|
644
677
|
</tr>
|
|
@@ -679,6 +712,7 @@ export function buildAccountPoolPanel(vm) {
|
|
|
679
712
|
`).join('')}
|
|
680
713
|
</div>
|
|
681
714
|
${Number(stats.expiring_soon ?? 0) > 0 ? `<p class="account-pool-expiry">7 天内到期 ${Number(stats.expiring_soon)} 个</p>` : ''}
|
|
715
|
+
${Number.isFinite(Number(stats.expiry_unknown)) && Number(stats.expiry_unknown) > 0 ? `<p class="account-pool-expiry unknown">到期时间未知 ${Number(stats.expiry_unknown)}</p>` : ''}
|
|
682
716
|
</div>
|
|
683
717
|
|
|
684
718
|
<div class="account-pool-toolbar">
|
|
@@ -767,7 +801,8 @@ function buildSettingsDrawer(vm) {
|
|
|
767
801
|
sources: '数据源配置',
|
|
768
802
|
speed_test: '测速配置',
|
|
769
803
|
availability_targets: 'AI可达性检测',
|
|
770
|
-
deploy: '部署配置'
|
|
804
|
+
deploy: '部署配置',
|
|
805
|
+
network: '网络出口'
|
|
771
806
|
}[section] ?? '设置';
|
|
772
807
|
|
|
773
808
|
const style = vm.modalTransform ? `style="transform: ${escapeHtml(vm.modalTransform)};"` : '';
|
|
@@ -830,7 +865,11 @@ const EMPTY_ACCOUNT_POOL = {
|
|
|
830
865
|
statusFilter: 'all',
|
|
831
866
|
brandFilter: 'all',
|
|
832
867
|
sortBy: 'brand',
|
|
833
|
-
busy: false
|
|
868
|
+
busy: false,
|
|
869
|
+
// Feature-detected from the preload bridge. The H5/browser build has no
|
|
870
|
+
// setAccountExpiry, so the row simply omits the affordance there.
|
|
871
|
+
canSetExpiry: false,
|
|
872
|
+
expiryEditorId: null
|
|
834
873
|
};
|
|
835
874
|
|
|
836
875
|
/** Credential sources are also configured by their official site. */
|
|
@@ -849,7 +888,13 @@ function buildSettingsDrawerBody(section, draft, vm = null) {
|
|
|
849
888
|
<div class="source-drawer-settings">
|
|
850
889
|
<div class="source-drawer-settings-copy">
|
|
851
890
|
<strong>每个加速器只填官网</strong>
|
|
852
|
-
<span>密钥从 APK 自动发现,账号密码由账号池提供。Android 与 iOS
|
|
891
|
+
<span>密钥从 APK 自动发现,账号密码由账号池提供。Android 与 iOS 两个通道同时抓取;「轮数」由两端各自独立跑满,1000 就是 Android 1000 轮加 iOS 1000 轮,不是 500+500。</span>
|
|
892
|
+
</div>
|
|
893
|
+
<div class="source-drawer-settings-copy">
|
|
894
|
+
<span>每次运行先按该加速器的节点列表生成一份固定区域列表(去掉中国大陆与免费区域、去重、保持原顺序),运行中不再改变。每一轮用游标依次取出「每轮区域数」个区域(默认 8 个,取完回绕);「区域并发」控制该轮这些区域请求同时发出的数量(默认 4)。31 个区域默认四轮即可覆盖一遍,轮数按各平台独立计算。</span>
|
|
895
|
+
</div>
|
|
896
|
+
<div class="source-drawer-settings-copy">
|
|
897
|
+
<span>「早停」按连续无新增节点的轮数计数。是否为新节点按节点指纹判断(address / port / id / transport / host / path / tls / sni),并在 Android 与 iOS 之间统一去重:改名重复的节点、或另一端已经抓到的节点,都不会让计数归零。</span>
|
|
853
898
|
</div>
|
|
854
899
|
</div>
|
|
855
900
|
</details>
|
|
@@ -884,14 +929,22 @@ function buildSettingsDrawerBody(section, draft, vm = null) {
|
|
|
884
929
|
<div class="source-credential-rounds">
|
|
885
930
|
<label class="source-field">
|
|
886
931
|
<span>轮数</span>
|
|
887
|
-
<input type="number" min="1" step="1" inputmode="numeric" aria-label="${label}:抓取轮数" title="
|
|
932
|
+
<input type="number" min="1" step="1" inputmode="numeric" aria-label="${label}:抓取轮数" title="Android 与 iOS 各自独立跑满该轮数(1000 = 两端各 1000 轮);每轮是固定区域列表上的一个游标步,默认只取 1 个区域" data-drawer-source="${escapeHtml(name)}" data-drawer-key="max_iterations" value="${escapeHtml(source.max_iterations ?? SOURCE_ROUND_DEFAULTS.max_iterations)}" />
|
|
888
933
|
</label>
|
|
889
934
|
<label class="source-field">
|
|
890
935
|
<span>早停</span>
|
|
891
|
-
<input type="number" min="1" step="1" inputmode="numeric" aria-label="${label}:连续空轮早停次数" title="
|
|
936
|
+
<input type="number" min="1" step="1" inputmode="numeric" aria-label="${label}:连续空轮早停次数" title="连续多少轮没有真正新增节点就停止;节点指纹在双端之间统一去重" data-drawer-source="${escapeHtml(name)}" data-drawer-key="plateau_limit" value="${escapeHtml(source.plateau_limit ?? SOURCE_ROUND_DEFAULTS.plateau_limit)}" />
|
|
937
|
+
</label>
|
|
938
|
+
<label class="source-field">
|
|
939
|
+
<span>每轮区域数</span>
|
|
940
|
+
<input type="number" min="1" max="64" step="1" inputmode="numeric" aria-label="${label}:每轮区域数" title="每轮从固定区域列表按游标取出的区域数,列表在运行中不变;取完回绕" data-drawer-source="${escapeHtml(name)}" data-drawer-key="areas_per_round" value="${escapeHtml(source.areas_per_round ?? SOURCE_ROUND_DEFAULTS.areas_per_round)}" />
|
|
941
|
+
</label>
|
|
942
|
+
<label class="source-field">
|
|
943
|
+
<span>区域并发</span>
|
|
944
|
+
<input type="number" min="1" max="16" step="1" inputmode="numeric" aria-label="${label}:区域并发" title="该轮区域请求同时发出的数量" data-drawer-source="${escapeHtml(name)}" data-drawer-key="area_concurrency" value="${escapeHtml(source.area_concurrency ?? SOURCE_ROUND_DEFAULTS.area_concurrency)}" />
|
|
892
945
|
</label>
|
|
893
946
|
</div>
|
|
894
|
-
<span class="source-round-summary"
|
|
947
|
+
<span class="source-round-summary">双端各自计轮 · 每轮按固定区域列表推进</span>
|
|
895
948
|
</td>
|
|
896
949
|
</tr>
|
|
897
950
|
`;
|
|
@@ -965,6 +1018,25 @@ function buildSettingsDrawerBody(section, draft, vm = null) {
|
|
|
965
1018
|
`;
|
|
966
1019
|
}
|
|
967
1020
|
|
|
1021
|
+
if (section === 'network') {
|
|
1022
|
+
const network = buildNetworkDraft(draft);
|
|
1023
|
+
return `
|
|
1024
|
+
<details class="context-help">
|
|
1025
|
+
<summary>出口与回退规则</summary>
|
|
1026
|
+
<p>代理:全程经 proxy_url 抓取,代理没在监听时自动改为直连;自动:先直连,遇到 IP 被封(403/429 或封锁页)后整轮改走代理;直连:不使用代理。默认走本机 127.0.0.1:7897。</p>
|
|
1027
|
+
</details>
|
|
1028
|
+
<div class="form-grid compact-form-grid network-form-grid">
|
|
1029
|
+
<label class="field compact">
|
|
1030
|
+
<span>出口模式</span>
|
|
1031
|
+
<select data-drawer-path="network.proxy_mode" aria-label="出口模式">
|
|
1032
|
+
${[['proxy', '代理'], ['auto', '自动(被封后切代理)'], ['direct', '直连']].map(([value, label]) => `<option value="${value}" ${network.proxy_mode === value ? 'selected' : ''}>${label}</option>`).join('')}
|
|
1033
|
+
</select>
|
|
1034
|
+
</label>
|
|
1035
|
+
${renderDrawerField('代理地址', 'text', network.proxy_url, 'network.proxy_url', true, 'inputmode="url" spellcheck="false" placeholder="http://127.0.0.1:7897"')}
|
|
1036
|
+
</div>
|
|
1037
|
+
`;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
968
1040
|
return `
|
|
969
1041
|
<div class="notice-card">
|
|
970
1042
|
<strong>无可用配置</strong>
|
|
@@ -973,6 +1045,29 @@ function buildSettingsDrawerBody(section, draft, vm = null) {
|
|
|
973
1045
|
`;
|
|
974
1046
|
}
|
|
975
1047
|
|
|
1048
|
+
export function buildNetworkDraft(network = {}) {
|
|
1049
|
+
const mode = String(network?.proxy_mode ?? '').trim().toLowerCase();
|
|
1050
|
+
const url = String(network?.proxy_url ?? '').trim();
|
|
1051
|
+
return {
|
|
1052
|
+
proxy_mode: ['proxy', 'direct', 'auto'].includes(mode) ? mode : NETWORK_DEFAULTS.proxy_mode,
|
|
1053
|
+
proxy_url: url || NETWORK_DEFAULTS.proxy_url
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
export function applyNetworkDraft(draft = {}) {
|
|
1058
|
+
const network = buildNetworkDraft(draft);
|
|
1059
|
+
return {
|
|
1060
|
+
proxy_mode: network.proxy_mode,
|
|
1061
|
+
proxy_url: network.proxy_url
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function networkSummary(network = {}) {
|
|
1066
|
+
const resolved = buildNetworkDraft(network);
|
|
1067
|
+
const modeLabel = { proxy: '代理', auto: '自动', direct: '直连' }[resolved.proxy_mode];
|
|
1068
|
+
return resolved.proxy_mode === 'direct' ? modeLabel : `${modeLabel} ${resolved.proxy_url}`;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
976
1071
|
function formatAvailabilityName(name) {
|
|
977
1072
|
const labels = { gemini: 'Gemini', chatgpt: 'ChatGPT', claude: 'Claude' };
|
|
978
1073
|
return labels[String(name ?? '').toLowerCase()] ?? String(name || '网站');
|
|
@@ -1057,8 +1152,10 @@ function buildSourceRows(profile) {
|
|
|
1057
1152
|
enabled: source.enabled !== false,
|
|
1058
1153
|
mode: 'credential',
|
|
1059
1154
|
officialSite: source.official_site ?? '',
|
|
1060
|
-
maxIterations: Number(source.max_iterations ??
|
|
1061
|
-
plateauLimit: Number(source.plateau_limit ??
|
|
1155
|
+
maxIterations: Number(source.max_iterations ?? SOURCE_ROUND_DEFAULTS.max_iterations),
|
|
1156
|
+
plateauLimit: Number(source.plateau_limit ?? SOURCE_ROUND_DEFAULTS.plateau_limit),
|
|
1157
|
+
areasPerRound: Number(source.areas_per_round ?? SOURCE_ROUND_DEFAULTS.areas_per_round),
|
|
1158
|
+
areaConcurrency: Number(source.area_concurrency ?? SOURCE_ROUND_DEFAULTS.area_concurrency)
|
|
1062
1159
|
}));
|
|
1063
1160
|
}
|
|
1064
1161
|
|
|
@@ -1122,8 +1219,10 @@ export function buildSourceIterationDraft(sources = {}) {
|
|
|
1122
1219
|
const firstSource = Object.values(normalized)[0] ?? {};
|
|
1123
1220
|
return {
|
|
1124
1221
|
sources: normalized,
|
|
1125
|
-
maxIterations: coercePositiveInteger(firstSource.max_iterations,
|
|
1126
|
-
plateauLimit: coercePositiveInteger(firstSource.plateau_limit,
|
|
1222
|
+
maxIterations: coercePositiveInteger(firstSource.max_iterations, SOURCE_ROUND_DEFAULTS.max_iterations),
|
|
1223
|
+
plateauLimit: coercePositiveInteger(firstSource.plateau_limit, SOURCE_ROUND_DEFAULTS.plateau_limit),
|
|
1224
|
+
areasPerRound: coerceBoundedInteger(firstSource.areas_per_round, SOURCE_ROUND_DEFAULTS.areas_per_round, 1, 64),
|
|
1225
|
+
areaConcurrency: coerceBoundedInteger(firstSource.area_concurrency, SOURCE_ROUND_DEFAULTS.area_concurrency, 1, 16)
|
|
1127
1226
|
};
|
|
1128
1227
|
}
|
|
1129
1228
|
|
|
@@ -1186,13 +1285,21 @@ export function applySourceIterationDraft(sources = {}, draft = {}) {
|
|
|
1186
1285
|
if (draft?.sources) {
|
|
1187
1286
|
return normalizeOfficialSourceDraft(draft.sources);
|
|
1188
1287
|
}
|
|
1189
|
-
const maxIterations = coercePositiveInteger(draft.maxIterations,
|
|
1190
|
-
const plateauLimit = coercePositiveInteger(draft.plateauLimit,
|
|
1288
|
+
const maxIterations = coercePositiveInteger(draft.maxIterations, SOURCE_ROUND_DEFAULTS.max_iterations);
|
|
1289
|
+
const plateauLimit = coercePositiveInteger(draft.plateauLimit, SOURCE_ROUND_DEFAULTS.plateau_limit);
|
|
1290
|
+
const areasPerRound = coerceBoundedInteger(draft.areasPerRound, SOURCE_ROUND_DEFAULTS.areas_per_round, 1, 64);
|
|
1291
|
+
const areaConcurrency = coerceBoundedInteger(draft.areaConcurrency, SOURCE_ROUND_DEFAULTS.area_concurrency, 1, 16);
|
|
1191
1292
|
const normalized = normalizeOfficialSourceDraft(sources);
|
|
1192
1293
|
return Object.fromEntries(
|
|
1193
1294
|
Object.entries(normalized).map(([name, source]) => [
|
|
1194
1295
|
name,
|
|
1195
|
-
{
|
|
1296
|
+
{
|
|
1297
|
+
...source,
|
|
1298
|
+
max_iterations: maxIterations,
|
|
1299
|
+
plateau_limit: plateauLimit,
|
|
1300
|
+
areas_per_round: areasPerRound,
|
|
1301
|
+
area_concurrency: areaConcurrency
|
|
1302
|
+
}
|
|
1196
1303
|
])
|
|
1197
1304
|
);
|
|
1198
1305
|
}
|
|
@@ -1222,6 +1329,20 @@ export function validateSourceDraft(draft = {}) {
|
|
|
1222
1329
|
if (!Number.isFinite(Number(draft.plateauLimit)) || Number(draft.plateauLimit) < 1) {
|
|
1223
1330
|
return '早停轮数至少为 1';
|
|
1224
1331
|
}
|
|
1332
|
+
for (const [name, source] of Object.entries(sources)) {
|
|
1333
|
+
if (!SOURCE_DISPLAY_ORDER.includes(name)) continue;
|
|
1334
|
+
for (const [key, label, [, max]] of [
|
|
1335
|
+
['areas_per_round', '每轮区域数', SOURCE_ROUND_LIMITS.areas_per_round],
|
|
1336
|
+
['area_concurrency', '区域并发', SOURCE_ROUND_LIMITS.area_concurrency]
|
|
1337
|
+
]) {
|
|
1338
|
+
const raw = source?.[key];
|
|
1339
|
+
if (raw === undefined || raw === null || raw === '') continue;
|
|
1340
|
+
const value = Number(raw);
|
|
1341
|
+
if (!Number.isFinite(value) || value < 1 || value > max) {
|
|
1342
|
+
return `${SOURCE_NAMES[name] ?? name}:${label}必须是 1-${max} 之间的整数`;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1225
1346
|
return '';
|
|
1226
1347
|
}
|
|
1227
1348
|
|
|
@@ -1239,6 +1360,16 @@ function coercePositiveInteger(value, fallback) {
|
|
|
1239
1360
|
return Math.trunc(parsed);
|
|
1240
1361
|
}
|
|
1241
1362
|
|
|
1363
|
+
// The two area controls have a closed range; a stored value from outside it is
|
|
1364
|
+
// clamped rather than rejected, so an older or hand-edited profile still opens.
|
|
1365
|
+
function coerceBoundedInteger(value, fallback, min, max) {
|
|
1366
|
+
const parsed = Number(value);
|
|
1367
|
+
if (!Number.isFinite(parsed)) {
|
|
1368
|
+
return fallback;
|
|
1369
|
+
}
|
|
1370
|
+
return Math.min(max, Math.max(min, Math.trunc(parsed)));
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1242
1373
|
function coerceAreaValue(value, fallback) {
|
|
1243
1374
|
const parsed = Number(value);
|
|
1244
1375
|
if (!Number.isFinite(parsed)) {
|
|
@@ -1374,6 +1505,8 @@ export function normalizeOfficialSourceDraft(sources = {}) {
|
|
|
1374
1505
|
// not overwrite them just by appearing later in the file.
|
|
1375
1506
|
max_iterations: existing.max_iterations ?? source.max_iterations,
|
|
1376
1507
|
plateau_limit: existing.plateau_limit ?? source.plateau_limit,
|
|
1508
|
+
areas_per_round: existing.areas_per_round ?? source.areas_per_round,
|
|
1509
|
+
area_concurrency: existing.area_concurrency ?? source.area_concurrency,
|
|
1377
1510
|
enabled: existing.enabled !== false || source.enabled !== false
|
|
1378
1511
|
}
|
|
1379
1512
|
: { ...source };
|
|
@@ -1391,8 +1524,10 @@ export function normalizeOfficialSourceDraft(sources = {}) {
|
|
|
1391
1524
|
? (OFFICIAL_SITES[name] ?? '')
|
|
1392
1525
|
: String(source.official_site).trim(),
|
|
1393
1526
|
enabled: source.enabled !== false,
|
|
1394
|
-
max_iterations: wasCredential ? coercePositiveInteger(source.max_iterations,
|
|
1395
|
-
plateau_limit: wasCredential ? coercePositiveInteger(source.plateau_limit,
|
|
1527
|
+
max_iterations: wasCredential ? coercePositiveInteger(source.max_iterations, SOURCE_ROUND_DEFAULTS.max_iterations) : SOURCE_ROUND_DEFAULTS.max_iterations,
|
|
1528
|
+
plateau_limit: wasCredential ? coercePositiveInteger(source.plateau_limit, SOURCE_ROUND_DEFAULTS.plateau_limit) : SOURCE_ROUND_DEFAULTS.plateau_limit,
|
|
1529
|
+
areas_per_round: wasCredential ? coerceBoundedInteger(source.areas_per_round, SOURCE_ROUND_DEFAULTS.areas_per_round, 1, 64) : SOURCE_ROUND_DEFAULTS.areas_per_round,
|
|
1530
|
+
area_concurrency: wasCredential ? coerceBoundedInteger(source.area_concurrency, SOURCE_ROUND_DEFAULTS.area_concurrency, 1, 16) : SOURCE_ROUND_DEFAULTS.area_concurrency
|
|
1396
1531
|
};
|
|
1397
1532
|
for (const field of OBSOLETE_SOURCE_FIELDS) delete normalized[field];
|
|
1398
1533
|
// A URL-mode floor is the same mismatch as its URL-mode cap: 10 URL fetches
|
|
@@ -1419,6 +1554,7 @@ function mergeProfile(profile) {
|
|
|
1419
1554
|
availability_targets: profile?.availability_targets ?? DEFAULT_AVAILABILITY_TARGETS,
|
|
1420
1555
|
speed_test: buildSpeedTestDraft(profile?.speed_test),
|
|
1421
1556
|
deploy: { ...FALLBACK_PROFILE.deploy, ...(profile?.deploy ?? {}) },
|
|
1557
|
+
network: buildNetworkDraft(profile?.network),
|
|
1422
1558
|
paths: mergedPaths,
|
|
1423
1559
|
workspace: mergedPaths
|
|
1424
1560
|
};
|