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 已被建立, 再次執行網頁測試結果相同. 

沒有留言 :