@swimmingliu/autovpn 2.0.2 → 2.0.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.
|
@@ -76,6 +76,45 @@ export function readLatestStageStatuses(dbPath) {
|
|
|
76
76
|
catch { /* malformed or locked databases are ignored */ }
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
// Reports are written at stage boundaries. A running UI needs current totals,
|
|
80
|
+
// and opening RunStore would migrate/write the database during observation.
|
|
81
|
+
export function readRunProgress(dbPath) {
|
|
82
|
+
if (!fs.existsSync(dbPath))
|
|
83
|
+
return undefined;
|
|
84
|
+
let db;
|
|
85
|
+
try {
|
|
86
|
+
db = new DatabaseSync(dbPath, { readOnly: true });
|
|
87
|
+
db.exec('PRAGMA busy_timeout=5000; BEGIN');
|
|
88
|
+
const runId = db.prepare('SELECT run_id FROM runs ORDER BY run_id DESC LIMIT 1').get()?.run_id;
|
|
89
|
+
if (typeof runId !== 'number' && typeof runId !== 'bigint')
|
|
90
|
+
return undefined;
|
|
91
|
+
const raw = Number(db.prepare('SELECT COUNT(*) AS n FROM raw_observations WHERE run_id=?').get(runId)?.n ?? 0);
|
|
92
|
+
const deduped = Number(db.prepare('SELECT COUNT(*) AS n FROM pipeline_nodes WHERE run_id=?').get(runId)?.n ?? 0);
|
|
93
|
+
const speed = Number(db.prepare("SELECT COUNT(*) AS n FROM speed_results WHERE run_id=? AND status='speed_passed'").get(runId)?.n ?? 0);
|
|
94
|
+
const availability = Number(db.prepare("SELECT COUNT(*) AS n FROM availability_results WHERE run_id=? AND status='availability_passed'").get(runId)?.n ?? 0);
|
|
95
|
+
const sources = {};
|
|
96
|
+
const completeOwnership = Number(db.prepare('SELECT COUNT(*) AS n FROM pipeline_nodes WHERE run_id=? AND first_source IS NULL').get(runId)?.n ?? 0) === 0;
|
|
97
|
+
for (const row of db.prepare('SELECT source,COUNT(*) AS n FROM raw_observations WHERE run_id=? GROUP BY source ORDER BY MIN(observation_id)').all(runId)) {
|
|
98
|
+
if (typeof row.source === 'string')
|
|
99
|
+
sources[row.source] = { raw_links: Number(row.n), ...(completeOwnership ? { deduped_links: 0 } : {}) };
|
|
100
|
+
}
|
|
101
|
+
if (completeOwnership) {
|
|
102
|
+
for (const row of db.prepare('SELECT first_source AS source,COUNT(*) AS n FROM pipeline_nodes WHERE run_id=? AND first_source IS NOT NULL GROUP BY first_source').all(runId)) {
|
|
103
|
+
if (typeof row.source === 'string')
|
|
104
|
+
sources[row.source] = { raw_links: sources[row.source]?.raw_links ?? 0, deduped_links: Number(row.n) };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
db.exec('COMMIT');
|
|
108
|
+
return { counts: { raw_links: raw, deduped_links: deduped, speedtest_links: speed, availability_links: availability }, source_counts: sources };
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Legacy or incomplete databases retain the existing report/event path.
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
db?.close();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
79
118
|
export class RunStore {
|
|
80
119
|
db;
|
|
81
120
|
statements = new Map();
|
package/dist/server/runtime.js
CHANGED
|
@@ -9,6 +9,7 @@ import { latestJobId, loadJob } from '../jobs/read.js';
|
|
|
9
9
|
import { AccountStore } from '../pool/account-store.js';
|
|
10
10
|
import { normalizeBrand } from '../pool/brands.js';
|
|
11
11
|
import { parseAccountFile } from '../pool/csv.js';
|
|
12
|
+
import { readRunProgress } from '../pipeline/run-store.js';
|
|
12
13
|
import { resolveArtifactsRoot, resolveProfilePath } from '../runtime/paths.js';
|
|
13
14
|
const DEPLOY_SECRET_KEYS = new Set([
|
|
14
15
|
'cloudflare_api_token',
|
|
@@ -66,9 +67,13 @@ function normalizeLatestArtifact(projectRoot, env) {
|
|
|
66
67
|
return undefined;
|
|
67
68
|
}
|
|
68
69
|
const preview = previewArtifact(String(latest.artifact_dir));
|
|
70
|
+
const progress = readRunProgress(path.join(String(latest.artifact_dir), 'run.db'));
|
|
69
71
|
return {
|
|
70
72
|
...latest,
|
|
71
73
|
...preview,
|
|
74
|
+
// The report can still be empty mid-stage. Its display preview must not
|
|
75
|
+
// replace the authoritative totals that already exist in the run database.
|
|
76
|
+
...(progress ? { counts: { ...(preview.counts ?? {}), ...progress.counts }, source_counts: progress.source_counts, progress_snapshot: progress } : {}),
|
|
72
77
|
outputFiles: (preview.files ?? []),
|
|
73
78
|
nodeRows: []
|
|
74
79
|
};
|
|
@@ -345,7 +350,16 @@ export function createServerRuntime(options) {
|
|
|
345
350
|
if (nextRunState) {
|
|
346
351
|
runState = nextRunState;
|
|
347
352
|
}
|
|
348
|
-
|
|
353
|
+
// A follower can deliver old result lines after loadState. Send
|
|
354
|
+
// absolute database totals so those lines cannot add twice.
|
|
355
|
+
const metricEvent = typeof event === 'object' && event !== null && 'type' in event
|
|
356
|
+
&& ['extract_iteration', 'extract_source_completed', 'speedtest_result', 'availability_link_result'].includes(String(event.type));
|
|
357
|
+
const job = metricEvent ? loadJob(options.projectRoot, jobId, options.env) : undefined;
|
|
358
|
+
const artifactDir = String(job?.artifact_dir ?? '');
|
|
359
|
+
const progress = artifactDir ? readRunProgress(path.join(artifactDir, 'run.db')) : undefined;
|
|
360
|
+
publish(progress && typeof event === 'object'
|
|
361
|
+
? { ...event, progress_snapshot: { ...progress, artifact_dir: artifactDir } }
|
|
362
|
+
: event);
|
|
349
363
|
}
|
|
350
364
|
}
|
|
351
365
|
}
|
package/dist/web/renderer/app.js
CHANGED
|
@@ -138,7 +138,8 @@ const state = {
|
|
|
138
138
|
qr: createQrState(),
|
|
139
139
|
runStartedAt: null,
|
|
140
140
|
lastUpdateAt: null,
|
|
141
|
-
modalTransform: ''
|
|
141
|
+
modalTransform: '',
|
|
142
|
+
progressSnapshot: null
|
|
142
143
|
};
|
|
143
144
|
|
|
144
145
|
let settingsDrawerOpener = null;
|
|
@@ -222,11 +223,13 @@ function hydrateHistoricalEvents(events = []) {
|
|
|
222
223
|
}
|
|
223
224
|
const previousRunState = state.runState;
|
|
224
225
|
const previousRunResult = state.runResult;
|
|
226
|
+
const snapshot = state.progressSnapshot;
|
|
225
227
|
for (const event of events) {
|
|
226
228
|
handlePipelineEvent(event, { historical: true });
|
|
227
229
|
}
|
|
228
230
|
state.runState = previousRunState;
|
|
229
231
|
state.runResult = previousRunState === 'idle' ? previousRunResult : previousRunState;
|
|
232
|
+
if (snapshot) applyProgressSnapshot(snapshot);
|
|
230
233
|
}
|
|
231
234
|
|
|
232
235
|
function bindActions() {
|
|
@@ -1554,7 +1557,24 @@ function finishRun(result = {}) {
|
|
|
1554
1557
|
void hydrateRetryArtifacts();
|
|
1555
1558
|
}
|
|
1556
1559
|
|
|
1560
|
+
function applyProgressSnapshot(snapshot) {
|
|
1561
|
+
if (!snapshot?.counts || !snapshot.source_counts) return;
|
|
1562
|
+
state.progressSnapshot = snapshot;
|
|
1563
|
+
state.counts = normalizeCounts({ ...state.counts, ...snapshot.counts });
|
|
1564
|
+
state.sourceCounts = normalizeSourceCounts(snapshot.source_counts);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1557
1567
|
function handlePipelineEvent(event, options = {}) {
|
|
1568
|
+
handlePipelineEventDetails(event, options);
|
|
1569
|
+
if (event.progress_snapshot && !options.historical
|
|
1570
|
+
&& (!event.progress_snapshot.artifact_dir || event.progress_snapshot.artifact_dir === state.artifactDir)) {
|
|
1571
|
+
applyProgressSnapshot(event.progress_snapshot);
|
|
1572
|
+
touchUpdate();
|
|
1573
|
+
renderRuntimeOnly({ chrome: false });
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function handlePipelineEventDetails(event, options = {}) {
|
|
1558
1578
|
const historical = Boolean(options.historical);
|
|
1559
1579
|
if (event.type === 'server_state') {
|
|
1560
1580
|
const nextRunState = String(event.run_state ?? '');
|
|
@@ -1703,7 +1723,7 @@ function handlePipelineEvent(event, options = {}) {
|
|
|
1703
1723
|
}
|
|
1704
1724
|
|
|
1705
1725
|
if (event.type === 'extract_source_completed') {
|
|
1706
|
-
updateExtractMetrics({ source_name: event.source_name, total_links: event.raw_links });
|
|
1726
|
+
if (!event.progress_snapshot && !(historical && state.progressSnapshot)) updateExtractMetrics({ source_name: event.source_name, total_links: event.raw_links });
|
|
1707
1727
|
appendLog(`[extract] ${event.source_name} 完成,成功 ${event.successful_iterations ?? 0} 次,失败 ${event.failed_iterations ?? 0} 次,原始节点 ${event.raw_links ?? 0} 个`, {
|
|
1708
1728
|
kind: 'stage',
|
|
1709
1729
|
stage: 'extract',
|
|
@@ -1713,7 +1733,7 @@ function handlePipelineEvent(event, options = {}) {
|
|
|
1713
1733
|
}
|
|
1714
1734
|
|
|
1715
1735
|
if (event.type === 'extract_iteration') {
|
|
1716
|
-
updateExtractMetrics(event);
|
|
1736
|
+
if (!event.progress_snapshot && !(historical && state.progressSnapshot)) updateExtractMetrics(event);
|
|
1717
1737
|
appendLog(`[extract] ${event.source_name} #${event.iteration ?? 0} 新增 ${event.new_items ?? 0} 个,本次解析 ${event.extracted_links ?? 0} 个,累计 ${event.total_links ?? 0} 个`, {
|
|
1718
1738
|
kind: 'stage',
|
|
1719
1739
|
stage: 'extract',
|
|
@@ -1723,6 +1743,7 @@ function handlePipelineEvent(event, options = {}) {
|
|
|
1723
1743
|
}
|
|
1724
1744
|
|
|
1725
1745
|
if (event.type === 'speedtest_result') {
|
|
1746
|
+
if (event.progress_snapshot || (historical && state.progressSnapshot)) return;
|
|
1726
1747
|
if (event.passed_threshold) {
|
|
1727
1748
|
state.counts.speedtest_links = Number(state.counts.speedtest_links ?? 0) + 1;
|
|
1728
1749
|
}
|
|
@@ -1732,6 +1753,7 @@ function handlePipelineEvent(event, options = {}) {
|
|
|
1732
1753
|
}
|
|
1733
1754
|
|
|
1734
1755
|
if (event.type === 'availability_link_result') {
|
|
1756
|
+
if (event.progress_snapshot || (historical && state.progressSnapshot)) return;
|
|
1735
1757
|
if (event.all_passed) {
|
|
1736
1758
|
state.counts.availability_links = Number(state.counts.availability_links ?? 0) + 1;
|
|
1737
1759
|
}
|
|
@@ -1770,6 +1792,7 @@ function normalizeStageStatuses(stageStatuses = {}) {
|
|
|
1770
1792
|
}
|
|
1771
1793
|
|
|
1772
1794
|
function resetRunScopedProgress() {
|
|
1795
|
+
state.progressSnapshot = null;
|
|
1773
1796
|
state.stageStatus = {};
|
|
1774
1797
|
state.counts = {};
|
|
1775
1798
|
state.sourceCounts = {};
|
|
@@ -1875,6 +1898,8 @@ function hydrateArtifactState(result) {
|
|
|
1875
1898
|
state.artifactDir = result.artifact_dir;
|
|
1876
1899
|
state.counts = normalizeCounts(result.counts ?? {});
|
|
1877
1900
|
state.sourceCounts = normalizeSourceCounts(result.source_counts ?? {});
|
|
1901
|
+
// Bounded log history cannot reconstruct metrics that predate its tail.
|
|
1902
|
+
state.progressSnapshot = result.progress_snapshot ?? null;
|
|
1878
1903
|
state.extractDedupedFingerprints = new Set();
|
|
1879
1904
|
state.outputFiles = result.outputFiles ?? [];
|
|
1880
1905
|
state.nodeRows = result.nodeRows ?? [];
|