Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 32 additions & 41 deletions GEMINI.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,38 @@
# TWSE eVoting Automation Project
# TWSE eVoting Auto Project

## Project Overview
The TWSE eVoting project is a desktop application built with Electron designed to automate the shareholder voting process on the Taiwan Depository & Clearing Corporation (TDCC) e-Voting platform. It automates logging in with a national ID, identifying pending or completed votes for specified or all companies, casting votes according to predefined preferences (e.g., agree, against, abstained), capturing full-page screenshot proofs of the voting results, and securely logging out.
## What Is
Electron desktop app. Automate TDCC e-Voting. Login with ID. Find pending/voted company. Vote agree/against/abstain. Take screenshot proof. Logout safe.

### Architecture
- **Main Process (`main.js`)**: Manages the application window, sets up a `BrowserView` for loading the target website, and handles IPC communication with the frontend.
- **Preload (`preload.js`)**: Acts as a secure bridge between the renderer process and the main process using `contextBridge`.
- **Renderer (`src/renderer/`)**: Contains the user interface built with plain HTML, Vanilla CSS, and JavaScript.
- **Automation Engine (`src/automation/`)**: Contains modular scripts for interacting with the target website:
- `main_flow.js`: Orchestrates the overall logic, handles session isolation (clearing cookies/cache), and respects TDCC's system maintenance hours (00:00 - 07:00 UTC+8).
- `login.js`: Automates login, certificate selection, and handles unexpected "duplicate login" or "no pending votes" native dialogs.
- `voting.js`: Scrapes the list of target companies, navigates through the voting forms, and submits votes based on the user's selected preference.
- `screenshot.js`: Captures full-page proofs of the voting completion and saves them locally.
- `logout.js`: Safely ends the session by finding and clicking logout controls and confirmation dialogs.
- **Main (`main.js`)**: Manage window. Set `BrowserView`. Handle IPC.
- **Preload (`preload.js`)**: Secure bridge renderer ↔ main (`contextBridge`).
- **Renderer (`src/renderer/`)**: UI. HTML, Vanilla CSS, JS. No big framework.
- **Auto Engine (`src/automation/`)**: Interact with TDCC.
- `main_flow.js`: Control flow. Clear session. Respect TDCC maintenance time (00:00 - 07:00 UTC+8).
- `login.js`: Auto login. Select cert. Handle native dialogs.
- `voting.js`: Scrape target companies. Navigate vote form. Submit vote.
- `screenshot.js`: Capture full-page proof. Save local.
- `logout.js`: Find logout button. Click confirm. End session.
- `utils.js`: Reusable tools. `delay`, `randomDelay`, `safeExecute` (timeout prevent hang), `waitForNavigation`.

## Building and Running
The project is built on Node.js and Electron. Use the following commands to manage the application:
## Run App
Use Node + Electron.
1. Install: `npm install`
2. Dev run: `npm start` or `npm run dev`

* **Install Dependencies:**
```bash
npm install
```
* **Run the Application (Development):**
```bash
npm start
# or
npm run dev
```
## Dev Rules
- **Clean + Mod**: Small helper functions. JSDoc them.
- **Variables**: `const` mostly. `let` only if mutate.
- **Early Return**: Avoid deep `if/else`. Bail early if DOM missing.
- **Fast**: No long fixed `delay()`. Use `waitForNavigation`, `safeExecute`, active polling. `some()`, `find()` for text match.
- **Module**: CommonJS (`require`/`module.exports`).
- **DOM**: Execute via `webContents.executeJavaScript`.
- **Async**: `async/await`. Polling wait for dynamic element.
- **Block Dialogs**: `window.alert` block JS thread. Override them first in injected script.
- **No Crash**: `try/catch` automation steps. `sendLog` error to UI. Move to next account.
- **Config**: Put URL in `src/constants.js`.
- **UI**: Vanilla JS/CSS. No Tailwind.
- **Style**: ESLint enforce `comma-dangle` object multiline.

## Development Conventions
When modifying or extending this codebase, adhere to the following established practices:

