2026年7月19日 星期日

Google Antigravity 學習筆記 : 重構 serverless 函式執行平台 (五)

經過前面的測試已驗證了重構後的程式碼基本上達成了讓平台具備 API Key 功能的目標, 在提交到 GitHub 之前, 想要先佈署到 Mapleboard 上更新 serverless 到具有 API Key 功能的最新版. 關於 Mapleboard 筆記參考 :


本系列全部文章索引參考 :



7. 在 Mapleboard 佈署新版 serverless : 

先用 VNC Cloud 遠端連線到 Mapleboard 桌面, 開啟終端機, 切換到 serverless 專案目錄下, 用 zip 指令把整個專案除指定之敏感檔案外全部壓縮成 zip 檔 : 

tony1966@LX2438:~/flask_apps/serverless$ zip -r serverless_v4.zip . -x "serverless.db" "serverless_error.log" "*.pyc" "__pycache__/*" ".env"

參數 -r 表示要遞迴壓縮, 即連同所有子資料夾 (如 functions/) 一起打包. serverless_backup.zip 是壓縮結果的檔檔名. 後面的 . . 代表壓縮當前目錄下的所有東西, -x 參數後面接的是排除 (不壓縮) 名單, 這裡排除了本地資料庫 (serverless.db), 日誌檔 (serverless_error.log), Python 快取檔, 以及含有敏感密碼的 .env 檔. 

然後用 WinSCP 連線 Mapleboard, 注意, 之前安裝 fail2ban 時為了提升資安, 已修改 SSH 埠 (不再是預設的 22), 埠號可用下列指令查得 :

tony1966@LX2438:~/flask_apps/serverless$ sudo nano /etc/fail2ban/jail.local  

設定 WinSCP 連線時除了要輸入固定 IP 外, 還要更改 SSH 埠號, 如果用 22 埠是無法連線的. 連線成功後, 先將上面備份的 zip 檔傳送至本地保存. 完成後將本地的 serverless.py 主程式與 functions 資料夾上傳到 Mapleboard 的 serverless 專案目錄下覆蓋舊版程式檔. 

由於主程式 serverless.py 有更改, 所以須用下例指令重啟服務才會運行新版程式 :

tony1966@LX2438:~/flask_apps/serverless$ sudo systemctl restart serverless

用瀏覽器測試 hello.py 函式 : 





在有登入情況下, 對函式的請求無需攜帶 API Key 即可順利執行;  如果沒有登入就會收到 401 錯誤, 例如 :



2026年7月18日 星期六

Google Antigravity 學習筆記 : 重構 serverless 函式執行平台 (四)

前面的測試已驗證了重構後的程式碼基本上達成了讓平台具備 API Key 功能的目標, 本篇要來測試本地自動化佈署功能, 在此過程中發現了原系統的一些缺陷, 都利用 agy 請 Claude Sonnet 修正, 除了主程式 serverless.py 外, 還包括 functions 下的 update_function.py 與 save_function.py. 

本系列全部文章索引參考 :



6. 本地自動化佈署 : 

第一版的 serverless 系統在新增或修改函式時都需登入平台, 透過手動操作網頁來完成, 在本次重構加上 API Key 機制後, 系統安全性增強, 可以在本地撰寫一個自動化佈署程式, 攜帶 API Key 經過驗證完成函式的佈署. 只要寫一個 Python 函式讀取要佈署的函式檔, 然後攜帶 API key 呼叫 update_function 或 save_function 即可完成函式的佈署. 


(1). 與 Claude 討論 :

> 我執行下列佈署程式 :
  # deploy.py
  import os
  import requests

  # 1. 設定伺服器資訊與 API Token
  # 地端測試可用 http://127.0.0.1:5000,上雲端後改成你的 Render 網址
  SERVER_URL="http://127.0.0.1:5000"
  API_TOKEN="new-test-token"

  # 2. 定義要更新的函式名稱與本地檔案路徑
  TARGET_FUNCTION_NAME="hello"
  LOCAL_FILE_PATH="hello.py"

  def deploy_function():
      # 檢查本地檔案是否存在
      if not os.path.exists(LOCAL_FILE_PATH):
          print(f"❌ 找不到本地檔案: {LOCAL_FILE_PATH}")
          return
      # 讀取本地最新的程式碼內容
      with open(LOCAL_FILE_PATH, "r", encoding="utf-8") as f:
          new_code=f.read()
      # 3. 準備發送給 update_function 的端點與資料
      # 路由設計是動態載入的 /function/update_function
      url=f"{SERVER_URL}/function/update_function"
      headers={
          "X-API-Key": API_TOKEN,
          "Content-Type": "application/json"
          }
      payload={
          "func_name": TARGET_FUNCTION_NAME,
          "code": new_code
          }

      print(f"🚀 正在將 {LOCAL_FILE_PATH} 部署至 {SERVER_URL}...")
      try:
          # 發送 POST 請求更新遠端函式
          response=requests.post(url, json=payload, headers=headers)
          # 檢查伺服器回應狀態
          if response.status_code == 200:
              print("✅ 函式更新成功!")
              print("伺服器回應:", response.json())
          else:
              print(f"❌ 更新失敗 (狀態碼: {response.status_code})")
              try:
                  print("錯誤原因:", response.json())
              except ValueError:
                  print("非 JSON 回應內容:", response.text)
      except requests.exceptions.RequestException as e:
          print(f"💥 連線發生異常: {e}")

      deploy_function()
  ===
  出現如下錯誤 :
  D:\python\test>python deploy.py
  🚀 正在將 hello.py 部署至 http://127.0.0.1:5000...
  ❌ 更新失敗 (狀態碼: 401)
  錯誤原因: {'error': 'Authentication required', 'login_url': '/login'}
  ===
  在提供有效 API Key 情況下應該要能通過不受 login 阻攔, 請修改

▸ Thought for 29s
  The user is experiencing a 401 error with the message "Authentication required" ...
  問題很清楚。看一下目前的驗證順序:

