好的!我幫你把今天的鐵人賽 Day14 文章生好(主題延續我們這幾天做的 LINE 金融助理:修好查股與翻譯模式)。直接貼到部落格就能用。
⸻
系列:打造一個好用又有個性的 LINE 金融助理(股價 / 匯率 / 金價 / 彩票 / 人設 / 翻譯)
🎯 今日目標
1.	修復 查股功能:避開 Yahoo API 401 Unauthorized、台股代碼相容(00937b/00937B 等)。
2.	修復 翻譯模式:開啟後,使用者輸入的每句話都直接翻譯(不中斷、不誤判)。
3.	維持 穩定輸出:OpenAI 金鑰失效時,自動改用 Groq,不影響上線。
⸻
🧩 問題現象
•	Yahoo 401:直接打 https://query1.finance.yahoo.com/v7/finance/quote 常被擋,導致即時價失敗。
•	00937b 失敗:台灣 ETF/權證等常見「數字+字母」寫法,yfinance 不認。
•	**翻譯模式壞掉:雖然能切換「翻譯->英文」,但之後的訊息會被一般聊天攔走,沒被翻譯。

🛠️ 解法總覽
1.	改走 HTML 解析:用 YahooStock 直接解析 Yahoo 股市頁面 DOM,拿到名稱/現價/漲跌/時間,避開 401。
2.	代碼正規化:
•	台股大盤/美股大盤:^TWII / ^GSPC。
•	台股:^\d{4,6}[A-Za-z]?$ → 自動補 .TW,英文字尾轉大寫(例:00937b → 00937B.TW)。
•	美股:^[A-Z]{1,5}$ 直接使用(排除 JPY)。
3.	翻譯直通攔截:開啟翻譯模式後,所有訊息都先走翻譯流程;只有結束才回一般聊天。
4.	模型容錯:
•	優先 OpenAI,失敗 → Groq (llama-3.1-8b-instant)。
•	模型名稱改可環境變數覆寫,避免退役爆錯。
⸻
🏗️ 關鍵程式片段
def normalize_stock_input(user_input: str) -> (str, str):
    s = user_input.strip()
    u = s.upper()
    if u in ["台股大盤", "大盤"]:
        return "^TWII", "台灣加權指數"
    if u in ["美股大盤", "美盤", "美股"]:
        return "^GSPC", "S&P 500 指數"
    if u.endswith(".TW") or u.startswith("^"):
        return u, u
# 台股:4~6位數 + 可選 1 位英文字母(大小寫都行)
    if re.fullmatch(r'\d{4,6}[A-Z]?', u):
        symbol = f"{u}.TW"
        base = re.match(r'(\d{4,6}[A-Z]?)', u).group(1)
        name = get_stock_name(base) or base
        return symbol, name
    if re.fullmatch(r'[A-Z]{1,5}', u) and u not in ["JPY"]:
        return u, u
    return u, s
if low.startswith("翻譯->"):
    lang = msg.split("->", 1)[1].strip()
    if lang == "結束":
        translation_states.pop(chat_id, None)
        return reply_with_quick_bar(reply_token, "✅ 已結束翻譯模式")
    translation_states[chat_id] = lang
    return reply_with_quick_bar(reply_token, f"🌐 已開啟翻譯 → {lang},請直接輸入要翻的內容。")
if chat_id in translation_states:
    out = await translate_text(msg, translation_states[chat_id])
    return reply_with_quick_bar(reply_token, f"🌐 ({translation_states[chat_id]})\n{out}")
3) 即時報價改走 YahooStock
newprice_stock = YahooStock(norm_code)              # 解析 Yahoo HTML
price_data     = stock_price(norm_code)             # 走既有日K函式
news_data      = str(stock_news(display_name))[:1024]
def get_analysis_reply(messages):
    try:
        if not openai_client: raise Exception("OpenAI not ready")
        r = openai_client.chat.completions.create(model="gpt-3.5-turbo", messages=messages)
        return r.choices[0].message.content
    except Exception:
        try:
            r = sync_groq_client.chat.completions.create(
                model=os.getenv("GROQ_MODEL_PRIMARY", "llama-3.1-8b-instant"),
                messages=messages, max_tokens=2000, temperature=0.8
            )
            return r.choices[0].message.content
        except Exception:
            r = sync_groq_client.chat.completions.create(
                model=os.getenv("GROQ_MODEL_FALLBACK", "llama-3.1-8b-instant"),
                messages=messages, max_tokens=1500, temperature=1.0
            )
            return r.choices[0].message.content
⸻
✅ 驗收清單
•	2330、2002、00937b/00937B 可查,顯示正確名稱/現價/漲跌/時間。
•	台股大盤 / 美股大盤 正確對應 ^TWII / ^GSPC。
•	NVDA、QQQ 等美股代碼可查。
•	「翻譯->英文」後,任何文字都直接翻譯;「翻譯->結束」恢復聊天。
•	OpenAI 401 時,自動切到 Groq;不中斷服務。
⸻
🧪 測試案例
1.	台股 ETF:輸入 00937b → 內部標準化為 00937B.TW → 正常報價。
2.	大盤:輸入 台股大盤 → 指定 ^TWII → 輸出指數報告(不抓營收/配息)。
3.	翻譯:
•	翻譯->英文 → 你好 → Hello
•	翻譯->日文 → 今天下雨嗎? → 今日は雨が降りますか?
•	翻譯->結束 → 回到一般聊天。
⸻
🗂️ 今天的提交
•	app_fastapi.py:主流程修正(翻譯直通 / 代碼正規化 / 容錯 / 介面維持)。
•	YahooStock.py:沿用 HTML 解析方案(避免 Yahoo 401)。
⸻
🔭 可能可以加入的功能
•	新增 錯誤回饋卡片(顯示錯誤原因 & 自助檢查步驟)。
•	引入 指令別名(/tw 查台股、/us 查美股…),讓群組裡更好用。
•	加上 快取層:短時間重複查詢直接回覆快取,省時省流量。
** 經測試etf的資訊會少一點
