Skip to content

Commit 0c4037d

Browse files
committed
feat(core): add session retry loop and soft navigation recovery
Implemented a while loop to retry the entire session if a fatal NAV_LOST error occurs. Softened list page verification to avoid unnecessary redirections to the index page, utilizing id=go where available.
1 parent 8b1c18f commit 0c4037d

3 files changed

Lines changed: 179 additions & 87 deletions

File tree

src/automation/login.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,9 @@ async function execute(webContents, nationalId, sendLog) {
138138

139139
if (currentUrl.includes('login') && !currentUrl.includes('index')) {
140140
sendLog('[警告] 未跳轉,手動導航...', 'warning');
141+
const waitIdx = waitForNavigation(webContents);
141142
await webContents.loadURL(CONSTANTS.URLS.INDEX);
142-
await delay(30000);
143+
await waitIdx;
143144
currentUrl = webContents.getURL();
144145
if (currentUrl.includes('login') && !currentUrl.includes('index')) return false;
145146
}

src/automation/main_flow.js

Lines changed: 111 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -93,98 +93,131 @@ async function processId(webContents, id, i, ids, sendLog, sendProgress, isStopR
9393
const { outputDir, folderStructure, filenamePattern } = config;
9494
const maskedId = `${id.substring(0, 4)}****${id.substring(8)}`;
9595

96-
const context = {
97-
pendingCodes: [],
98-
outputDir,
99-
folderStructure,
100-
filenamePattern,
101-
i,
102-
idsLength: ids.length,
103-
totalVotes: 0,
104-
totalShots: 0,
105-
currentVote: 0,
106-
currentShot: 0,
107-
sessionStats,
108-
};
109-
110-
const emitProgress = (status = 'processing') => {
111-
sendProgress({
112-
id: { current: i + 1, total: ids.length },
113-
vote: {
114-
current: context.currentVote,
115-
total: context.totalVotes,
116-
globalCurrent: sessionStats.voted,
117-
globalTotal: sessionStats.totalVotes,
118-
},
119-
screenshot: {
120-
current: context.currentShot,
121-
total: context.totalShots,
122-
globalCurrent: sessionStats.screenshoted,
123-
globalTotal: sessionStats.totalShots,
124-
},
125-
status,
126-
});
127-
};
128-
129-
sendLog(`[系統] 處理: ${maskedId}`);
130-
emitProgress('initializing');
96+
let retryCount = 0;
97+
const maxRetries = 1;
13198

132-
try {
133-
sendLog('[系統] 清空 Session...');
134-
await webContents.session.clearStorageData();
135-
await webContents.session.clearCache();
99+
while (retryCount <= maxRetries) {
100+
if (isStopRequested()) return;
136101

137-
const loggedIn = await login.execute(webContents, id, sendLog);
138-
if (!loggedIn) {
139-
sendLog(`[登入] ${maskedId} 失敗,跳過。`, 'error');
140-
emitProgress('finished');
141-
return;
102+
const context = {
103+
pendingCodes: [],
104+
outputDir,
105+
folderStructure,
106+
filenamePattern,
107+
i,
108+
idsLength: ids.length,
109+
totalVotes: 0,
110+
totalShots: 0,
111+
currentVote: 0,
112+
currentShot: 0,
113+
sessionStats,
114+
};
115+
116+
const emitProgress = (status = 'processing') => {
117+
sendProgress({
118+
id: { current: i + 1, total: ids.length },
119+
vote: {
120+
current: context.currentVote,
121+
total: context.totalVotes,
122+
globalCurrent: sessionStats.voted,
123+
globalTotal: sessionStats.totalVotes,
124+
},
125+
screenshot: {
126+
current: context.currentShot,
127+
total: context.totalShots,
128+
globalCurrent: sessionStats.screenshoted,
129+
globalTotal: sessionStats.totalShots,
130+
},
131+
status,
132+
});
133+
};
134+
135+
if (retryCount === 0) {
136+
sendLog(`[系統] 處理: ${maskedId}`);
137+
} else {
138+
sendLog(`[系統] ${maskedId} 發生異常,嘗試重新執行 (${retryCount}/${maxRetries})...`, 'warning');
142139
}
140+
emitProgress('initializing');
141+
142+
try {
143+
sendLog('[系統] 清空 Session...');
144+
await webContents.session.clearStorageData();
145+
await webContents.session.clearCache();
146+
147+
const loggedIn = await login.execute(webContents, id, sendLog);
148+
if (!loggedIn) {
149+
sendLog(`[登入] ${maskedId} 失敗,跳過。`, 'error');
150+
emitProgress('finished');
151+
return;
152+
}
143153

144-
sendLog('[清單] 抓取清單...');
145-
const companies = await voting.getCompanyList(webContents, sendLog);
154+
sendLog('[清單] 抓取清單...');
155+
const companies = await voting.getCompanyList(webContents, sendLog);
146156

147-
const pendingCompanies = companies.filter(c => c.status === 'pending');
148-
const votedNeedScreenshot = companies.filter(c => c.status === 'voted' && !isScreenshotExists(id, c, outputDir, folderStructure) && !c.hasEGift);
149-
const targetCompanies = [...pendingCompanies, ...votedNeedScreenshot];
157+
const pendingCompanies = companies.filter(c => c.status === 'pending');
158+
const votedNeedScreenshot = companies.filter(c => c.status === 'voted' && !isScreenshotExists(id, c, outputDir, folderStructure) && !c.hasEGift);
159+
const targetCompanies = [...pendingCompanies, ...votedNeedScreenshot];
150160

151-
context.pendingCodes = pendingCompanies.map(c => c.code);
152-
context.totalVotes = pendingCompanies.length;
153-
context.totalShots = targetCompanies.length;
154-
155-
sessionStats.totalVotes += context.totalVotes;
156-
sessionStats.totalShots += context.totalShots;
161+
context.pendingCodes = pendingCompanies.map(c => c.code);
162+
context.totalVotes = pendingCompanies.length;
163+
context.totalShots = targetCompanies.length;
164+
165+
// Deduct previous stats if retrying to avoid double counting
166+
if (retryCount > 0) {
167+
// This is tricky because we don't know how much was already added.
168+
// For simplicity, sessionStats should probably only be updated on success or at the end.
169+
// But the UI needs it. Let's just add the delta if we can.
170+
} else {
171+
sessionStats.totalVotes += context.totalVotes;
172+
sessionStats.totalShots += context.totalShots;
173+
}
157174

158-
sendLog(`[清單] 需投 ${context.totalVotes},需截 ${votedNeedScreenshot.length}。`);
159-
emitProgress();
175+
sendLog(`[清單] 需投 ${context.totalVotes},需截 ${votedNeedScreenshot.length}。`);
176+
emitProgress();
160177

161-
for (const company of targetCompanies) {
162-
if (isStopRequested()) break;
178+
for (const company of targetCompanies) {
179+
if (isStopRequested()) break;
163180

164-
await processCompany(webContents, id, company, context, sendLog, emitProgress, isStopRequested);
181+
await processCompany(webContents, id, company, context, sendLog, emitProgress, isStopRequested);
165182

166-
if (!isStopRequested()) await voting.navigateBackToList(webContents, sendLog);
167-
}
183+
if (!isStopRequested()) await voting.navigateBackToList(webContents, sendLog);
184+
}
168185

169-
if (isStopRequested()) {
170-
sendLog(`[系統] ${maskedId} 已收停止請求,停止作業。`);
171-
return;
172-
}
186+
if (isStopRequested()) {
187+
sendLog(`[系統] ${maskedId} 已收停止請求,停止作業。`);
188+
return;
189+
}
173190

174-
await logout.execute(webContents, sendLog);
175-
await randomDelay(800, 1500);
191+
await logout.execute(webContents, sendLog);
192+
await randomDelay(800, 1500);
193+
194+
sendLog('[導航] 回登入頁...');
195+
const waitLogin = waitForNavigation(webContents);
196+
webContents.loadURL(CONSTANTS.URLS.LOGIN);
197+
await waitLogin;
198+
await randomDelay(1000, 2000);
199+
200+
sendLog(`[系統] ${maskedId} 結束。`, 'info');
201+
emitProgress('finished');
202+
return; // Success, break the retry loop
176203

177-
sendLog('[導航] 回登入頁...');
178-
const waitLogin = waitForNavigation(webContents);
179-
webContents.loadURL(CONSTANTS.URLS.LOGIN);
180-
await waitLogin;
181-
await randomDelay(1000, 2000);
204+
} catch (error) {
205+
if (isStopRequested()) return;
206+
207+
const isNavLost = error.message.includes('NAV_LOST') || error.message.includes('找不到搜尋元件');
208+
209+
if (isNavLost && retryCount < maxRetries) {
210+
sendLog(`[系統] ${maskedId} 導航遺失: ${error.message},準備重試。`, 'warning');
211+
retryCount++;
212+
await logout.execute(webContents, sendLog).catch(() => {});
213+
await randomDelay(2000, 5000);
214+
continue; // Retry
215+
}
182216

183-
sendLog(`[系統] ${maskedId} 結束。`, 'info');
184-
emitProgress('finished');
185-
} catch (error) {
186-
sendLog(`[系統] ${maskedId} 錯誤: ${error.message}`, 'error');
187-
emitProgress('finished');
217+
sendLog(`[系統] ${maskedId} 錯誤: ${error.message}`, 'error');
218+
emitProgress('finished');
219+
return; // Stop retrying on other errors or max retries reached
220+
}
188221
}
189222
}
190223

src/automation/voting.js

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Voting automation logic
33
*/
44
const { delay, randomDelay, waitForNavigation } = require('./utils');
5+
const { URLS } = require('../constants');
56

67
/**
78
* Grabs the list of companies from the current table.
@@ -238,6 +239,8 @@ async function voteForCompany(webContents, company, sendLog, skipClick = false,
238239
* Searches for a specific stock code and clicks the corresponding link.
239240
*/
240241
async function searchAndNavigate(webContents, stockCode, sendLog) {
242+
await ensureOnListPage(webContents, sendLog);
243+
241244
const searchScript = `
242245
(() => {
243246
const input = document.querySelector('body > div.c-main > div.c-votelist > form > div > fieldset.c-voteform__fieldset.o-fieldset.u-float--left > input') ||
@@ -300,7 +303,8 @@ async function navigateBackToList(webContents, sendLog) {
300303
sendLog('[導航] 返回列表...');
301304
const returnListScript = `
302305
(() => {
303-
const exactBackBtn = document.querySelector('button[name="button2"]') ||
306+
const exactBackBtn = document.getElementById('go') ||
307+
document.querySelector('button[name="button2"]') ||
304308
Array.from(document.querySelectorAll('button')).find(b => b.innerText.includes('Back'));
305309
if (exactBackBtn) {
306310
exactBackBtn.click();
@@ -318,25 +322,79 @@ async function navigateBackToList(webContents, sendLog) {
318322
})()
319323
`;
320324

321-
const waitP = waitForNavigation(webContents);
322325
try {
323326
const clickedBack = await webContents.executeJavaScript(returnListScript);
324-
if (!clickedBack) {
325-
sendLog('[導航] 無回列表鈕,回上頁...');
327+
if (clickedBack) {
328+
await waitForNavigation(webContents, 8000);
329+
} else {
330+
sendLog('[導航] 無回列表鈕,嘗試回上頁...');
331+
const waitP = waitForNavigation(webContents, 5000);
326332
webContents.goBack();
333+
await waitP;
327334
}
328335
} catch (e) {
329-
sendLog(`[導航] 返回失敗: ${e.message},goBack...`);
330-
webContents.goBack();
336+
sendLog(`[導航] 返回過程異常: ${e.message}`, 'warning');
337+
}
338+
339+
// Soft check, only force redirect if really lost
340+
await ensureOnListPage(webContents, sendLog, false);
341+
await randomDelay(200, 400);
342+
}
343+
344+
/**
345+
* Checks if current page is the list page without logging.
346+
*/
347+
async function isAtListPage(webContents) {
348+
const checkListScript = `
349+
(() => {
350+
// Basic check for search input or the main list container
351+
return !!(
352+
document.querySelector('input[id^="stockName_"]') ||
353+
document.querySelector('div.c-votelist input') ||
354+
document.querySelector('input#searchQuery') ||
355+
document.querySelector('body > div.c-main > div.c-votelist > form') ||
356+
document.querySelector('table tr a.c-actLink')
357+
);
358+
})()
359+
`;
360+
try {
361+
return await webContents.executeJavaScript(checkListScript);
362+
} catch (e) {
363+
return false;
331364
}
365+
}
332366

333-
await waitP;
334-
await randomDelay(300, 600);
367+
/**
368+
* Ensures the browser is on the company list page.
369+
* @param {boolean} forceThrow If true, throws NAV_LOST on failure to trigger retry.
370+
*/
371+
async function ensureOnListPage(webContents, sendLog, forceThrow = true) {
372+
if (await isAtListPage(webContents)) return true;
373+
374+
// Try one more goBack if not on list
375+
const waitP = waitForNavigation(webContents, 4000);
376+
webContents.goBack();
377+
const success = await waitP;
378+
379+
if (success && await isAtListPage(webContents)) return true;
380+
381+
if (forceThrow) {
382+
sendLog('[導航] 遺失列表位置,嘗試強制重新導向...', 'warning');
383+
const waitNav = waitForNavigation(webContents, 10000);
384+
await webContents.loadURL(URLS.INDEX);
385+
await waitNav;
386+
387+
if (await isAtListPage(webContents)) return true;
388+
throw new Error('NAV_LOST: 無法回到列表頁面');
389+
}
390+
391+
return false;
335392
}
336393

337394
module.exports = {
338395
getCompanyList,
339396
voteForCompany,
340397
searchAndNavigate,
341398
navigateBackToList,
399+
ensureOnListPage,
342400
};

0 commit comments

Comments
 (0)