● Read(D:/antigravity_cli/projects/serverless/serverless.py) (ctrl+o to expand)

  根本原因找到了:

  步驟 1(L165)先攔截, update_function  在  PROTECTED_FUNCTIONS  裡,API Client 沒有 Session →
  直接 401,根本輪不到步驟 2 的 Token 驗證。

  解法:將兩層驗證合併,一次判斷 Session 或 Token 擇一通過,同時可以移除  TOKEN_PROTECTED_FUNCTIONS
  (已不需要):

● Read(D:/antigravity_cli/projects/serverless/serverless.py)
  if __name__ == "__main__":
● Edit(D:/antigravity_cli/projects/serverless/serverless.py) (ctrl+o to expand)

  完成。來驗證最終結果:

● Read(D:/antigravity_cli/projects/serverless/serverless.py) (ctrl+o to expand)

  還有一個小問題:L184 的步驟編號  # 3.  與 L182 的  # 3.  重複了,順手修正:
● Edit(D:/antigravity_cli/projects/serverless/serverless.py) (ctrl+o to expand)

  完成。以下是這次修改的完整說明:
  ──────
  ## 問題根源

    原本流程(有 Bug):
      步驟 1:update_function 在 PROTECTED_FUNCTIONS? → 是
               check_auth() = False(API Client 無 Session)
               → 直接 401 ❌ (Token 驗證根本輪不到)

  ## 修正後的流程
    新流程(handle_function 前三步):

    步驟 1:一次性完成雙重驗證
            is_session_auth = check_auth()
            if 無 Session → 嘗試讀 X-API-Key → 查資料庫 → is_token_auth
            is_authorized = is_session_auth OR is_token_auth

    步驟 2:管理模組 → 需要 is_authorized,否則 401
    步驟 3:一般函式 → 需要 is_authorized,否則 401

  ## 各情境結果

   呼叫者                      | Session              | API Key              | 結果
  -----------------------------|----------------------|----------------------|----------------------
   瀏覽器管理者(update/save) | ✅                   | ❌                   | ✅ 放行
    deploy.py  API Client      | ❌                   | ✅ 有效              | ✅ 放行
   未認證請求                  | ❌                   | ❌                   | ❌ 401
   無效 Token                  | ❌                   | ❌ 無效              | ❌ 401
────────────────────────────────────────────────────────────
> 成功了, 但出現連線異常:
  D:\python\test>python deploy.py
  🚀 正在將 hello.py 部署至 http://127.0.0.1:5000...
  ✅ 函式更新成功!
  💥 連線發生異常: Expecting value: line 1 column 1 (char 0)
  這要修改 deploy.py 還是系統的 update_function.py 較好?
▸ Thought for 27s
  The user is seeing a successful update (✅) but then a connection error. Let me...
  先看清楚錯誤發生在哪一行:

   deploy.py  收到 200 → 印出 ✅ → 再執行  response.json()  時爆炸,代表  update_function.py
  回傳的是 HTML 而非 JSON。

  讓我確認  update_function.py  的完整內容:

● Read(D:/antigravity_cli/projects/serverless/functions/update_function.py) (ctrl+o to expand)

  問題找到了,而且有兩個 bug 同時存在:

   # | 問題         | 說明
  ---|--------------|-------------------------------------------------------------------------------
   1 | 欄位名稱不符 |  deploy.py  送  func_name ,但  update_function.py  讀  module_name  →
     |              | 讀到空字串
   2 | 輸入格式不符 |  deploy.py  送 JSON body,但  update_function.py  只讀  request.form  →
     |              | 永遠讀不到資料
   3 | 回應格式不符 | 成功時回傳 HTML, deploy.py  呼叫  response.json()  爆炸
  ──────
  ## 修哪個?

  修  update_function.py ,理由:

  • 這是 server 端的問題(不認識 JSON 輸入、不回傳 JSON)
  • 瀏覽器用表單呼叫、API Client 用 JSON 呼叫,server 應該兩者都支援
  •  deploy.py  的設計是正確的,不需要改
● Edit(D:/antigravity_cli/projects/serverless/functions/update_function.py)
● Create(D:/antigravity_cli/projects/serverless/functions/update_function.py) (ctrl+o to expand)

  完成。以下是修改摘要:
  ──────
  ## 修改的檔案

  ### update_function.py

  修正了三個問題:

   # | 問題                                 | 修正方式
  ---|--------------------------------------|-------------------------------------------------------
   1 | 只讀  request.form ,不懂 JSON       | 用  request.is_json  判斷,JSON 時改讀
     |                                      | request.get_json()
   2 | 只認  module_name ,不認  func_name  | JSON 輸入時同時嘗試讀取  func_name  與  module_name
   3 | 成功時只回傳 HTML                    | JSON 請求時回傳  {"message": "...", "func_name":
     |                                      | "..."}
  ──────
  ## 預期執行結果
    D:\python\test>python deploy.py
    🚀 正在將 hello.py 部署至 http://127.0.0.1:5000...
    ✅ 函式更新成功!
    伺服器回應: {'func_name': 'hello', 'message': '模組 hello 已成功更新'}


────────────────────────────────────────────────────────────
> update_function 是更新已有的函式, 如果要新增函式則是使用 add_function 與 save_function,
  如果要佈署新函式, deploy.py 要怎麼改? 同時請分析 add_function.py 與 save_function.py 在執行 deploy.p
  佈署方面是否有需要配合修改之處?
▸ Thought for 1s
  The user wants to understand the deployment flow for new functions. Let me first...
  先讀取三個相關檔案:

