Files
bot-rates/SUMMARY.md

127 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 開發過程摘要
記錄這個專案的演進脈絡、遇到的問題、以及為什麼做出這些選擇。
## 需求演進
| 階段 | 需求 | 產出 |
|------|------|------|
| 1 | 抓台銀 USD / JPY / EUR 即期賣出 | Python CLI (`bot_rates.py`) |
| 2 | 要給別人用、Windows、GUI、雙擊即跑、不要裝東西 | Python tkinter + PyInstaller 方案 |
| 3 | 太麻煩,改用 Rust使用者無 Windows 機器 | Rust + `eframe` + 交叉編譯 |
| 4 | 也給我一份 macOS 能跑的 | `BotRates.app` bundle |
| 5 | 壓縮 exe 方便傳送 | `bot_rates.zip`5.4 MB → 2.7 MB |
| 6 | 加上金額輸入 / 即時換算 | 三欄布局:幣別 / 輸入 / TWD |
| 7 | 換算結果要能直接複製 | 「複製」按鈕 + 可選取文字 |
## 階段 1Python CLI
直接用 `urllib` 抓台銀 CSV起初用 `csv.DictReader`但踩到一個坑——CSV 有**重複欄名**(買入、賣出區段都用 `匯率 / 現金 / 即期``DictReader` 只會保留最後一個同名欄位,所以找不到「本行即期賣出」。
**解法**:改用位置索引,`即期賣出` 在 col 13。
## 階段 2Python GUI最終未採用
計畫:`tkinter` + ttk 美化 + `PyInstaller --onefile --windowed` 打包成 exe。寫了 `bot_rates_gui.py``build.bat`
**問題**:使用者手上沒有 Windows 機器PyInstaller 必須在目標 OS 上執行,無法從 macOS 直接產出 Windows exe。只能給使用者 `build.bat` 要他在 Windows 上自己打包——違背「不要裝東西」的初衷。
## 階段 3改用 Rust
Rust 可以從 macOS 乾淨地交叉編譯到 Windows單一靜態執行檔零 runtime 依賴,剛好解決 Python 方案的痛點。
### 技術選型
| 選擇 | 理由 |
|------|------|
| `eframe` / `egui` | Immediate mode GUI使用 OpenGLWindows 內建),不需要 GTK / Qt 這類系統依賴。交叉編譯幾乎沒坑。 |
| `ureq` + `rustls` | `reqwest` 預設用 `native-tls` 會綁 OpenSSL 或 Windows SChannel交叉編譯會有系統 library 問題。`ureq``rustls` 是純 Rust無系統依賴。 |
| `x86_64-pc-windows-gnu` | 用 mingw-w64 當 linker`-msvc` 少了需要 Windows SDK 的麻煩。`brew install mingw-w64` 就好。 |
### 踩過的坑
**LLVM 在 release 編譯時 SIGSEGV**
```
error: rustc interrupted by SIGSEGV, printing backtrace
llvm::DeadArgumentEliminationPass::run
```
第一次 release 編譯時,`Cargo.toml` 用了 `opt-level = "z"` + `lto = true`LLVM 的 DeadArgumentElimination pass 在處理 `eframe` 依賴時堆疊用光。
**解法**
1.`opt-level``"z"` 改成 `3``lto``true` 改成 `"thin"`(降低 pass 壓力)
2. 加上 `RUST_MIN_STACK=16777216`(把執行緒堆疊從預設 2MB 拉到 16MB
兩個同時做下去就過了。
**CJK 字型**
egui 預設字型不含中文字,幣別名稱會顯示成「豆腐」。
**解法**:不 bundle 字型(會讓 exe 大 5-15MB改在執行期讀取作業系統字型。
- Windows`C:\Windows\Fonts\msjh.ttc`(微軟正黑體)
- macOS`/System/Library/Fonts/PingFang.ttc`
fallback 鏈有多個候選msjhl / msyh / simhei找到第一個能讀的就用。
## 階段 4macOS `.app` bundle
`cargo build --release` 只產出裸執行檔,從 Finder 雙擊會跳 terminal 然後才開 GUI體驗差。
**解法**:手動組 `.app` bundle——其實就是特定目錄結構加一個 `Info.plist`
```
BotRates.app/
├── Contents/
│ ├── Info.plist # 告訴 Finder 這是 app
│ └── MacOS/BotRates # 實際執行檔
```
寫 plist 時標示 `CFBundlePackageType = APPL``CFBundleExecutable = BotRates` 即可。另外跑 `xattr -cr` 清掉 quarantine 屬性避免 Gatekeeper 擋下。
## 階段 5壓縮
`bot_rates.exe` 5.4 MB`zip -9` 後 2.7 MB壓縮率 50%。PE 檔因為有大量重複的 LLVM / rustc 樣板程式碼壓縮效果很好。
## 階段 6金額換算
重新設計每一列為三欄 horizontal layout
```
[幣別+匯率 寬110] [輸入 寬120] [ → ] [TWD 靠右]
```
狀態放在 `App.amounts: HashMap<String, String>`(幣別 → 輸入字串)。為什麼存字串而不是 `f64`?因為使用者輸入過程中可能有中間態(例如 `"100."`、空字串、`"1,000"`),存字串才能保留原樣。
輸入解析用 `parse_amount`,忽略逗號和空白再 parse。
TWD 顯示用 `format_twd`,手刻千分位 separator沒引入 `num-format` crate 以減少 binary size
## 階段 7複製功能
兩種複製方式並存:
1. **按鈕複製**(主要 UX按「複製」→ `ui.output_mut(|o| o.copied_text = raw)`,複製純數字 `3162.00`(不含 `NT$`、不含逗號)。原因:這樣貼到 Excel 會被識別成數字而不是文字。
2. **選取複製**`egui::Label::new(...).selectable(true)`,使用者可以用滑鼠拉選 TWD 文字後 `Cmd/Ctrl+C`
按下按鈕後短暫顯示「已複製」1.2 秒),用 `Option<(String, Instant)>` 追蹤狀態 + `ctx.request_repaint_after` 觸發回復重繪。
## 最終成果
| 項目 | 數值 |
|------|------|
| Windows exe 大小 | 5.4 MBzip 後 2.7 MB |
| macOS 執行檔大小 | 5.2 MB |
| 執行期依賴 | 無(兩平台皆零依賴) |
| 首次冷啟動 | < 1 秒 |
| 抓匯率耗時 | 網路 + 解析 < 500ms |
| 總原始碼行數 | ~310 行 Rust |
## 如果要擴充
- **加更多幣別**:改 `TARGETS` 即可,其他地方都自動處理。
- **換成即期買入 / 現金匯率**:改 `fetch_rates` 裡的 col index3 買入 / 13 賣出 / 2 現金買入 / 12 現金賣出)。
- **雙向換算**TWD → 外幣):在每列加第二個輸入框,狀態多存一組字串;注意避免無限循環更新。
- **歷史紀錄 / 圖表**:台銀有 `flcsv/1/month` 等端點可以抓歷史,但就不是「即期」而是均價了。