* **Readability & Maintainability**: Keep code clean and modular. Extract complex logic into smaller, well-named helper functions (e.g., `isScreenshotExists`, `navigateBackToList`). Document functions using JSDoc.
* **Variable Declarations**: Use `const` as much as possible for variables and references that do not get reassigned. Only use `let` when mutation is strictly necessary.
* **Early Returns**: Avoid deep `if/else` nesting. Use early `return`, `continue`, or `break` statements to exit functions or loops as soon as a condition fails (e.g., if a DOM element is not found).
* **Performance & Speed Up**: Minimize arbitrary, long `delay()` calls. Prefer active polling (checking for an element in a loop with a small delay) so the script can proceed immediately when the condition is met. Use efficient array methods like `.some()` or `.find()` for text matching.
* **Module System**: The backend (Main Process) and Automation logic use CommonJS (`require` / `module.exports`).
* **DOM Interaction**: All interactions with the TDCC website are executed securely within the `BrowserView` via `webContents.executeJavaScript`.
* **Asynchronous Flow**: Web automation heavily relies on `async` / `await` and manual polling or `delay()` functions to wait for dynamic elements and page loads.
* **Native Dialog Prevention**: Native dialogs like `window.alert` and `window.confirm` block the `executeJavaScript` thread. Scripts interacting with pages that may trigger these (like `login.js` and `logout.js`) proactively override these methods at the start of their injected scripts.
* **Error Handling**: Wrap automation steps in `try/catch` blocks. Errors should be logged to the UI using the provided `sendLog` callback rather than crashing the main loop, allowing the system to proceed to the next company or account.
* **Constants**: Hardcoded URLs and configuration strings should be placed in `src/constants.js`.
* **UI Framework**: Stick to Vanilla JavaScript and pure CSS for the frontend in `src/renderer/`. TailwindCSS or other large frameworks are not used.

## Usage (AI Context)
This file serves as the primary system context for AI agents interacting with the repository. When implementing new features or fixing bugs, prioritize maintaining the robustness of the automation engine, especially regarding DOM parsing (which can fail if the target website changes) and asynchronous timing.
## Use
AI read this. Keep auto engine robust. DOM parsing break if site change. Time async careful.
17 changes: 12 additions & 5 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module.exports = [
{
ignores: ["dist/**/*", "node_modules/**/*", "assets/**/*"]
ignores: ["dist/**/*", "node_modules/**/*", "assets/**/*"],
},
{
languageOptions: {
Expand All @@ -17,11 +17,18 @@ module.exports = [
module: "readonly",
process: "readonly",
__dirname: "readonly",
Promise: "readonly"
}
Promise: "readonly",
},
},
rules: {
"indent": ["error", 2]
}
"indent": ["error", 2],
"comma-dangle": ["error", {
"arrays": "never",
"objects": "always-multiline",
"imports": "never",
"exports": "always-multiline",
"functions": "never",
}],
},
}
];
78 changes: 64 additions & 14 deletions main.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
const { app, BrowserWindow, BrowserView, ipcMain, session, dialog, Menu } = require('electron');
const { app, BrowserWindow, BrowserView, ipcMain, dialog, Menu, Notification } = require('electron');
const path = require('path');
const fs = require('fs');

let mainWindow;
let browserView;

// Mask Electron User-Agent to avoid ReCAPTCHA and bot detection
app.userAgentFallback = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
let stopRequested = false;

const CONFIG_PATH = path.join(app.getPath('userData'), 'config.json');
Expand Down Expand Up @@ -41,7 +44,7 @@ function createWindow() {
contextIsolation: true,
nodeIntegration: false,
},
title: '台灣股東會自動投票系統'
title: '股東會投票幫手',
});

mainWindow.loadFile(path.join(__dirname, 'src/renderer/index.html'));
Expand All @@ -66,7 +69,7 @@ function createWindow() {
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
}
},
});