● Read(D:/antigravity_cli/projects/serverless/functions/add_function.py)
● Read(D:/antigravity_cli/projects/serverless/functions/save_function.py) (ctrl+o to expand)
  分析完畢。以下是完整報告:
  ──────
  ## 架構釐清

    add_function  → 純 UI 頁面,回傳 HTML 表單,action 指向 save_function
    save_function → 實際寫檔(新增),只接受 form POST
    update_function → 實際寫檔(覆蓋),已修改支援 JSON

  deploy.py 要新增函式,只需呼叫  save_function ,不需碰  add_function (那是給瀏覽器用的 UI)。
  ──────
  ## save_function.py 的問題(與 update_function 一樣)

   #                           | 問題                                 | 影響
  -----------------------------|--------------------------------------|-----------------------------
   1                           | 只讀  request.form ,不懂 JSON       | API Client 送來的資料讀不到
   2                           | 只認  module_name ,不認  func_name  | deploy.py 欄位名稱不符
   3                           | 成功/失敗都回傳 HTML                 |  response.json()  會爆炸
  ──────
  ## 修改計劃

  修改  save_function.py :與  update_function.py  相同邏輯,支援 JSON/form 雙輸入。

  修改  deploy.py :加入「新增 or 更新」判斷邏輯,自動選擇呼叫  save_function  或  update_function
  。

  開始修改:
● Create(D:/antigravity_cli/projects/serverless/functions/save_function.py) (ctrl+o to expand)

  現在修改  deploy.py ,加入自動判斷新增或更新:
● Create(D:/python/test/deploy.py) (ctrl+o to expand)

  完成。以下是完整摘要:
  ──────
  ## 修改了哪些檔案

   檔案                           | 修改內容
  --------------------------------|-----------------------------------------------------------------
   save_function.py               | 支援 JSON/form 雙輸入、相容  func_name / module_name 、回傳
                                  | JSON(201)或 HTML
   deploy.py                      | 新增  function_exists()  探測,自動選  save_function  或
                                  | update_function
    functions/update_function.py  | 上一輪已修改,本次不動
    add_function.py               | 不需修改,它只是瀏覽器 UI 頁面
  ──────
  ## deploy.py 新流程

    執行 deploy.py
        ↓
    function_exists():GET /function/{func_name}
        ├─ 404 → 函式不存在 → POST /function/save_function  → 回 201 ✅
        └─ 其他 → 函式已存在 → POST /function/update_function → 回 200 ✅
    ──────
  ## 預期執行結果

    # 第一次佈署(函式不存在)
    🚀 正在將 hello.py 新增至 http://127.0.0.1:5000...
    ✅ 函式新增成功!
    伺服器回應: {'func_name': 'hello', 'message': '模組 hello 已成功建立'}

    # 第二次佈署(函式已存在)
    🚀 正在將 hello.py 更新至 http://127.0.0.1:5000...
    ✅ 函式更新成功!
    伺服器回應: {'func_name': 'hello', 'message': '模組 hello 已成功更新'}


(2). 本地佈署測試 :

上面程序中 Claude 已經修改 serverless.py, update_function.py, 以及 save_function.py, 且修正了原先使用的佈署程式 deploy.py. 接下來要實測看看是否可在本機用佈署程式 deploy.py 將 hello.py 發布到後端平台上. 

先複習一下 serverless 平台的函式檔基本架構 :

# func_module.py
def main(request, **kwargs):
    config=kwargs.get('config', {})  # 預設為空 dict
    result='do something'
    return result

注意, main() 的參數一定要傳入 request 與關鍵字參數 **kwargs : 
  • request :
    用來接收 HTTP 請求的相關資訊 (例如前端傳進來的 Payload, Query Parameters, Headers等).
  • **kwargs 與 config :
    這是一個極具擴充性的設計, 透過 kwargs.get('config', {}) 可以在伺服器端 (Flask 主程式) 執行該函式時動態注入系統級的配置或環境變數 (例如資料庫連線字串, 加密金鑰等), 而不需要把這些敏感資訊寫死在 hello.py 裡面. 
為了展示此架構中 main() 的兩個參數用途, 先開啟 .env 檔, 添加一個環境變數 DB_TIMEOUT :

DB_TIMEOUT=30

然後按照上面的函式架構寫了一個新的 hello.py 函式檔 (其實只是加上讀取環境變數 DB_TIMEOUT 而已) :

# hello.py
def main(request, **kwargs):
    # 讀取系統注入的設定(如果有的話)
    config=kwargs.get('config', {})    
    # 這裡可以安全地取得環境變數
    db_timeout=config.get('DB_TIMEOUT', 30)
    # 先從請求字串擷取 name 參數 (?name=)
    name=request.args.get('name')
    if not name:   # 請求字串沒有 name 參數
        # 嘗試從 RESTful 子路徑 (例如 /function/hello/Tony) 中擷取 name 參數 
        subpath=request.view_args.get('subpath', '')   # 取得 Flask 傳入的 subpath 參數
        parts=subpath.strip('/').split('/')   # 拆解成串列例如 ['Tony']
        if parts:
            name=parts[0]   # 取第一段作為 name (例如 'Tony')
    # 從 request 也沒有找到
    if not name:
        name='World'    
    result=f"Hello {name}! Database timeout is set to {db_timeout}s."
    return result

然後用下列 Claude 修正過的 deploy.py 程式來佈署此新的 hello.py (注意 API_TOKEN 必須填入有效的 API Key) :

# deploy.py
import os
import requests

# 1. 設定伺服器資訊與 API Token
# 地端測試可用 http://127.0.0.1:5000,上雲端後改成你的 Render 網址
SERVER_URL="http://127.0.0.1:5000"
API_TOKEN="new-test-token"

# 2. 定義要佈署的函式名稱與本地檔案路徑
TARGET_FUNCTION_NAME="hello"
LOCAL_FILE_PATH="hello.py"

def function_exists(func_name):
    """檢查遠端伺服器上的函式是否已存在(透過 GET 探測)"""
    url=f"{SERVER_URL}/function/{func_name}"
    headers={"X-API-Key": API_TOKEN}
    try:
        response=requests.get(url, headers=headers)
        # 404 表示不存在,其他狀態(200/400/500)表示檔案存在
        return response.status_code != 404
    except requests.exceptions.RequestException:
        return False  # 連線失敗時保守假設不存在

def deploy_function():
    # 檢查本地檔案是否存在
    if not os.path.exists(LOCAL_FILE_PATH):
        print(f"❌ 找不到本地檔案: {LOCAL_FILE_PATH}")
        return
    # 讀取本地最新的程式碼內容
    with open(LOCAL_FILE_PATH, "r", encoding="utf-8") as f:
        new_code=f.read()

    headers={
        "X-API-Key": API_TOKEN,
        "Content-Type": "application/json"
    }
    payload={
        "func_name": TARGET_FUNCTION_NAME,
        "code": new_code
    }

    # 3. 自動判斷:函式已存在 → update,不存在 → save(新增)
    if function_exists(TARGET_FUNCTION_NAME):
        url=f"{SERVER_URL}/function/update_function"
        action="更新"
    else:
        url=f"{SERVER_URL}/function/save_function"
        action="新增"

    print(f"🚀 正在將 {LOCAL_FILE_PATH} {action}至 {SERVER_URL}...")
    try:
        response=requests.post(url, json=payload, headers=headers)
        # 新增成功是 201,更新成功是 200
        if response.status_code in (200, 201):
            print(f"✅ 函式{action}成功!")
            print("伺服器回應:", response.json())
        else:
            print(f"❌ {action}失敗 (狀態碼: {response.status_code})")
            try:
                print("錯誤原因:", response.json())
            except ValueError:
                print("非 JSON 回應內容:", response.text)
    except requests.exceptions.RequestException as e:
        print(f"💥 連線發生異常: {e}")

if __name__ == "__main__":
    deploy_function()

可見 Claude 修改後的 deploy.py 是透過讀取 functions 資料夾下有無該函式來判斷是要新增 (無) 還是更新 (有), 目前 functions 下有 hello.py, 所以執行 

執行結果 : 

D:\python\test>python deploy.py   
🚀 正在將 hello.py 部署至 http://127.0.0.1:5000...
✅ 函式更新成功!
伺服器回應: {'func_name': 'hello', 'message': '模組 hello 已成功更新'}

接下來驗證此新佈署的 hello.py 函式功能是否正確 :






可見不同的參數傳遞方式功能都正確, 且 .env 中的環境變數 DB_TIMEOUT 也正確讀到了. 接下來測試 save_function 是否能順利佈署新函式, 先把 functions 底下的 hello.py 刪除, 然後再執行一次 deploy.py : 

D:\python\test>python deploy.py   
🚀 正在將 hello.py 新增至 http://127.0.0.1:5000...
✅ 函式新增成功!
伺服器回應: {'func_name': 'hello', 'message': '模組 hello 已成功建立'}

檢視 functions 下果然 hello.py 已被建立, 再次執行網頁測試結果相同. 

Google Antigravity 學習筆記 : 重構 serverless 函式執行平台 (三)

在前一篇測試中利用 Antigravity CLI (agy) 完成 serverless 函式執行平台的重構, 為其加上 API Key 認證機制, 所有請求必須在標頭中攜帶有效的 X-API-Key 屬性值, 否則請求將被拒絕, 使用 curl.exe 驗證新版 serverless 功能確實符合要求, 本篇則要改用 requests 套件來測試, 這是在實際應用中最常見用法. 

本系列全部文章索引參考 :



5. 使用 requests 提出請求 : 

使用 requests 套件發送請求時, 要將 API Key 放進 headers 參數中的 X-API-Key 欄位傳送給伺服器, 請求的 HTTP 方法可以使用 GET 或 POST, 分別呼叫 requests.get() 與 requests.post() 函式. 

注意, 在前一篇測試中我們已將 API Key 更新如下 :




以下測試將使用  new-test-token 這個 API Key.


(1). 使用 GET 方法 : 

GET 方法不帶 Body, 只需要單純將 API Key 放入 headers 中的 X-API-Key 欄位, 然後在呼叫 requests.get() 時傳入 headers 參數即可, 例如 : 

# serverless_api_key_get_ok.py
import requests

url="http://localhost:5000/function/hello"
headers={"X-API-Key": "new-test-token"}
response=requests.get(url, headers=headers)
if response.status_code == 200:
    print("回應:", response.text)
else:
    print(f"錯誤 {response.status_code}:", response.text)

此處因為 hello.py 傳回值是純文字, 所以用 resposne.text 而非 response.json() 取得回應值, 執行結果如下, 因為傳送了有效的 API Key, 所以伺服器回應 200 OK :

>>> %Run serverless_api_key_get_ok.py   
回應: Hello World!

如果沒有傳送有效的 API Key, 請求會被拒絕而得到 401 回應, 例如 :

# serverless_api_key_get_ng.py
import requests

url="http://localhost:5000/function/hello"
response=requests.get(url)
if response.status_code == 200:
    print("回應:", response.text)
else:
    print(f"錯誤 {response.status_code}:", response.text)

執行結果如下 :

>>> %Run serverless_api_key_get_ng.py  
錯誤 401: {
  "error": "Missing API token",
  "hint": "Provide X-API-Key header"
}


(2). 使用 POST 方法 : 

呼叫 requests.post() 時傳入有效的 API Key 就可順利取得 200 OK 回應, 例如 :

# serverless_api_key_post_ok.py
import requests

url="http://localhost:5000/function/hello"
headers={"X-API-Key": "new-test-token"}
response=requests.post(url, headers=headers)
if response.status_code == 200:
    try:
        # 先嘗試用 JSON 解析
        print("回應 (JSON):", response.json())
    except requests.exceptions.JSONDecodeError:
        # 如果不是 JSON 就印出純文字
        print("回應 (純文字):", response.text)
else:
    print(f"錯誤 {response.status_code}:", response.text)

此例使用 try catch 來判斷回應物件型態是 JSON 格式字串還是純文字, 前者呼叫 response.json() 取得回應內容, 後者則用 response.text, 執行結果如下 :

>>> %Run serverless_api_key_post_ok.py  
回應 (純文字): Hello World!

如果呼叫 requests.post() 時沒有傳入有效的 API Key 就會收到 401 錯誤, 例如 :

# serverless_api_key_post_ng.py
import requests