mainWindow.setBrowserView(browserView);
Expand Down Expand Up @@ -118,7 +121,7 @@ function setupApplicationMenu() {
{ role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' }
]
],
}]
: []),
{
Expand All @@ -134,14 +137,14 @@ function setupApplicationMenu() {
? [
{ role: 'pasteAndMatchStyle' },
{ role: 'delete' },
{ label: '全選 (Select All)', role: 'selectAll' },
{ label: '全選 (Select All)', role: 'selectAll' }
]
: [
{ label: '刪除 (Delete)', role: 'delete' },
{ type: 'separator' },
{ label: '全選 (Select All)', role: 'selectAll' }
])
]
],
},
{
label: '檢視 (View)',
Expand All @@ -156,15 +159,15 @@ function setupApplicationMenu() {
if (browserView) {
browserView.webContents.toggleDevTools();
}
}
},
},
{ type: 'separator' },
{ label: '實際大小 (Reset Zoom)', role: 'resetZoom' },
{ label: '放大 (Zoom In)', role: 'zoomIn' },
{ label: '縮小 (Zoom Out)', role: 'zoomOut' },
{ type: 'separator' },
{ label: '切換全螢幕 (Toggle Full Screen)', role: 'togglefullscreen' }
]
],
},
{
label: '關於 (About)',
Expand All @@ -176,7 +179,7 @@ function setupApplicationMenu() {
const pkg = require('./package.json');
let releaseDate = '未知';
try {
// 在封裝後的 asar 檔案中,package.json 的修改時間即為打包/釋出時間
// In the packaged asar file, the package.json modification time serves as the release date
const stat = require('fs').statSync(require('path').join(__dirname, 'package.json'));
releaseDate = stat.mtime.toISOString().split('T')[0];
} catch (e) { }
Expand All @@ -187,7 +190,7 @@ function setupApplicationMenu() {
message: `TWSE Auto eVoting\n\n版本 (Version): ${pkg.version}\n日期 (Release Date): ${releaseDate}`,
buttons: ['使用說明 (README)', 'GitHub', '作者網站', '關閉'],
defaultId: 0,
cancelId: 3
cancelId: 3,
});

if (response === 0) {
Expand All @@ -197,9 +200,9 @@ function setupApplicationMenu() {
} else if (response === 2) {
shell.openExternal('https://ssarcandy.tw');
}
}
},
}
]
],
}
];

Expand All @@ -225,26 +228,73 @@ ipcMain.handle('start-voting', async (event, { ids, outputDir, folderStructure }
stopRequested = false;
const automation = require('./src/automation/main_flow');
try {
await automation.run(browserView.webContents, ids, (msg) => {
const stats = await automation.run(browserView.webContents, ids, (msg) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('log', String(msg));
}
}, (progress) => {
if (mainWindow && !mainWindow.isDestroyed()) {
const sanitizedProgress = JSON.parse(JSON.stringify(progress));
mainWindow.webContents.send('progress', sanitizedProgress);

const { id, screenshot } = sanitizedProgress;
let percent = 0;
if (id && id.total > 0) {
let baseCompleted = id.current;
let subProgress = 0;
if (screenshot && screenshot.total > 0) {
baseCompleted = id.current - 1;
subProgress = screenshot.current / screenshot.total;
}
percent = Math.floor(((baseCompleted + subProgress) / id.total) * 100);
percent = Math.min(100, Math.max(0, percent));
}
mainWindow.setTitle(`(${percent}%) 股東會投票幫手`);
}
}, () => stopRequested, outputDir, folderStructure);

if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.setTitle('股東會投票幫手');
if (stats) {
mainWindow.webContents.send('log', `[系統] 完成。累計投票: ${stats.voted},累計截圖: ${stats.screenshoted}`);
}
if (!mainWindow.isFocused()) {
mainWindow.flashFrame(true);
if (Notification.isSupported()) {
new Notification({
title: '投票完成',
body: stats ? `所有作業已結束。累計投票: ${stats.voted},累計截圖: ${stats.screenshoted}` : '所有排定的股東會投票已結束。',
icon: path.join(__dirname, 'assets/icons/icon.png'),
}).show();
}
}
}

return JSON.parse(JSON.stringify({ success: true }));
} catch (error) {
console.error('Automation error:', error);

if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.setTitle('股東會投票幫手');
if (!mainWindow.isFocused()) {
mainWindow.flashFrame(true);
if (Notification.isSupported()) {
new Notification({
title: '投票發生錯誤',
body: '執行過程中發生錯誤,請查看應用程式日誌。',
icon: path.join(__dirname, 'assets/icons/icon.png'),
}).show();
}
}
}

return JSON.parse(JSON.stringify({ success: false, error: String(error.message) }));
}
});

ipcMain.handle('select-directory', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory']
properties: ['openDirectory'],
});
const path = result.canceled ? null : result.filePaths[0];
return JSON.parse(JSON.stringify(path));
Expand Down
Loading