url="http://localhost:5000/function/hello"
response=requests.post(url)
if response.status_code == 200:
    try:
        # 先嘗試用 JSON 解析
        print("回應 (JSON):", response.json())
    except requests.exceptions.JSONDecodeError:
        # 如果不是 JSON 就印出純文字
        print("回應 (純文字):", response.text)
else:
    print(f"錯誤 {response.status_code}:", response.text)

執行結果如下 :

>> %Run serverless_api_key_post_ng.py   
錯誤 401: {
  "error": "Missing API token",
  "hint": "Provide X-API-Key header"
}

以上測試說明重構後的 serverless 平台確實已改為 API Key 版本, 任何請求必須攜帶 API Key 才可能取得 200 OK 回應, 否則請求都會被拒絕, 收到 401 錯誤. 


6. 主程式原始碼異動摘要 : 

經過上面測試, 我們確認了此重購專案已達成了預期目標, 以下做個 Code Review, 將主程式 serverless.py 的異動摘要如下 :

首先是在 init_db() 函式中添加了一個儲存 API Key 的認證資料表 api_tokens :

    # API Token 認證表
    cursor.execute("""
        CREATE TABLE IF NOT EXISTS api_tokens (
            id         INTEGER PRIMARY KEY AUTOINCREMENT,
            token      TEXT    NOT NULL UNIQUE,
            owner      TEXT    NOT NULL,
            is_active  INTEGER NOT NULL DEFAULT 1,
            created_at TEXT    NOT NULL DEFAULT (datetime('now'))
        )
    """)

其次是添加了一個 TOKEN_PROTECTED_FUNCTIONS 清單 : 

# 需要 API Token 驗證的函式列表(儲存/更新 function 及所有一般函式執行)
TOKEN_PROTECTED_FUNCTIONS=['save_function', 'update_function']

這兩個系統函式 save_function 與 update_function 用來將函式內容寫進檔案中, 屬於最高權限的敏感操作, 必須通過 API Key 驗證. 

其三是新增了一個 require_api_token() 函式來檢查請求標頭中是否有攜帶有效之 API Key, 有就傳回所呼叫的函式, 否則傳回 401 錯誤 : 

def require_api_token(f):  # 裝飾器:驗證請求 Header 中的 X-API-Key
    @wraps(f)
    def decorated(*args, **kwargs):
        token=request.headers.get('X-API-Key')
        if not token:  # Header 遺失
            return jsonify({'error': 'Missing API token', 'hint': 'Provide X-API-Key header'}), 401
        conn=sqlite3.connect(DB_PATH)
        cursor=conn.cursor()
        cursor.execute(
            'SELECT id FROM api_tokens WHERE token=? AND is_active=1',
            (token,)
        )
        row=cursor.fetchone()
        conn.close()
        if not row:  # Token 不存在或已停用
            return jsonify({'error': 'Invalid or inactive API token'}), 401
        return f(*args, **kwargs)
    return decorated

最後是在 handle_function() 函式中添加雙軌互補驗證 (Session 或 Token 擇一通過) 機制 :

    # 2. API Token 驗證:已登入管理者直接放行;否則需提供有效 Token
    #    觸發範圍:TOKEN_PROTECTED_FUNCTIONS(save/update)及所有一般函式
    if func_name in TOKEN_PROTECTED_FUNCTIONS or func_name not in PROTECTED_FUNCTIONS:
        if not check_auth():  # 管理者已登入 → 信任,跳過 Token 驗證
            token=request.headers.get('X-API-Key')
            if not token:
                return jsonify({'error': 'Missing API token', 'hint': 'Provide X-API-Key header'}), 401
            conn=sqlite3.connect(DB_PATH)
            cursor=conn.cursor()
            cursor.execute(
                'SELECT id FROM api_tokens WHERE token=? AND is_active=1',
                (token,)
            )
            row=cursor.fetchone()
            conn.close()
            if not row:
                return jsonify({'error': 'Invalid or inactive API token'}), 401

這段程式碼採用了巢狀條件設計, 提供了兩道過濾網 :
  • 第一層 (判定範圍) :只要呼叫的函式是敏感的系統功能 (save/update_function) 或任何使用者自訂函式 (不在白名單內) 就會觸發此區塊.
  • 第二層 (身份切換) :
    • 情境 A (管理員) :
      瀏覽器帶著 Cookie 請求, check_auth() 回傳 True. 此時 if not check_auth() 判定不成立, 直接跳過整個 Token 檢查與資料庫查詢, 此安排讓管理員登入後線上操作函式更新與存檔不因為沒帶 API Key 而被 401 擋掉. 
    • 情境 B (外部 API 呼叫/排程腳本) :
      本機 Python 腳本或 curl 請求沒有瀏覽器 Session, 呼叫 check_auth() 會回傳 False, 程式進入 if 內部強制執行後續的 X-API-Key 擷取與 SQLite 比對. 
if not check_auth() 是檢查使用者是否有登入, 沒有登入就要接受 API Key 查驗, 檢查 HTTP 請求的 Header 有沒有帶 X-API-Key 欄位, 沒有帶就直接拒絕請求, 回傳 401 Unauthorized 狀態碼. 如果已登入, 則程式會直接跳過整個 if not check_auth(): 內部的所有程式碼, 直接放行去執行該函式. 

注意此處使用了 SQLite 的參數化查詢 token=? 並把變數包在元組 (token,) 裡傳進去, 這樣可以徹底杜絕駭客在 Header 亂寫 SQL 語法, 利用 SQL Injection 來破解驗證. 如果資料庫回傳 None, 表示 API Key 不存在, 或存在但被停用 (is_active 不是 1), 那就回傳 401 錯誤拒絕請求; 反之就是通過了驗證, 准許呼叫此函式, 放行讓後續的業務邏輯繼續執行. 

Google Antigravity 學習筆記 : 重構 serverless 函式執行平台 (二)

在前一篇測試中, 我已將 serverless 第一版原始碼從 GitHub 上複製到本機 Antigravity CLI 的專案目錄下, 並做好前置處理, 包含將原專案打上標籤, 建立新分支, 建立隔離資料夾並移動應用 App, 最後提交 Git 紀錄. 本篇要用 agy 來進行重構, 為 serverless 平台加上 API Token 認證機制. 

本系列全部文章索引參考 :



2. 用 agy 重構專案 : 

切換到專案目錄下後輸入 agy 啟動 Antigravity CLI 介面, 這次仍然使用 Claude Sonnet 模型來完成此重構計劃. 輸入下列提示詞 :

我們正在 feature/api-token-auth 分支上,要為這個 serverless 專案加上 API Token 認證機制。請協助修改 serverless.py,完成以下任務:

1. 初始化資料庫:
   在系統啟動時(或初始化 DB 的地方),自動建立 `api_tokens` 資料表(欄位包含:id, token, owner, is_active, created_at)。

2. 實作驗證裝飾器(Decorator):
   撰寫一個 `@require_api_token` 裝飾器,從請求的 Header 中讀取 `X-API-Key`。
   - 若 Header 遺失或 Token 不存在於資料庫中(且 is_active 必須為 1),回傳 401 Unauthorized。

3. 保護 API 路由:
   將這個裝飾器套用到「儲存/更新 function」以及「執行 function」的 Flask Route 上。

請先分析原始專案的架構, 提出你的重構計劃, 讓我看過後再修改程式碼。




Claude 閱讀原專案後規劃結果如下 :

> 我們正在 feature/api-token-auth 分支上,要為這個 serverless 專案加上 API Token 認證機制。
  請協助修改 serverless.py,完成以下任務:

  1. 初始化資料庫:
     在系統啟動時(或初始化 DB 的地方),自動建立 `api_tokens` 資料表(欄位包含:id, token, owner,
  is_active, created_at)。

  2. 實作驗證裝飾器(Decorator):
     撰寫一個 `@require_api_token` 裝飾器,從請求的 Header 中讀取 `X-API-Key`。
     - 若 Header 遺失或 Token 不存在於資料庫中(且 is_active 必須為 1),回傳 401 Unauthorized。

  3. 保護 API 路由:
     將這個裝飾器套用到「儲存/更新 function」以及「執行 function」的 Flask Route 上。

  請先分析原始專案的架構, 提出你的重構計劃, 讓我看過後再修改程式碼。
▸ Thought for 2s
  Let me first explore the project structure to understand the codebase before mak...

● ListDir(D:/antigravity_cli/projects/serverless)
● Read(D:/antigravity_cli/projects/serverless/serverless.py)
● ListDir(D:/antigravity_cli/projects/serverless/functions)
● Read(D:/antigravity_cli/projects/serverless/requirements.txt)
● Read(D:/antigravity_cli/projects/serverless/functions/save_function.py)
● Read(D:/antigravity_cli/projects/serverless/functions/update_function.py) (ctrl+o to expand)

  已完整閱讀專案架構,以下是我的分析與重構計劃。
  ──────

● Create(C:/Users/tony1/.gemini/antigravity-...-a43e-cfa6fbc204e5/refactor_plan.md) (ctrl+o to expand)

  以下是重構計劃的摘要:

  ## 架構分析結論

  現有系統有兩層認證:

  1. Session Cookie — 管理者透過  /login  密碼登入,保護  PROTECTED_FUNCTIONS  清單
  2. 一般函式執行 — 完全沒有保護,任何人只要知道 URL 就能呼叫
  ──────
  ## 三個關鍵設計決策

  ### 1.  init_db()  修正

  移除  if not os.path.exists(DB_PATH)  判斷,改用  CREATE TABLE IF NOT EXISTS
  ,確保每次啟動都檢查表是否存在,不會因 DB 已建立但缺表而漏掉建立  api_tokens 。

  ### 2.  @require_api_token  裝飾器

  讀取  X-API-Key  Header → 查詢  api_tokens  表( is_active=1 )→ 不符合一律回 401 JSON。

  ### 3. 保護路由的方案

  因為  save_function 、 update_function  與一般函式執行共用同一個  handle_function()
  路由,無法直接套裝飾器。

  採用方案 A:在路由內部邏輯中呼叫驗證輔助函式,改動最小且與現有 Session 機制並存。
  ──────
  計劃文件已整理完畢,請確認後按「Proceed」或告訴我需要調整的地方,即可開始撰寫程式碼。

此計劃看來很周到, 輸入 Proceed 按 Enter 看它表演即可 :

> Proceed

▸ Thought for 1s
  The user wants to proceed with the refactoring plan. Let me implement the change...
  好的,開始實作!

● Read(D:/antigravity_cli/projects/serverless/serverless.py)
● Edit(D:/antigravity_cli/projects/serverless/serverless.py) (ctrl+o to expand)

  修改成功,來驗證最終結果:

● Read(D:/antigravity_cli/projects/serverless/serverless.py) (ctrl+o to expand)

  內容正確。最後修正一個步驟編號的小問題(step 2 重複出現):
● Edit(D:/antigravity_cli/projects/serverless/serverless.py) (ctrl+o to expand)

  所有修改完成,以下是完整摘要:
  ──────
  ## ✅ 修改完成摘要
  檔案:serverless.py

  ### 變更一覽

   #             | 位置          | 修改內容
  ---------------|---------------|------------------------------------------------------------------
   1             | L6            | 新增  from functools import wraps
   2             | L16–37        |  init_db()  改為冪等執行,新增  api_tokens  資料表
   3             | L56–73        | 新增  require_api_token(f)  裝飾器(備用,可掛在未來獨立路由上)
   4             | L105–106      | 新增  TOKEN_PROTECTED_FUNCTIONS  常數
   5             | L167–181      |  handle_function()  步驟 2 加入 API Token 驗證邏輯
   6             | L207–208      | 移除  __main__  區塊重複的  init_db()  呼叫

  ### 認證邏輯流程(修改後)

    /function/<func_name>
        │
        ├─ func_name 在 PROTECTED_FUNCTIONS?
        │   └─ 未登入 Session → 401 (Authentication required)
        │
        ├─ func_name 在 TOKEN_PROTECTED_FUNCTIONS(save/update)
        │  OR func_name 不在 PROTECTED_FUNCTIONS(一般函式)?
        │   ├─ 無 X-API-Key Header → 401 (Missing API token)
        │   └─ Token 無效或 is_active=0 → 401 (Invalid or inactive API token)
        │
        └─ 驗證通過 → 動態載入並執行函式

  ### 快速測試方式

    # 插入測試 Token
    sqlite3 serverless.db "INSERT INTO api_tokens (token, owner) VALUES ('test-token-123',
  'developer');"

    # ✅ 正確呼叫
    curl http://localhost:5000/function/hello -H "X-API-Key: test-token-123"

    # ❌ 無 Token → 401
    curl http://localhost:5000/function/hello

不到三分鐘就完成重構了, 這若是照以前人工來改程式碼, 恐怕得花上一周或更久. 我叫 Claude 說明它改了哪些地方 :

> 解釋你修改了哪些檔案? 哪個部分(函式)?  

▸ Thought Process
  The user wants a clear explanation of which files and which functions were modif...
  這次只修改了 1 個檔案,共改動 4 個區塊:
  ──────
  ## 修改的檔案
  ### serverless.py
  ──────
  ### 1. Import 區塊(L1–8)

    + from functools import wraps

  新增  wraps ,讓裝飾器能保留被包裝函式的原始名稱與文件字串。
  ──────
  ### 2.  init_db()  函式(L16–37)
  改動前:用  if not os.path.exists(DB_PATH)  判斷,只有 DB 不存在時才建表,且只建  call_stats
  一張表。

  改動後:
  • 移除  os.path.exists  條件判斷,改為每次啟動都執行( CREATE TABLE IF NOT EXISTS
  本身就是冪等的,不會重複建表)
  • 新增  api_tokens  資料表,欄位為  id, token, owner, is_active, created_at
  ──────
  ### 3. 新增  require_api_token()  裝飾器(L56–73)

  全新函式,位於  record_call()  之後、 app = Flask(...)  之前。

  功能:從 Request Header 讀取  X-API-Key ,查詢資料庫確認 Token 存在且  is_active=1 ,否則回傳  401
  。

  │ 此裝飾器目前定義為備用,供未來掛在獨立路由上使用。
  ──────
  ### 4.  handle_function()  路由函式(L163–181)

  在原有的 Session 驗證之後,插入第二層 API Token 驗證邏輯(步驟 2):

    觸發條件(滿足其一即驗證):
      A. func_name 在 TOKEN_PROTECTED_FUNCTIONS → save_function、update_function
      B. func_name 不在 PROTECTED_FUNCTIONS → 所有一般使用者自訂函式

  兩種情況都會檢查  X-API-Key  Header,並查詢  api_tokens  表,不符合就回傳  401 。
  ──────
  ### 未修改的部分

   未動到的檔案/函式                               | 原因
  -------------------------------------------------|------------------------------------------------
    functions/  目錄下所有模組                     | 驗證邏輯集中在  serverless.py ,子模組不需變動
    check_auth()                                   | Session 登入機制保持原樣
    record_call()                                  | 呼叫統計邏輯不變
    /login 、 /logout  路由                        | 管理者登入流程不變

這個說明後續可以開啟主程式 serverless.py 來檢視. 


3. 功能測試 : 

上面 agy 已完成重構, 接下來實地測試 Claud 實作的 API Token 是否能正常運作. 在進行測試之前, 須在專案目錄下建立一個純文字檔 .env, 紀錄管理者的 token 與 key, 前者是自己容易記的密碼, 後者是用工具函式產生的一長串金鑰, 可以用 Python 內建的 secrets 模組來產生, 參考 :


在專案目錄下用記事本編輯一個 .env 檔, 輸入如下內容 (範例) :

SECRET_TOKEN=admin123
SECRET_KEY=fKFQPound41lv8FrWzU-lmFywxAT47uMwfvmBbiuLTE

其中的 SECRET_KEY 可在 Thonny 的 Python 互動環境匯入 secrets 模組後呼叫 secrets.token_urlsafe(32) 函式產生, 例如 :

>>> import secrets  
>>> secrets.token_urlsafe(32)  
'fKFQPound41lv8FrWzU-lmFywxAT47uMwfvmBbiuLTE'

由於我主要的測試環境主要是使用 Thonny 自帶的 Python, 所有 requirements.txt 中的套件在此開發環境都有安裝 (此專案主要是用到 Flask), 所以按 Thonny 的 "工具/開啟系統終端機" 開啟命令提示字元視窗, 切換到專案目錄下, 用下列指令啟動 Flask 開發伺服器 :

D:\antigravity_cli\projects\serverless>python serverless.py  
 * Serving Flask app 'serverless'
 * Debug mode: on

伺服器運行起來後, serverless.py 就會建立一個 SQlite 資料庫 serverless.db, 執行下列指令在此系統資料庫裡的 api_tokens 資料表填入測試用的權杖 test-token-123 : 

PS D:\antigravity_cli\projects\serverless> python -c "import sqlite3; conn = sqlite3.connect('serverless.db'); conn.execute('INSERT INTO api_tokens (token, owner, is_active) VALUES (?, ?, ?)', ('test-token-123', 'developer', 1)); conn.commit(); conn.close(); print('🎉 測試 Token 寫入成功!')"
🎉 測試 Token 寫入成功!

然後在瀏覽器網址列輸入 Flask 開發伺服器預設網址 http://localhost:5000/ 會看到 serverless 平台網站已順利運行, 按 "登入系統" :



輸入 .env 檔案中設定的 SECRET_TOKEN 密碼, 按 "登入" 鈕 :



登入成功後按 "函式列表" 進入管理頁面首頁 :



按 "資料表" :




可見多了一個 api_tokens 的資料表, 這就是 Claude 重構此平台時所添加, 用來儲存 API Key (token) 的資料表, 按 "檢視" :



顯示前面用 Python 指令寫入 api_tokens 資料表的測試用權杖 :




接下來用 curl.exe 測試沒帶 token 的情況, 這樣的請求會被拒絕 : 

PS D:\antigravity_cli\projects\serverless> curl.exe -i http://localhost:5000/function/hello   
HTTP/1.1 401 UNAUTHORIZED
Server: Werkzeug/2.3.7 Python/3.10.11
Date: Fri, 17 Jul 2026 11:36:12 GMT
Content-Type: application/json
Content-Length: 73
Connection: close

{
  "error": "Missing API token",
  "hint": "Provide X-API-Key header"
}

可見未攜帶 token 的請求會回應 "401 UNAUTHORIZED". 主體 (body) 內容為 JSON 訊息, error 鍵之值表示沒有收到 API token; 而 hit 鍵之值則表示必須在標頭攜帶 X-API-Key, 可在 curl.exe 指令中用 -H 參數指定此 token, 例如 : 

PS D:\antigravity_cli\projects\serverless> curl.exe -i http://localhost:5000/function/hello -H "X-API-Key: test-token-123"
HTTP/1.1 200 OK
Server: Werkzeug/2.3.7 Python/3.10.11
Date: Fri, 17 Jul 2026 15:41:03 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 12
Connection: close

Hello World!

可見若有攜帶正確的 X-API-Key 就會執行該函式, 回應 200 OK. 


4. 線上管理 API Key : 

在第一版的 serverless 函式執行平台就已實作了線上管理 serverless.db 的功能, 除了提供超連結可刪除資料表與紀錄外, 還可以執行 SQL 指令以新增, 更新, 或刪除紀錄. 

按資料表列表頁面下方的 "執行 SQL" :




輸入 SQL 指令後按 "執行" 即可, 例如要修改 owner=developer 使用中的 token 的 SQL 指令 :

UPDATE api_tokens 
SET token = 'new-test-token' WHERE owner='developer' AND is_active=1;




執行結果為成功 :




返回資料表內容檢視頁面可知 API Key (token 欄位) 已經更新了 : 




新增 API key 的 SQL 例如 : 

INSERT INTO api_tokens (token, owner, is_active) 
VALUES ('fKFQPound41lv8FrWzU-lmFywxAT47uMwfvmBbiuLTE', 'proj-a', 1);






只要以管理員身分登入, 就可以線上管理 API Key 了. 

以上測試顯示 agy 的重構確實達成了預期的目標, 讓 serverless 平台升級為需要 API Key 才能使用的網路服務, 避免伺服器被濫用或遭到攻擊. 

2026年7月17日 星期五

購買 Hahow 課程 : AI Coding 無痛上手

昨天在 Hahow 購買了 Kai 老師的 "AI Coding 無痛上手" 課程募資 : 





雖然我平常就在用 Vibe coding, 但看到課程介紹就很想去了解人家的作法, 不同的老師有不同的方法論, 值得花點學費去了解別人的 AI 工作術. 但我最近似乎買太多課程了, 月底從岡山回來後, 八月份起要來好好上課了. 

購買 MasterTalk 課程 (補記)

今天查看 LINE 紀錄才想起上周三 (7/8) 請假陪水某與菁菁去大甲鎮瀾宮拜拜, 在自強號上滑手機時看到 MasterTalk 平台的新課程通知 :





有點數折換打 9 折, 實付 3492 元. 

2026年7月15日 星期三

2026年7月14日 星期二

參加嘖嘖 ORIVA 隨行烘乾機募資預購

最近在 FB 看到嘖嘖的 ORIVA 隨行烘乾機募資廣告, 覺得對出國旅行衣物烘乾很方便, 就參加預購了, 但要八月才開始出貨, 趕不上月底跟阿中的岡山之旅 :






市圖還書 1 本 : 超有料!職場第一實用的AI工作術

此書被預約須還 : 
旗標出版的書內容質感都不錯 (個人評價 No.1), 此書亦然. 此書對於剛要步入職場的社會新鮮人實用度非常高, 從 AI 製做簡報, 影片修圖, 處理郵件+合約, 到廣宣文案生成等等, 涵蓋面足敷剛踏入職場的新手應付工作所需. 

2026年7月13日 星期一

申請中國信託和泰聯名卡

由於最近打算買新車 (豐田 RAV4 旗艦版), 我詢問 Gemini 付款方式, 它說可以辦車貸, 若資金充足建議全款付清, 不用給銀行賺利息. 我算是個小田僑, 當然是全款付清啦! 他說那這樣可以先辦一張中國信託和泰聯名卡, 這樣尾款可以刷這張卡 (綁定和泰 Pay), 全額刷卡可以現賺 0.6% 的和泰 Points 點數 (無上限, 刷 120 萬可拿到 7200 點, 1 點等於 1 元), 可以直接拿來折抵 RAV4 年底前的原廠定期保養費用 (目前點數計算方式 1 點等於 1 元在 2026-12-31 前有效). 

刷卡購車點數回饋辦法 : 





線上申辦網址 : 


填寫姓名與生日, 勾選卡片類型 (可綁悠遊卡), 馬上就辦好了, 實體卡約需五天寄達, 但現在就可以下載和泰 Pay app 綁定此張信用卡消費了 : 





現在刷聯名卡有洗車優惠 : 





如果前段的訂金與後段的規費+保險也可刷這張卡, 回饋是 1%; 中段的高額尾款 (約 120 萬) 回饋比率是 0.6%, 以 5 萬元訂金計算可得 500 點, 合計 7700 點. 在刷卡付尾款前一天先打電話給中國信託客服, 表示明天買新車要刷和泰 Pay 尾款 120 萬, 現在直接轉帳 120 萬進中信指定帳戶, 要求客服幫我溢繳調高臨額 (人話 : 預存現金然後叫銀行暫時放寬刷卡上限) 即可. 


# 2026-07-14 補充 :

已下載 My Toyota app, 等收到信用卡再綁定. 很巧昨日剛好收到公司福利會寄來的 Toyota 購車優惠 DM, RAV4 Adventure 價格寫 12? 萬 (我猜是 127~129), 雖沒寫旗艦版價格, 估計可能 123~125, 已填寫諮詢單回傳, 等業務員電話來時再詳細詢問.