[Go] 用 Go + Gemini + GCP 打造智慧垃圾車 LINE Bot:從查詢到提醒的完整解決方案

image-20251102143236468

前情提要

在台灣,垃圾車的時間總是讓人捉摸不定。明明記得昨天是晚上七點來,今天卻遲遲等不到;或是剛好外出倒垃圾,垃圾車就這樣錯過了。相信這是許多人共同的困擾。

隨著智慧城市的發展,越來越多城市開始提供垃圾車即時資訊 API,但這些資料對一般民眾來說並不容易使用。

這時候我看到臉書上一個朋友貼文 ,他敘述他做出了一個垃圾車追蹤的網站。 (網站github)

image-20251102143208176

這時候我在想,難道不能結合 LINE Bot 做出一個可以很快速幫助到其他的工具嗎?因此,我決定打造一個垃圾車 LINE Bot,讓大家可以透過最熟悉的通訊軟體,輕鬆查詢垃圾車資訊,甚至設定提醒通知。更重要的是,這個 Bot 不只是簡單的指令查詢,而是整合了 Google Gemini AI,能夠理解「我晚上七點前在哪裡倒垃圾?」這樣的自然語言,提供真正智慧化的服務體驗。

專案程式碼

https://github.com/kkdai/linebot-garbage-helper

(透過這個程式碼,可以快速部署到 GCP Cloud Run,並使用 Cloud Build 實現自動化 CI/CD)

🗑️ 專案功能介紹

核心功能

  1. 🗑️ 即時查詢垃圾車
    • 輸入地址或分享位置即可查詢附近垃圾車站點
    • 顯示預計抵達時間、路線資訊和 Google Maps 導航連結
  2. ⏰ 智慧提醒系統
    • 可設定垃圾車抵達前 N 分鐘提醒
    • 自動推播通知,再也不會錯過垃圾車
    • 支援多種提醒狀態管理(活躍、已發送、已過期、已取消)
  3. 🤖 自然語言查詢
    • 整合 Google Gemini AI,支援自然語言理解
    • 例如:「我晚上七點前在台北市大安區哪裡倒垃圾?」
    • 自動提取地點、時間範圍等查詢條件
  4. ❤️ 收藏地點功能
    • 儲存常用地點(家、公司等)
    • 快速查詢收藏地點的垃圾車資訊

🏗️ 技術架構說明

系統架構圖

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   LINE Client   │───▶│    Cloud Run     │───▶│   Firestore     │
└─────────────────┘    │  (Go App)        │    │   (Database)    │
                       └──────────────────┘    └─────────────────┘
                              │
                              ▼
                    ┌──────────────────┐
                    │  External APIs   │
                    │  • Google Maps   │
                    │  • Gemini AI     │
                    │  • 垃圾車資料源   │
                    └──────────────────┘
                              │
                              ▼
                    ┌──────────────────┐
                    │ Cloud Scheduler  │
                    │ (提醒排程觸發)    │
                    └──────────────────┘

💻 核心功能實作

1. 垃圾車資訊的處理

這是整個系統的核心部分,負責從開放資料 API 取得垃圾車資訊,並進行智慧化的查詢處理。

資料來源與結構設計

我們使用了開源專案 Yukaii/garbage 提供的垃圾車資料 API,這個 API 提供了台北市的即時垃圾車收集點資訊。

type GarbageData struct {
    Result GarbageResult `json:"result"`
}

type CollectionPoint struct {
    ID            int         `json:"_id"`
    District      string      `json:"行政區"`     // 行政區域
    Neighborhood  string      `json:"里別"`      // 里別資訊
    VehicleNumber string      `json:"車號"`      // 垃圾車車號
    Route         string      `json:"路線"`      // 收集路線
    ArrivalTime   string      `json:"抵達時間"`   // 預計抵達時間
    DepartureTime string      `json:"離開時間"`   // 預計離開時間
    Location      string      `json:"地點"`      // 收集地點
    Longitude     string      `json:"經度"`      // 經度座標
    Latitude      string      `json:"緯度"`      // 緯度座標
}

垃圾車資料適配器 (GarbageAdapter)

我們設計了一個專門的適配器來處理垃圾車資料的取得和查詢:

type GarbageAdapter struct {
    httpClient *http.Client
}

func (ga *GarbageAdapter) FetchGarbageData(ctx context.Context) (*GarbageData, error) {
    url := "https://raw.githubusercontent.com/Yukaii/garbage/data/trash-collection-points.json"
    
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, err
    }
    
    resp, err := ga.httpClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
    }
    
    var data GarbageData
    if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
        return nil, err
    }
    
    return &data, nil
}

智慧距離計算與最近站點查詢

系統能夠根據使用者位置,找出最近的垃圾車收集點:

func (ga *GarbageAdapter) FindNearestStops(userLat, userLng float64, data *GarbageData, limit int) ([]*NearestStop, error) {
    var nearestStops []*NearestStop
    now := time.Now()
    
    for _, point := range data.Result.Results {
        // 解析座標
        lat, lng, err := ga.parseCoordinates(point.Latitude, point.Longitude)
        if err != nil {
            continue
        }
        
        // 使用 Haversine 公式計算距離
        distance := geo.CalculateDistance(userLat, userLng, lat, lng)
        
        // 解析時間並處理跨日問題
        eta, err := parseTimeToToday(point.ArrivalTime)
        if err != nil {
            continue
        }
        
        // 如果時間已過,調整到明天
        if eta.Before(now) {
            eta = eta.Add(24 * time.Hour)
        }
        
        nearestStops = append(nearestStops, &NearestStop{
            Stop:            stop,
            Route:           route,
            Distance:        distance,
            ETA:             eta,
            CollectionPoint: &point,
        })
    }
    
    // 按距離排序
    sort.Slice(nearestStops, func(i, j int) bool {
        return nearestStops[i].Distance < nearestStops[j].Distance
    })
    
    return nearestStops[:limit], nil
}

時間窗口查詢功能

這是配合 Gemini AI 的進階功能,當使用者詢問「晚上七點前在哪裡倒垃圾?」時:

func (ga *GarbageAdapter) FindStopsInTimeWindow(userLat, userLng float64, data *GarbageData, timeWindow TimeWindow, maxDistance float64) ([]*NearestStop, error) {
    var validStops []*NearestStop
    
    for _, point := range data.Result.Results {
        lat, lng, err := ga.parseCoordinates(point.Latitude, point.Longitude)
        if err != nil {
            continue
        }
        
        distance := geo.CalculateDistance(userLat, userLng, lat, lng)
        
        // 距離過濾
        if maxDistance > 0 && distance > maxDistance {
            continue
        }
        
        eta, err := parseTimeToToday(point.ArrivalTime)
        if err != nil {
            continue
        }
        
        // 時間窗口過濾
        if !isTimeInWindow(eta, timeWindow) {
            continue
        }
        
        validStops = append(validStops, &NearestStop{
            Stop:            stop,
            Route:           route,
            Distance:        distance,
            ETA:             eta,
            CollectionPoint: &point,
        })
    }
    
    // 按時間排序,顯示最早的收集點
    sort.Slice(validStops, func(i, j int) bool {
        return validStops[i].ETA.Before(validStops[j].ETA)
    })
    
    return validStops, nil
}

彈性的時間解析處理

考慮到資料來源的時間格式可能不一致,我們實作了彈性的時間解析:

func parseTimeToToday(timeStr string) (time.Time, error) {
    now := time.Now()
    
    // 處理 4 位數格式 (1830)
    if len(timeStr) == 4 {
        layout := "1504"
        t, err := time.Parse(layout, timeStr)
        if err != nil {
            return time.Time{}, err
        }
        return time.Date(now.Year(), now.Month(), now.Day(), 
                        t.Hour(), t.Minute(), 0, 0, now.Location()), nil
    }
    
    // 處理標準格式 (18:30)
    layout := "15:04"
    t, err := time.Parse(layout, timeStr)
    if err != nil {
        return time.Time{}, err
    }
    
    return time.Date(now.Year(), now.Month(), now.Day(), 
                    t.Hour(), t.Minute(), 0, 0, now.Location()), nil
}

整合到查詢流程

在實際的查詢處理中,這些功能被整合到統一的介面:

func (h *Handler) searchNearbyGarbageTrucks(ctx context.Context, userID string, lat, lng float64, intent *gemini.IntentResult) {
    log.Printf("Searching nearby garbage trucks for user %s at coordinates: lat=%f, lng=%f", userID, lat, lng)
    
    // 取得最新的垃圾車資料
    garbageData, err := h.garbageAdapter.FetchGarbageData(ctx)
    if err != nil {
        log.Printf("Error fetching garbage data for user %s: %v", userID, err)
        h.replyMessage(ctx, userID, "抱歉,無法取得垃圾車資料。")
        return
    }
    
    var nearestStops []*garbage.NearestStop
    
    // 如果有時間窗口查詢,使用時間過濾
    if intent != nil && (intent.TimeWindow.From != "" || intent.TimeWindow.To != "") {
        fromTime, toTime, err := h.geminiClient.ParseTimeWindow(intent.TimeWindow)
        if err == nil {
            timeWindow := garbage.TimeWindow{From: fromTime, To: toTime}
            nearestStops, err = h.garbageAdapter.FindStopsInTimeWindow(lat, lng, garbageData, timeWindow, 2000)
        }
    }
    
    // 如果沒有時間過濾結果,使用一般的最近距離查詢
    if len(nearestStops) == 0 {
        nearestStops, err = h.garbageAdapter.FindNearestStops(lat, lng, garbageData, 5)
    }
    
    // 發送結果給使用者
    h.sendGarbageTruckResults(ctx, userID, nearestStops)
}

這樣的設計讓系統能夠:

  1. 高效率處理大量資料:每次查詢都會取得最新資料,確保資訊準確性
  2. 智慧化查詢:結合地理位置、時間窗口等多重條件
  3. 彈性擴展:易於加入其他城市的垃圾車資料源
  4. 錯誤處理:對於資料格式異常有適當的容錯機制

2. LINE Webhook 處理

首先來看看如何處理 LINE 的 webhook 事件:

func (h *Handler) HandleWebhook(w http.ResponseWriter, r *http.Request) {
    log.Printf("Webhook received from %s", r.RemoteAddr)
    
    events, err := webhook.ParseRequest(h.channelSecret, r)
    if err != nil {
        log.Printf("Error parsing webhook request: %v", err)
        http.Error(w, "Bad Request", http.StatusBadRequest)
        return
    }
    
    ctx := r.Context()
    for _, event := range events {
        go h.handleEvent(ctx, event)  // 使用 goroutine 處理事件
    }
    
    w.WriteHeader(http.StatusOK)
}

3. Gemini AI 關於 LLM 理解的部分

這是整個系統最有趣的部分,透過 Gemini 來理解使用者的自然語言查詢:

func (gc *GeminiClient) AnalyzeIntent(ctx context.Context, userMessage string) (*IntentResult, error) {
    model := gc.client.GenerativeModel(gc.model)
    
    prompt := fmt.Sprintf(`你是一個查詢意圖分析器,專門分析使用者關於垃圾車的查詢。

使用者輸入可能包含地名與時間。請分析輸入並輸出 JSON 格式的結果。

輸出格式:
{
  "district": "地區名稱(如果有的話)",
  "time_window": {
    "from": "開始時間(HH:MM格式,如果有的話)",
    "to": "結束時間(HH:MM格式,如果有的話)"
  },
  "keywords": ["關鍵字陣列"],
  "query_type": "garbage_truck_eta"
}

範例:
輸入:「我晚上七點前在台北市大安區哪裡倒垃圾?」
輸出:
{
  "district": "台北市大安區",
  "time_window": {
    "from": "",
    "to": "19:00"
  },
  "keywords": ["台北市", "大安區", "倒垃圾", "晚上", "七點"],
  "query_type": "garbage_truck_eta"
}

請分析以下使用者輸入:
「%s」

請只回傳 JSON,不要包含其他說明文字。`, userMessage)

    resp, err := model.GenerateContent(ctx, genai.Text(prompt))
    if err != nil {
        return nil, err
    }
    
    // 解析 JSON 回應
    var result IntentResult
    if err := json.Unmarshal([]byte(responseText), &result); err != nil {
        // 如果 Gemini 無法解析,回退到簡單的關鍵字比對
        return &IntentResult{
            District:  extractDistrict(userMessage),
            Keywords:  []string{userMessage},
            QueryType: "garbage_truck_eta",
        }, nil
    }
    
    return &result, nil
}

4. 智慧提醒排程系統

提醒系統是這個專案的核心功能之一,設計上考慮了可靠性和效能:

func (s *Scheduler) ProcessReminders(ctx context.Context) error {
    now := time.Now()

    // 效能優化:先檢查是否有活躍提醒
    count, err := s.store.CountActiveReminders(ctx)
    if err != nil {
        log.Printf("Warning: failed to count active reminders: %v", err)
    } else if count == 0 {
        log.Printf("No active reminders, skipping processing")
        return nil
    }

    reminders, err := s.store.GetActiveReminders(ctx, now)
    if err != nil {
        return fmt.Errorf("failed to get active reminders: %w", err)
    }

    log.Printf("Found %d active reminders to process", len(reminders))

    for _, reminder := range reminders {
        notificationTime := reminder.ETA.Add(-time.Duration(reminder.AdvanceMinutes) * time.Minute)
        
        // 檢查是否到了發送提醒的時間
        if now.Before(notificationTime) {
            continue  // 還不到發送時間
        }

        if now.After(reminder.ETA) {
            // ETA 已過期,標記為過期
            s.store.UpdateReminderStatus(ctx, reminder.ID, "expired")
            continue
        }

        // 發送提醒通知
        if err := s.sendReminderNotification(ctx, reminder); err != nil {
            log.Printf("Error sending reminder %s: %v", reminder.ID, err)
            continue
        }

        // 更新狀態為已發送
        s.store.UpdateReminderStatus(ctx, reminder.ID, "sent")
    }

    return nil
}

5. Firestore 資料結構設計

我們使用 Firestore 來儲存使用者資料和提醒資訊:

type Reminder struct {
    ID             string    `firestore:"id"`
    UserID         string    `firestore:"userId"`
    StopName       string    `firestore:"stopName"`
    RouteID        string    `firestore:"routeId"`
    ETA            time.Time `firestore:"eta"`
    AdvanceMinutes int       `firestore:"advanceMinutes"`
    Status         string    `firestore:"status"`  // active, sent, expired, cancelled
    CreatedAt      time.Time `firestore:"createdAt"`
    UpdatedAt      time.Time `firestore:"updatedAt"`
}

type User struct {
    ID        string     `firestore:"id"`
    Favorites []Favorite `firestore:"favorites"`
    CreatedAt time.Time  `firestore:"createdAt"`
    UpdatedAt time.Time  `firestore:"updatedAt"`
}

6. 雙重保障的提醒機制

為了確保提醒不會遺漏,系統設計了雙重保障機制:

  1. 本地排程器:應用啟動時自動開始背景排程服務
  2. 外部觸發:透過 Cloud Scheduler 定期調用 /tasks/dispatch-reminders
// 本地排程器
func (s *Scheduler) StartScheduler(ctx context.Context) {
    ticker := time.NewTicker(1 * time.Minute)
    defer ticker.Stop()

    cleanupTicker := time.NewTicker(1 * time.Hour)
    defer cleanupTicker.Stop()

    for {
        select {
        case <-ctx.Done():
            return
        case <-ticker.C:
            s.ProcessReminders(ctx)
        case <-cleanupTicker.C:
            s.CleanupExpiredReminders(ctx)
        }
    }
}

// 外部觸發端點
r.HandleFunc("/tasks/dispatch-reminders", func(w http.ResponseWriter, r *http.Request) {
    token := r.Header.Get("Authorization")
    if token != "Bearer "+cfg.InternalTaskToken {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }

    if err := reminderService.ProcessReminders(r.Context()); err != nil {
        log.Printf("Error processing reminders: %v", err)
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }

    w.WriteHeader(http.StatusOK)
})

🔧 遇到的挑戰與解決方案

1. 地址處理的多層 Fallback 策略

問題:使用者輸入具體地址(如「台北市中正區重慶南路一段122號」)時,Gemini 意圖分析可能失敗或只提取部分資訊,導致查詢失敗。

解決方案:設計多層 fallback 機制,確保任何情況下都能找到合適的地址:

func (h *Handler) handleTextMessage(ctx context.Context, userID, text string) {
    // 意圖分析失敗時,仍然繼續處理而非直接返回錯誤
    intent, err := h.geminiClient.AnalyzeIntent(ctx, text)
    if err != nil {
        log.Printf("Error analyzing intent: %v", err)
        intent = nil  // 設為 nil 繼續處理
    }
    
    var addressToGeocode string
    var addressMethod string
    
    // Method 1: 使用 Gemini 解析的 District
    if intent != nil && intent.District != "" {
        addressToGeocode = intent.District
        addressMethod = "intent.District"
    } else {
        // Method 2: 使用 Gemini 地址提取
        extractedLocation, err := h.geminiClient.ExtractLocationFromText(ctx, text)
        if err == nil && strings.TrimSpace(extractedLocation) != "" {
            addressToGeocode = strings.TrimSpace(extractedLocation)
            addressMethod = "gemini.ExtractLocation"
        } else {
            // Method 3: 直接使用原始文字
            addressToGeocode = text
            addressMethod = "original.text"
        }
    }
    
    // 地理編碼處理 + Fallback 策略
    location, err := h.geoClient.GeocodeAddress(ctx, addressToGeocode)
    if err != nil {
        // Fallback 1: 如果不是原始文字,嘗試原始文字
        if addressMethod != "original.text" {
            location, err = h.geoClient.GeocodeAddress(ctx, text)
            if err == nil {
                h.searchNearbyGarbageTrucks(ctx, userID, location.Lat, location.Lng, intent)
                return
            }
        }
        
        // Fallback 2: 嘗試簡化地址(提取縣市區)
        simplifiedAddress := h.extractSimplifiedAddress(text)
        if simplifiedAddress != "" {
            location, err = h.geoClient.GeocodeAddress(ctx, simplifiedAddress)
            if err == nil {
                h.searchNearbyGarbageTrucks(ctx, userID, location.Lat, location.Lng, intent)
                return
            }
        }
        
        // 所有方法都失敗才回傳錯誤
        h.replyMessage(ctx, userID, "抱歉,我找不到位置資訊...")
        return
    }
    
    h.searchNearbyGarbageTrucks(ctx, userID, location.Lat, location.Lng, intent)
}

2. Gemini API 的穩定性處理

問題:Gemini API 偶爾會回傳非 JSON 格式的回應,導致解析失敗。

解決方案:實作錯誤處理和回退機制:

var result IntentResult
if err := json.Unmarshal([]byte(responseText), &result); err != nil {
    // 如果 Gemini 無法解析,回退到簡單的關鍵字比對
    return &IntentResult{
        District:  extractDistrict(userMessage),
        Keywords:  []string{userMessage},
        QueryType: "garbage_truck_eta",
        TimeWindow: TimeWindow{From: "", To: ""},
    }, nil
}

3. Firestore 查詢效能優化

問題:每次都查詢所有活躍提醒會造成不必要的資料讀取。

解決方案:加入 count 查詢作為早期回傳優化:

// 先檢查是否有活躍提醒
count, err := s.store.CountActiveReminders(ctx)
if count == 0 {
    log.Printf("No active reminders, skipping processing")
    return nil
}

4. Cloud Scheduler 區域設定問題

問題:Cloud Scheduler 在某些區域可能不支援,導致自動提醒失效。

解決方案:設計雙重保障機制,即使外部排程器失效,本地排程器仍能正常運作。

5. 時區處理的複雜性

問題:在雲端環境中,伺服器可能運行在 UTC 時區,但垃圾車資料和使用者都在台灣時區,導致提醒時間計算錯誤。

具體症狀

  • 使用者在半夜收到「垃圾車 9 分鐘後抵達」的提醒
  • 提醒時間與實際垃圾車時間不符

問題分析

// 問題代碼:混用不同時區
now := time.Now()                    // 可能是 UTC
eta := parseTimeToToday("19:00")     // 台灣時間 19:00
timeUntil := eta.Sub(now)           // 時區不一致導致計算錯誤

解決方案:建立統一的時區處理機制:

// 1. 建立時區工具函數
package utils

func GetTaiwanTimezone() *time.Location {
    taipeiTZ, err := time.LoadLocation("Asia/Taipei")
    if err != nil {
        // 備用方案:固定時區 UTC+8
        taipeiTZ = time.FixedZone("CST", 8*3600)
    }
    return taipeiTZ
}

func NowInTaiwan() time.Time {
    return time.Now().In(GetTaiwanTimezone())
}

func ToTaiwan(t time.Time) time.Time {
    return t.In(GetTaiwanTimezone())
}

// 2. 修復時間解析
func parseTimeToToday(timeStr string) (time.Time, error) {
    taipeiTZ := utils.GetTaiwanTimezone()
    now := utils.NowInTaiwan()
    
    // 確保解析出的時間在台灣時區
    return time.Date(now.Year(), now.Month(), now.Day(), 
                    hour, minute, 0, 0, taipeiTZ), nil
}

// 3. 修復提醒計算
func (s *Scheduler) processReminder(reminder *store.Reminder) error {
    now := utils.NowInTaiwan()
    etaInTaipei := utils.ToTaiwan(reminder.ETA)
    
    notificationTime := etaInTaipei.Add(-10 * time.Minute)
    timeUntilArrival := etaInTaipei.Sub(now)
    
    // 現在時間計算是正確的
    if now.After(notificationTime) && now.Before(etaInTaipei) {
        // 發送提醒
    }
}

測試驗證

// 建立測試來驗證時區修復
func TestTimezoneHandling() {
    now := utils.NowInTaiwan()
    eta := time.Date(now.Year(), now.Month(), now.Day(), 19, 0, 0, 0, utils.GetTaiwanTimezone())
    
    timeUntil := eta.Sub(now)
    fmt.Printf("距離垃圾車抵達: %.0f 分鐘", timeUntil.Minutes())
}

這個修復確保了無論伺服器運行在哪個時區,所有時間計算都基於台灣時間進行。

6. LINE Bot 訊息推播限制

問題:LINE Bot 有推播訊息的頻率限制。

解決方案

  • 實作提醒狀態管理,避免重複發送
  • 加入適當的錯誤處理和重試機制
  • 使用 goroutine 非同步處理,避免阻塞主要流程

7. Google Maps API 授權配置問題

問題:在實作地理編碼功能時遇到 REQUEST_DENIED - This API project is not authorized to use this API 錯誤。

問題分析

Geocoding API 是 Google Maps Platform 的核心服務,但預設並未啟用。即使有 API Key,如果沒有在 GCP 專案中啟用相應的 API,仍會被拒絕存取。

完整解決方案

1. 啟用必要的 Google Maps API

在 GCP Console 中啟用以下 API(這些都是本專案會用到的):

# 使用 gcloud 命令一次性啟用
gcloud services enable \
  geocoding-backend.googleapis.com \
  maps-backend.googleapis.com \
  places-backend.googleapis.com \
  geolocation.googleapis.com \
  --project=your-project-id

或透過 GCP Console 手動啟用:

  • Geocoding API - 地址轉坐標(必需)
  • Maps JavaScript API - 地圖顯示
  • Places API - 地點搜索
  • Geolocation API - 定位服務

2. 建立並限制 API Key

建立 API Key

  1. 前往 APIs & Services → Credentials
  2. 點擊 “CREATE CREDENTIALS” → “API key”
  3. 複製產生的 API key

設定安全限制(重要!):

API restrictions(推薦設定):
✅ Restrict key
  ✅ Geocoding API
  ✅ Places API
  ✅ Maps JavaScript API
  ✅ Geolocation API

Application restrictions(生產環境必需):
選項 A: HTTP referrers - 適合網頁應用
選項 B: IP addresses - 適合後端服務(本專案建議)
選項 C: None - 僅限開發測試

3. 配置環境變數

Cloud Build 部署: 在觸發器的替代變數中設定:

_GOOGLE_MAPS_API_KEY = 你的API_KEY

本地開發

# .env 文件
GOOGLE_MAPS_API_KEY=你的API_KEY

4. 驗證 API 生效

等待 1-2 分鐘後測試:

# 測試 Geocoding API
curl "https://maps.googleapis.com/maps/api/geocode/json?address=台北101&key=你的API_KEY"

# 成功回應應包含:
# {
#   "results": [...],
#   "status": "OK"
# }

關鍵學習

  1. API 啟用是前提:有 API Key ≠ 有權限使用 API,必須明確啟用每個服務
  2. 安全性優先:生產環境務必設定 API restrictions,避免 API key 被濫用
  3. 費用控管:Google Maps API 有免費額度(每月 $200),但建議設定預算提醒
  4. 區域考量:確保 API key 的限制設定不會阻擋 Cloud Run 的請求

費用說明

  • Geocoding API:每月 $200 免費額度(約 40,000 次請求)
  • 超過後:$5/1000 次請求
  • 建議在代碼中實作快取機制,減少 API 調用次數

這個問題提醒我們:在整合第三方服務時,不僅要有正確的憑證,還要確保所有必要的服務都已正確啟用和配置。

📊 效能監控與可靠性

健康檢查端點

r.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("OK"))
}).Methods("GET")

詳細的日誌記錄

系統在關鍵節點都有詳細的日誌記錄,方便除錯和監控:

log.Printf("Processing reminder %s: ETA=%s, NotificationTime=%s, AdvanceMinutes=%d",
    reminder.ID, reminder.ETA.Format("2006-01-02 15:04:05"),
    notificationTime.Format("2006-01-02 15:04:05"), reminder.AdvanceMinutes)

🎯 總結與未來改進

這個垃圾車 LINE Bot 專案展示了如何結合現代化的技術棧來解決日常生活中的實際問題。透過 Go 語言的高效能、Gemini AI 的自然語言理解、以及 GCP 的雲端服務,我們打造了一個既實用又智慧的解決方案。

專案亮點

  1. 智慧化查詢:透過 Gemini AI 理解自然語言,提供更友善的使用體驗
  2. 可靠的提醒系統:雙重保障機制確保重要通知不會遺漏
  3. 現代化架構:使用微服務架構,易於擴展和維護
  4. 自動化部署:完整的 CI/CD 流程,降低維運成本

未來改進方向

  1. 效能優化
    • 建立 Firestore 複合索引提升查詢效能
    • 實作批量推播減少 API 調用
  2. 可靠性提升
    • 加入分布式鎖避免重複執行
    • 實作指數退避重試機制
  3. 功能擴展
    • 支援更多城市的垃圾車資料
    • 加入使用統計和分析功能
    • 整合更多 AI 能力,如圖片識別
  4. 監控強化
    • 整合 Prometheus/OpenTelemetry
    • 建立完整的效能監控儀表板

透過這個專案,我深刻體會到 Go 語言在雲端原生應用開發上的優勢,以及 AI 技術如何讓傳統應用變得更加智慧化。

實戰經驗分享

在開發過程中,我們遇到了兩個典型的挑戰:

1. AI 模型的不穩定性處理

挑戰:如何處理 AI 模型的不穩定性。透過建立本地測試環境和多層 fallback 策略,我們確保了系統的穩定性:

# 我們建立了完整的測試環境
export GEMINI_API_KEY='your_key_here'
cd test && go run simple_main.go

這個測試程式幫助我們發現 Gemini 在地址解析上的限制,並及時實作了相應的解決方案。

2. 雲端應用的時區陷阱

挑戰:最容易被忽略但影響最大的問題 - 時區處理。

發現過程

  • 使用者反映在半夜收到垃圾車提醒
  • 透過日誌發現時間計算異常
  • 建立時區測試程式驗證問題
# 時區測試驗證
cd test && go run timezone_test_main.go

關鍵學習

  • 雲端部署時,絕對不能假設伺服器時區
  • 建立專門的時區工具函數,統一處理所有時間相關操作
  • 完整的測試覆蓋,包含時區邊界情況

這兩個經驗提醒我們:在 AI 驅動的雲端應用中,永遠要有備用方案,並且要特別注意基礎設施層面的差異。

希望這個經驗分享能夠幫助到正在學習相關技術的開發者們!

相關資源

[好書分享] 矽谷帝國 - 商業巨頭如何掌控經濟與社會

矽谷帝國
商業巨頭如何掌控經濟與社會
Silicon States : The Power and Politics of Big Tech and What It Means for Our Future
作者: 露西.葛芮妮  
原文作者: Lucie Greene  
譯者: 林俊宏  出版社:天下文化 

買書推薦網址:

前言:

這是 2025 年第 9 本讀完的書。當初想要了解一下矽谷內關於創業的精神與幾間巨頭對於社會的觀感。

裡面就有提到對於政治覺醒,社會政治,還有相關的社群媒體變化的內容。

大綱

世界的下一批強權將是創業家,而不是民族國家

從私部門到公部門,從地球到太空
科技企業正在重新思考一切、重新發明一切
但是──
矽谷定義的進步,是我們想要的進步嗎?
矽谷想要的未來,是我們能接受的未來嗎?

隨著民眾對政府及其機構的信心迅速流失,這個空缺正在由矽谷企業逐步填上。一小群企業擁有巨量的現金、人才和野心,正在世界各地掌握領導權與消費者的信心。

儘管科技帶來美好的承諾,對於科技企業勾勒的創新願景,我們卻不能照單全收。相反地,在我們把未來交給一小群精英公司之前,必須先認清科技企業的商業本質,理解它們對政治和社會經濟制度可能造成的影響,從而對抗它們的利益、偏見和固有缺陷。

本書中,著名的未來學者暨國際智庫領導者露西・葛芮妮訪問企業領導者、重要風投資本家、學者、記者與運動人士等,探討矽谷因其全球影響力必然帶來的緊張局勢,洞察這些公司真正著眼的利益所在,還有它們對不知情的公民擁有怎麼樣的巨大力量。

理解谷歌、蘋果、亞馬遜、臉書如何在全球發揮影響力
洞察矽谷巨頭企業如何全面蠶食我們的環境

本書帶你見識:
矽谷的影響力:它如何從晶片中心變成全球的強勢集團?
矽谷的未來版圖:創新跨界的深、遠、廣,超乎你的想像
矽谷的陰暗面:壟斷、操縱、歧視,嚴重的文化缺陷

《矽谷帝國》重點整理

根據提供的28則劃線內容,本書探討矽谷(Silicon Valley)作為科技帝國的崛起、其文化、社會影響、挑戰與爭議。書中強調矽谷不僅是科技創新中心,更是重塑民主、慈善、教育、媒體與全球價值的力量,但也暴露偏見、隱私與不平等問題。以下按主題分類整理重點,每項包含相關劃線摘要,並標註主要標籤與日期。分類基於內容的邏輯關聯,旨在捕捉書的核心論點。

1. 矽谷歷史與文化起源

  • 矽谷的定義與擴張:矽谷一詞源於1971年,指舊金山灣區南部聖塔克拉拉谷的矽晶片製造商,後擴展至舊金山與奧克蘭(2015年Uber遷至奧克蘭)。(日期:2023/11/4)
  • PayPal黑幫的影響:PayPal前員工(如彼得.提爾、伊隆.馬斯克、雷德.霍夫曼)在2002年售出公司後,創立或投資Palantir、SpaceX、特斯拉、LinkedIn、Yelp、YouTube等,定義矽谷文化與名人地位。(日期:2024/05/11;標籤:折衷求生的理想主義)
  • 企業擴張模式:矽谷從賣軟體給傳統產業轉為自建解決方案,如Uber與Lyft取代計程車業。(日期:2024/05/11;標籤:折衷求生的理想主義)

2. 科技與民主:投票、隱私與政府干預

  • 年輕選民的投票障礙:投票系統過時(如紙本登記需印表、郵寄,家有印表機僅4%),非冷漠所致。Vote.org(Y-Combinator支持)用科技、簡訊與行銷推動登記,強調選舉日應放假。(日期:2024/05/18、2025/11/1;標籤:駭進政府、不起眼的安全網)
  • 資料隱私與公眾理解不足:矽谷擴及醫療、金融,民眾欠缺科技隱私知識,後果憂心。FutureGov協助政府數位轉型。(日期:2025/11/1;標籤:不起眼的安全網)
  • 外國干預與社群媒體:俄國用臉書、推特等干預美國大選與脫歐公投,穆勒起訴13名俄國人等。社群媒體成分裂工具。(日期:2024/05/11、2024/06/1;標籤:駭進政府、假新聞及未來)
  • 文化衝突:祖克柏推Free Basics在印度遭批,忽略當地價值(如象神珍惜獨立),象徵西方企業強加控制。(日期:2024/06/1;標籤:矽谷傳教士)

3. 慈善與社會公益:商業化與風險

  • 慈善事業2.0:矽谷以登月級規模進軍慈善,2017年個人捐款達2,810億美元(成長超GDP),不止捐錢,還用資料、科技追求曝光與成效。(日期:2025/10/11;標籤:策略性慈善事業)
  • 公益商業化的危險:將公益變hashtag行銷,扭曲問題、塑矽谷領袖為救世主,僅救「值得」問題。(日期:2025/11/1;標籤:策略性慈善事業)
  • SXSW與潮流:2016年SXSW推社會公益企業,聯合國與谷歌合作,出書如《做好事正時尚》。(日期:2025/10/11;標籤:策略性慈善事業)
  • 臉書的柳樹村:在門羅公園建59英畝社區,包括員工/低收入住宅、自行車道與鐵路更新,仿維多利亞模範村。(日期:2023/11/4)

4. 多元性與偏見:性別、種族與數據問題

  • 女性與少數族群弱勢:臉書女性員工僅35%,科技職位女性19%、大學畢業生27%。零工經濟(如Airbnb)女性更不利。(日期:2025/10/11;標籤:零工經濟裡的女性更形弱勢、增進多元)
  • 矽谷盲點:慈善與創新反映「白人、有錢、25歲男人」視角,種族/性別盲點普遍。(日期:2025/11/1;標籤:策略性慈善事業)
  • 數據偏見在醫療:矽谷試圖去中心化醫療(提供資訊、追蹤健康),但無重大突破。醫療支出加劇不平等,最窮10%所得降47.6%,701萬人陷貧窮。(日期:2025/08/30;標籤:數據的偏見)

5. 教育與技能轉型:矽谷的替代模式

  • UnCollege的間隔年:9個月密集計畫,標榜「掌握教育」,用「同齡群」與「旅程」(如印度志工)教社會影響、同理心、適應。搭教育新創熱潮。(日期:2025/08/30;標籤:如果矽谷成立了一所學校)
  • 未來技能需求:2015年需解決問題、協調、管理;2020年加情緒智商、認知靈活性。但傳統課程未涵蓋。(日期:2025/08/30;標籤:如果矽谷成立了一所學校)

6. 媒體、假新聞與文化影響

  • 假新聞與灰色地帶:矽谷平台成新聞中介/製造者,與未查證內容並列。廣告抽離僅限明顯爭議;AI新聞、業配、俄國宣傳難處理。英國要求祖克柏證據。(日期:2024/06/1、2024/05/18;標籤:假新聞及未來)
  • 媒體觀感惡化:2017年調查,53%對電視、48%對社群、45%對線上新聞變差(69%視為負面)。傳統媒體權力受矽谷侵蝕,川普推特追隨者超SNL收視。(日期:2024/05/18;標籤:假新聞及未來)
  • 矽谷傳教士文化:Instagram驅動旅行(如古巴哈瓦那),但上傳受限。媒體轉向推崇科技新貴(如《浮華世界》峰會),反對聲音(如歐盟罰款、亞馬遜壟斷)被淹沒。(日期:2024/06/1、2024/05/18;標籤:矽谷傳教士、假新聞及未來)

7. 企業活動與市場擴張

  • Airbnb的轉型:從房屋共享跨足旅遊(線上指南、預訂),避法規問題。2016年房東大會門票345美元+60美元獎項,6,000人需住宿。(日期:2025/08/30;標籤:增進多元)
  • 其他活動:Salesforce Dreamforce門票近2,000美元;Refinery29的29Rooms互動展,適合Instagram。(日期:2025/08/30;標籤:增進多元)

心得

如果你也想買,我建議你要找一下有沒有其他更新的書籍。畢竟矽谷是一個高科技密集與人才密集的地方,許多變化都是相當的快速的。而這一本書的原文是在 2019 寫的,也代表著他減少了近幾年幾件重要的事項:

  • COVID 19 的市場變化
  • 區塊鏈的大起大落
  • 川普就任對於矽谷的影響。

但是除了這些之外,你可以觀察到以些相關的矽谷事件。比如說:

  • PayPal 成功後帶來之後對矽谷的影響
  • Uber 對於擴張市場造成各國的反抗浪潮
  • Airbnb 對於市場的破壞性創新

當然,還是有一些比較進階的話題:

  • 性別平等在矽谷中的相關衝突
  • 政治議題帶來矽谷科技圈的影響

這是一本寫得蠻深刻的書籍,雖然時間比較久遠,但是也都是很有趣的一些內容。很推薦給大家。

[Gemini][Python] LINE Bot 跟 Google 自動化支付整合試試看 Agent Payments Protocol (AP2) - 企業級架構升級版

Agent Payments Protocol Graphic

前情提要

現在 Agent 的相關框架相當的多,但是其實 Google 在日前也宣布了一個蠻有趣的傳輸協定 Agent Payments Protocol (AP2) ,用來做進銷存管理跟支付的一套 agent framework 。之前我也有分享過一些 Gemini 跟 Google Agent 相關的文章,這次想要來試試看整合 AP2 協議到 LINE Bot 裡面,看看能不能做出一個完整的購物助手。

建議大家可以看一下這個由 NotebookLM 透過我的程式碼還有部落格內容產生的影片,有個快速概念。

這篇文章主要會跟大家分享:

  • 什麼是 AP2 (Agent Payments Protocol)
  • 為什麼要使用 AP2 來做電商整合
  • 如何實作一個具備完整購物流程的 LINE Bot
  • 實際的程式碼架構跟踩坑經驗

實際展示

image-20251031135254894

  • 首先你跟 LINE Bot 說你想要買什麼樣的產品,這時候會啟動 Shopping Agent

LINE 2025-10-31 13.49.26

  • 這時候,Shopping Agent 會去詢問庫存系統,並且回報產品資訊給你。

LINE 2025-10-31 13.49.38

  • 當他看到要付款,就會轉給 Payment Agent 來處理付款相關的資訊。

LINE 2025-10-31 13.49.47

  • 這裡也會看到,Payment Agent 有展示了一個付款 OTP 的 Demo (當然是用測試用 OTP )

範例程式碼

https://github.com/kkdai/linebot-ap2

歡迎給 Star 與分享,如果覺得實用也歡迎參與貢獻添加一些新功能。(透過這個程式碼,可以快速部署到 GCP Cloud Run)

關於 AP2 (Agent Payment Protocol) 的架構圖

image-20251031114041457

AP2 (Agent Payments Protocol) 是 Google 推出的一套創新的支付協議框架,專門設計來整合 AI Agent 與電商支付流程。跟傳統的電商支付不一樣的地方是,AP2 是專門為了 AI Agent 設計的,讓 AI 可以更自然地處理整個購物到支付的完整流程。

完整的架構介紹,我推薦大家看一下官方的介紹影片

🚀 主要整合進 LINE Bot 的相關部分

這次的升級我把整個專案改造成企業級的架構,主要分成兩個階段來實作:

第一階段:現代化專案結構

  • 模組化架構: 採用 src/linebot_ap2/ 的標準 Python 專案結構
  • Pydantic v2: 完整的資料驗證和設定管理
  • 現代化配置: 使用 pyproject.toml 取代傳統的 setup.py

第二階段:企業級安全與穩定性

  • AP2 完全合規: HMAC-SHA256 數位簽章確保交易安全
  • Circuit Breaker: 自動故障恢復機制
  • 增強型服務: 模組化的服務架構,更容易維護和擴展

AP2 的核心組件

1. Cart Mandate (購物車委託) - 企業級升級版 現在的 Cart Mandate 不只是基本的購物車功能,我們加入了完整的 AP2 合規性和安全機制:

# 新版本:企業級 Cart Mandate 創建
async def enhanced_create_cart_mandate(
    cart_items: List[Dict], 
    user_id: str,
    signature_required: bool = True
) -> str:
    """創建具備 AP2 合規性的購物車委託"""
    
    # 計算總金額並創建 mandate
    mandate_data = {
        "mandate_id": mandate_id,
        "type": "cart_mandate",
        "user_id": user_id,
        "items": validated_items,
        "total_amount": total_amount,
        "currency": "USD",
        "created_at": datetime.now(timezone.utc).isoformat(),
        "expires_at": (datetime.now(timezone.utc) + timedelta(hours=24)).isoformat()
    }
    
    # 🔐 重點:AP2 合規的數位簽章
    if signature_required:
        mandate_service = MandateService()
        mandate_data["signature"] = mandate_service.sign_mandate(mandate_data)
        mandate_data["signature_algorithm"] = "HMAC-SHA256"
    
    return json.dumps(mandate_data, ensure_ascii=False, indent=2)

這裡最重要的改進是加入了 HMAC-SHA256 數位簽章,這是 AP2 協議的核心安全要求。

2. 企業級數位簽章服務 這是這次升級最重要的安全功能,符合 AP2 協議的完整要求:

# src/linebot_ap2/services/mandate_service.py
class MandateService:
    """AP2 合規的 Mandate 管理服務"""
    
    def sign_mandate(self, mandate_data: Dict[str, Any]) -> str:
        """使用 HMAC-SHA256 簽署 mandate"""
        # 準備簽章資料
        signable_data = self._prepare_signable_data(mandate_data)
        
        # 🔐 HMAC-SHA256 數位簽章
        signature = hmac.new(
            self.secret_key.encode('utf-8'),
            signable_data.encode('utf-8'),
            hashlib.sha256
        ).hexdigest()
        
        logger.info(f"Mandate signed: {mandate_data.get('mandate_id')}")
        return signature
    
    def verify_mandate_signature(self, mandate_data: Dict[str, Any], signature: str) -> bool:
        """驗證 mandate 簽章"""
        expected_signature = self.sign_mandate(mandate_data)
        is_valid = hmac.compare_digest(signature, expected_signature)
        
        if not is_valid:
            logger.warning(f"Invalid signature for mandate: {mandate_data.get('mandate_id')}")
        
        return is_valid

3. 增強型 OTP 驗證機制 現在的 OTP 系統不只是基本驗證,還加入了企業級的安全機制:

# 新版本:增強型 OTP 驗證
async def enhanced_verify_otp(
    mandate_id: str, 
    otp_code: str, 
    user_id: str,
    max_attempts: int = 3
) -> str:
    """增強型 OTP 驗證,具備防暴力破解機制"""
    
    try:
        # 🛡️ 檢查嘗試次數限制
        if otp_data["attempts"] >= max_attempts:
            await cleanup_expired_data()  # 清理過期資料
            return json.dumps({
                "error": "Too many failed attempts",
                "status": "blocked",
                "retry_after": 300  # 5 分鐘後重試
            })
        
        # ✅ OTP 驗證成功
        if otp_data["otp"] == otp_code:
            # 🔄 Circuit Breaker 保護機制
            with retry_handler.circuit_breaker():
                transaction_id = f"txn_{uuid.uuid4().hex[:12]}"
                
                # 🔐 完整的交易記錄
                payment_service = PaymentService()
                await payment_service.record_transaction(
                    transaction_id, mandate_id, user_id
                )
        
        return json.dumps({
            "mandate_id": mandate_id,
            "transaction_id": transaction_id,
            "status": "payment_successful",
            "timestamp": datetime.now(timezone.utc).isoformat()
        })
        
    except Exception as e:
        logger.error(f"OTP verification failed: {e}")
        # 🔄 自動重試機制
        return await retry_handler.with_retry(
            enhanced_verify_otp, mandate_id, otp_code, user_id
        )

為什麼要使用 AP2 來做電商整合

我之前也有試過很多不同的電商 API 整合方案,但是說實話,大部分都有一些問題。要不就是安全性有疑慮,要不就是整合起來很麻煩。AP2 協議讓我覺得眼睛一亮的地方,主要有以下幾點:

跟 Google ADK 完美整合

如果你之前有玩過 Google ADK (Agent SDK) 的話,就會知道它跟 Gemini 模型的整合非常順暢。AP2 基本上就是為了這個生態系統設計的,所以你可以很輕鬆地:

  • 直接使用 Gemini-2.5-flash 模型來處理用戶的購物需求
  • 自動化的意圖識別,用戶說「我想買 iPhone」就會自動轉到購物 Agent
  • 多語言支援,中英文都沒問題

開發效率真的很高

說實話,如果你要自己從零開始做一套完整的電商支付系統,那真的是會累死。AP2 提供了:

# 基本上概念就是這樣,很簡單(這是簡化示意)
shopping_agent = Agent(
    name="ap2_shopping_assistant",
    model="gemini-2.5-flash",
    tools=[
        search_products,
        get_product_details,
        create_cart_mandate,
        get_shopping_recommendations
    ]
)

注意:上面是概念性的簡化示例。實際專案中我們使用了企業級的 factory pattern 來創建 agents,詳細實作請見下方的「Enhanced Shopping Agent」和「Enhanced Payment Agent」章節。

  • 不需要重新造輪子,協議都幫你定義好了
  • 標準化的 API 介面,跟不同支付服務商整合都是同一套邏輯
  • 內建的安全機制,不用擔心被駭客攻擊

安全性考量很完整

因為涉及到真的要付錢,所以安全性絕對是最重要的。AP2 在這方面做得很不錯:

  • OTP 雙重認證,每筆交易都要驗證碼確認
  • 數位簽章技術,防止交易被篡改
  • 完整的 audit trail,所有交易都有記錄可查

我在測試的時候,還特別試了一下如果 OTP 輸入錯誤會怎樣:

# 最多只能試 3 次,超過就會被鎖定
if otp_data["attempts"] > 3:
    del _otp_store[mandate_id]
    return json.dumps({
        "error": "Too many attempts",
        "status": "blocked"
    })

🏗️ LINE Bot 架構升級實作

這次的升級我完全重新設計了整個系統架構,從原本的三個基本 Agent 升級成企業級的模組化架構。

🛍️ Enhanced Shopping Agent - 企業級購物助手

新的購物 Agent 不只是基本功能,而是具備完整企業級特性:

# src/linebot_ap2/agents/enhanced_shopping_agent.py
def create_enhanced_shopping_agent(model: str = "gemini-2.5-flash") -> Agent:
    """創建增強型購物 Agent,具備企業級功能"""
    
    return Agent(
        name="enhanced_shopping_agent",
        model=model,
        description="""Advanced shopping assistant with comprehensive product search, 
        cart management, and AP2-compliant payment mandate creation.""",
        
        instruction="""You are an intelligent shopping assistant with advanced capabilities:
        
        🛍️ **Core Shopping Functions:**
        - **Product Search**: Use enhanced_search_products with filters (category, price range, brand)
        - **Product Details**: Get comprehensive info with enhanced_get_product_details  
        - **Recommendations**: Provide personalized suggestions with enhanced_get_recommendations
        - **Cart Management**: Add items and manage shopping carts with enhanced_add_to_cart
        
        🔐 **AP2 Payment Integration:**
        - **Secure Mandates**: Create signed payment mandates with enhanced_create_cart_mandate
        - **Transaction Security**: All mandates use HMAC-SHA256 signatures for AP2 compliance
        - **Audit Trail**: Full transaction logging and verification""",
        
        tools=[
            enhanced_search_products,      # 🔍 增強型商品搜尋
            enhanced_get_product_details,  # 📋 詳細商品資訊  
            enhanced_create_cart_mandate,  # 🛒 AP2 合規購物車
            enhanced_get_recommendations,  # 🎯 智能推薦
            enhanced_add_to_cart,         # ➕ 購物車管理
            get_product_categories,       # 📂 商品分類
            get_shopping_cart            # 🛍️ 購物車查詢
        ]
    )

💳 Enhanced Payment Agent - 企業級支付處理

新的支付 Agent 具備完整的企業級安全機制和 AP2 合規性:

# src/linebot_ap2/agents/enhanced_payment_agent.py
def create_enhanced_payment_agent(
    model: str = "gemini-2.5-flash",
    max_otp_attempts: int = 3,
    otp_expiry_minutes: int = 5
) -> Agent:
    """創建增強型支付 Agent,具備企業級安全性"""
    
    return Agent(
        name="enhanced_payment_agent",
        model=model,
        description="""Advanced payment processor with AP2 compliance, enhanced security 
        features, and comprehensive error handling.""",
        
        instruction=f"""You are a secure payment processing agent with advanced capabilities:

        🔐 **Security & Compliance:**
        - **AP2 Protocol**: Full compliance with Agent Payments Protocol standards
        - **OTP Security**: Maximum {max_otp_attempts} attempts, {otp_expiry_minutes}-minute expiry
        - **Encryption**: AES-256 encryption for all payment data
        - **Audit Trail**: Complete transaction logging and monitoring

        💳 **Payment Processing:**
        1. **Payment Methods**: Show available methods with enhanced_get_payment_methods
        2. **Payment Initiation**: Secure processing with enhanced_initiate_payment
        3. **OTP Verification**: Guide users through enhanced_verify_otp process
        4. **Transaction Status**: Real-time updates with enhanced_get_transaction_status
        5. **Refund Processing**: Handle refunds with enhanced_process_refund

        🛡️ **Security Features You Must Explain:**
        - **Mandate Signing**: HMAC-SHA256 signatures ensure transaction integrity
        - **OTP Protection**: Time-limited codes prevent unauthorized access
        - **Fraud Detection**: Real-time monitoring and risk assessment
        - **Data Protection**: PCI DSS Level 1 compliance""",
        
        tools=[
            enhanced_get_payment_methods,    # 🔍 增強型支付方式
            enhanced_initiate_payment,       # 💰 安全支付發起
            enhanced_verify_otp,            # 🔐 強化 OTP 驗證
            enhanced_get_transaction_status, # 📊 交易狀態追蹤
            enhanced_process_refund,        # 💸 退款處理
            get_mandate_details,           # 📋 Mandate 詳情
            cleanup_expired_data          # 🧹 過期資料清理
        ]
    )

🔄 Circuit Breaker 自動故障恢復機制

這是企業級系統必備的穩定性機制:

# src/linebot_ap2/common/retry_handler.py
class CircuitBreaker:
    """Circuit Breaker 實現自動故障恢復"""
    
    def __init__(self, failure_threshold: int = 5, timeout: int = 60):
        self.failure_threshold = failure_threshold
        self.timeout = timeout
        self.failure_count = 0
        self.last_failure_time = None
        self.state = "CLOSED"  # CLOSED, OPEN, HALF_OPEN
    
    def __enter__(self):
        if self.state == "OPEN":
            if time.time() - self.last_failure_time > self.timeout:
                self.state = "HALF_OPEN"
                logger.info("Circuit breaker: HALF_OPEN -> 嘗試恢復")
            else:
                raise CircuitBreakerOpenException("🚫 Circuit breaker is OPEN")
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is None:
            # 🟢 成功執行
            if self.state == "HALF_OPEN":
                self.state = "CLOSED"
                self.failure_count = 0
                logger.info("Circuit breaker: HALF_OPEN -> CLOSED (恢復成功)")
        else:
            # 🔴 執行失敗
            self.failure_count += 1
            self.last_failure_time = time.time()
            
            if self.failure_count >= self.failure_threshold:
                self.state = "OPEN"
                logger.warning(f"Circuit breaker: CLOSED -> OPEN (失敗次數: {self.failure_count})")

支付流程大概是這樣:

  1. 用戶確認要付款
  2. 系統產生 OTP 驗證碼
  3. 用戶輸入驗證碼
  4. 驗證成功後完成交易
def initiate_payment(mandate_id: str, payment_method_id: str, user_id: str) -> str:
    # 產生 6 位數 OTP
    otp = f"{random.randint(100000, 999999)}"
    
    # 儲存 OTP (5 分鐘有效期)
    _otp_store[mandate_id] = {
        "otp": otp,
        "user_id": user_id,
        "expires_at": datetime.now() + timedelta(minutes=5),
        "attempts": 0
    }

🤖 智能意圖識別系統

這個是我覺得最有趣的部分,系統會自動判斷用戶想要做什麼:

def determine_intent(message: str) -> str:
    message_lower = message.lower()
    
    # 購物關鍵字
    shopping_keywords = [
        'buy', 'purchase', 'shop', 'product', 
        '買', '購買', '商品', '產品', '購物',
        'iphone', 'macbook', 'airpods'  # 商品名稱
    ]
    
    # 支付關鍵字  
    payment_keywords = [
        'pay', 'payment', 'checkout', 'otp',
        '付款', '支付', '結帳', '驗證'
    ]
    
    # 檢查關鍵字並回傳對應意圖
    for keyword in payment_keywords:
        if keyword in message_lower:
            return 'payment'
            
    for keyword in shopping_keywords:
        if keyword in message_lower:
            return 'shopping'
            
    return 'shopping'  # 預設為購物

📱 LINE Bot 整合的部分

最後是 LINE Bot 的整合,主要在 main.py 裡面:

@app.post("/")
async def handle_callback(request: Request):
    # 處理 LINE webhook
    for event in events:
        if event.message.type == "text":
            msg = event.message.text
            user_id = event.source.user_id
            
            # 判斷意圖並路由到對應 Agent
            intent = determine_intent(msg)
            response = await call_agent_async(msg, user_id, intent)
            
            reply_msg = TextSendMessage(text=response)
            await line_bot_api.reply_message(event.reply_token, reply_msg)

每個用戶都會有獨立的 session,所以可以保持對話的上下文:

async def get_or_create_session(user_id):
    if user_id not in active_sessions:
        session_id = f"session_{user_id}"
        await session_service.create_session(
            app_name=APP_NAME, user_id=user_id, session_id=session_id
        )
        active_sessions[user_id] = session_id
    return active_sessions[user_id]

🔍 如何驗證自己是符合 AP2

這一段大概是我寫(Vibe Coding) 完成後,一直無法完全確定是否是正確的部分。以往一直會以為要符合相關的 Protocol ,一定要使用到相關的套件 SDK ,也就是一定要用到 AP2 的套件才算合規。這邊也是完整對過 Spec 跟討論之後的結果。

AP2 不需要特別的 SDK,只要符合協議規範即可:

  1. ✅ 協議合規 - 你的實作完全符合 AP2 標準

    1. ✅ 數位簽章合規 - HMAC-SHA256 算法(AP2 標準)

      ✅ 生命週期管理 - 創建時間、過期時間、狀態追蹤

      ✅ 安全驗證 - OTP 機制、簽章驗證

      ✅ 審計軌跡 - 完整的交易記錄和狀態追蹤

      ✅ 資料完整性 - 必要欄位驗證、格式標準化

      ✅ 狀態管理 - 符合 AP2 的狀態流轉規則

  2. ✅ 資料格式正確 - Cart/Payment Mandate 格式符合規範

    這邊列出相關程式碼:

    def create_cart_mandate(product_id: str, quantity: int = 1, user_id: str = "") -> str: 
      cart_mandate = {
         "mandate_id": mandate_id,           # ✅ AP2 必要欄位
          "type": "cart_mandate",             # ✅ AP2 協議類型
         "user_id": user_id,                 # ✅ 用戶識別
          "items": [{                         # ✅ 商品清單
              "product_id": product_id,
              "name": product["name"],
              "price": product["price"],
              "quantity": quantity,
              "subtotal": total_amount
          }],
          "total_amount": total_amount,       # ✅ 總金額
          "currency": product["currency"],    # ✅ 貨幣類型
          "created_at": datetime.now().isoformat(),  # ✅ 建立時間
          "status": "pending_payment"         # ✅ 狀態
      }
    

    關於 CartMadate 的資料結構:

      # 創建結構化資料
      mandate = CartMandate(
          mandate_id=mandate_id,              # ✅ 唯一識別碼
          user_id=user_id,                    # ✅ 用戶ID
          items=cart_items,                   # ✅ 商品清單 (CartItem 物件)
          total_amount=total_amount,          # ✅ 總金額
          currency=currency,                  # ✅ 貨幣
          created_at=datetime.now(),          # ✅ 建立時間
          expires_at=datetime.now() + timedelta(...),  # ✅ 過期時間
          status=PaymentStatus.PENDING        # ✅ 狀態
      )
           
        class CartMandate(BaseModel):
          mandate_id: str                         # ✅ 必要:mandate 識別碼
          type: str = "cart_mandate"              # ✅ 必要:AP2 類型標識
          user_id: str                           # ✅ 必要:用戶識別
          items: List[CartItem]                  # ✅ 必要:商品清單
          total_amount: float                    # ✅ 必要:總金額
          currency: str = "USD"                  # ✅ 必要:貨幣類型
          created_at: datetime                   # ✅ 必要:建立時間
          status: PaymentStatus                  # ✅ 必要:處理狀態
          expires_at: Optional[datetime]         # ✅ 可選:過期時間
    
  3. ✅ 安全機制完整 - HMAC-SHA256 簽章、OTP 驗證 附上相關程式碼

      payload = {
          "mandate_id": mandate.mandate_id,   # ✅ 簽章內容
          "user_id": mandate.user_id,
          "total_amount": mandate.total_amount,
          "currency": mandate.currency,
          "items_count": len(mandate.items),
          "timestamp": timestamp,
          "nonce": nonce
      }
         
      # ✅ HMAC-SHA256 簽章 (AP2 標準)
      signature = hmac.new(
          self.secret_key.encode('utf-8'),
          payload_string.encode('utf-8'),
          hashlib.sha256
      ).hexdigest()
    
  4. ✅ 工作流程正確 - 購物→支付→驗證的完整流程

🔧 企業級升級踩坑經驗分享

1. Pydantic v2 升級挑戰

升級到企業級架構時遇到的第一個問題是 Pydantic v2 的導入變更:

# ❌ 舊版本 (Pydantic v1)
from pydantic import BaseSettings

# ✅ 新版本 (Pydantic v2) 
from pydantic_settings import BaseSettings

# 解決方案:更新 pyproject.toml
dependencies = [
    "pydantic>=2.5.0",
    "pydantic-settings>=2.1.0",  # 新增這個套件
]

2. Enhanced Session 管理升級

原本的 session 管理太簡單,企業級版本加入了完整的監控和清理機制:

# src/linebot_ap2/common/session_manager.py
class EnhancedSessionManager:
    """企業級 Session 管理,具備清理和監控功能"""
    
    async def get_or_create_session(self, user_id: str) -> str:
        """取得或創建用戶 session,具備自動清理機制"""
        
        if user_id not in self.active_sessions:
            session_id = f"session_{user_id}_{int(time.time())}"
            
            # 🧹 清理過期 session
            await self.cleanup_expired_sessions()
            
            # 📊 記錄 session 創建
            logger.info(f"Creating new session for user: {user_id}")
            
            await self.session_service.create_session(
                app_name=self.app_name,
                user_id=user_id,
                session_id=session_id
            )
            
            self.active_sessions[user_id] = {
                "session_id": session_id,
                "created_at": datetime.now(timezone.utc),
                "last_activity": datetime.now(timezone.utc)
            }
        
        # 更新最後活動時間
        self.active_sessions[user_id]["last_activity"] = datetime.now(timezone.utc)
        return self.active_sessions[user_id]["session_id"]

3. Circuit Breaker 整合挑戰

實作 Circuit Breaker 時發現 decorator 模式太複雜,簡化為 context manager:

# ❌ 原本想用複雜的 decorator
@retry_handler.with_circuit_breaker()
async def some_function():
    pass

# ✅ 改用簡單的 context manager
async def enhanced_verify_otp(...):
    try:
        with retry_handler.circuit_breaker():
            # 執行 OTP 驗證邏輯
            result = await process_otp_verification(...)
            return result
    except CircuitBreakerOpenException:
        return json.dumps({
            "error": "Service temporarily unavailable",
            "status": "circuit_breaker_open",
            "retry_after": 60
        })

4. AP2 HMAC 簽章的正確實作

一開始對 AP2 的數位簽章規範理解不夠深入,後來研究官方文件才知道:

def _prepare_signable_data(self, mandate_data: Dict[str, Any]) -> str:
    """準備用於簽章的資料,必須按照 AP2 規範排序"""
    
    # 🔐 重要:必須按照特定順序排列欄位
    signable_fields = [
        "mandate_id", "type", "user_id", "total_amount", 
        "currency", "created_at", "expires_at"
    ]
    
    # 過濾並排序欄位
    filtered_data = {
        key: mandate_data[key] 
        for key in signable_fields 
        if key in mandate_data
    }
    
    # 產生可簽章的字串
    return "|".join([f"{k}={v}" for k, v in sorted(filtered_data.items())])

🚀 結語與未來方向

整合完才會知道,其實 VP2 並不是一整套的 SDK 或是框架,而是一個協定與相關規範。讓每一個組織都能夠打造出自己的 Payment Protocol 而不需要依賴其他人。

以下是未來的一些方向

進階 AI 功能

  • 多模態搜尋: 圖片、語音、文字混合搜尋
  • 個人化推薦: 基於用戶行為的 ML 推薦引擎
  • 對話式客服: 整合 Gemini Pro 處理複雜查詢

希望這篇文章能幫助大家了解 AP2 協議的實作方式。如果你對程式碼有任何問題,歡迎到 GitHub 專案 留言討論,也歡迎大家 fork 回去改進!

記得如果覺得有用的話,給個 ⭐ Star 支持一下 :)

[數位憑證皮夾] 入門版 - 如何創立一個新的數位憑證與驗證方式,應用場景為一間公司的 HR 補助申請(附上範例修改程式重點)

image-20251009102618401

(圖片來源: 數位憑證皮夾官方網站)

前提:

數位憑證皮夾是近幾年數發部推動的一個主要政策,希望是透過數位憑證皮夾來取代大大小小的證件、會員卡跟相關的實體晶片卡片。數位憑證皮夾可以是一單一的一個 App ,甚至可以是讓所有的 App 都來當數位憑證皮夾。

這一篇文章稍微解釋數位憑證皮夾的使用方法,還有如何應用數位憑證皮夾,透過一個場景來建立一個數位憑證的發行方與數個認證方。

先來玩一下數位憑證皮夾

首先你需要下載數位憑證皮夾的官方 App (目前 iOS 是 TestFlight 版本)

image-20251009103231602

下載之後,你會發現好像裡面空空的。那是因為你還沒有建立你自己的卡片。

測試記者會的相關流程

打開 App 會看到有以下

image-20251009113551533

建議可以先加入

  • OTP 電子卡(需要輸入電話,收簡訊)
  • 駕照電子卡(可以輸入測試資料沒問題)

加入了相關憑證後,就可以透過出示憑證來測試。 建議可以走「超商取貨」的範例來測試一下。

image-20251009114128661

可以看到,超商領貨需要兩種資料,是可以從兩種數位憑證上面取得。

  • 駕照卡 -> 姓名
  • OTP卡 -> 電話

然後都不需要其他的欄位,這就是選擇性揭露的原則。

數位豆泥卡範例 Web App

網址: https://mashbeanvc.tonyq.org/

Google Chrome 2025-10-09 17.39.55

這個 Web App 作為展示有以下兩個主要功能:

  • 申請一張豆泥卡(只需要暱稱,生日是選填)
  • 幫豆泥點蠟(也就是驗證的意思)

這個範例也充分了應用以下主要 API :

打造一個簡單數位憑證驗證場景吧 (HR數位員工卡系統)

Avatar

接下來要做一個給 HR 的數位員工卡系統,需要有以下的相關功能:

  • 連線小學堂的同仁可以自行申請一張員工卡(輸入: 姓名、英文名字、出生年月日、入職年份、養育小孩數字)
  • 可以申請以下兩種補助:分別是育兒補助跟體育補助兩種。
  • 育兒補助條件:
    • 英文名字(作為帳戶匯款用)
    • 需要入職滿一年
    • 需要有一個小孩以上
  • 體育補助:
    • 需要入職滿一年
    • 英文名字(作為帳戶匯款用)

以上就是一個使用數位憑證皮夾的系統的假設場景,接下來要來說明要如何打造。

透過沙盒系統來設計與發行相關數位憑證

申請沙盒相關流程:

發行數位憑證

(可以參考官方的發行端使用手冊)

到了 發行端沙盒系統 (負責建立發行數位憑證) 透過「建立VC模板」

image-20251010132418021

建立以下的數位憑證

Google Chrome 2025-10-10 13.12.08

其中有一些資料需要記住:

  • 序號
  • 證件類型(credentialType)

這邊可以透過「產生 VC 資料」來輸入一個新的資料。

image-20251010132543934

這樣就會產生一個 QR Code 並且可以讓你匯入到數位錢包之內。

驗證數位憑證

(可以參考官方的驗證端使用手冊)

這邊解釋一下,如何建立一個運動補助的數位憑證驗證的方法,首先來複習一下「運動補助」的條件有哪些:

體育補助條件:

  • 需要入職滿一年
  • 英文名字(作為帳戶匯款用)

接下來,你可以到 「建立 VP 」經過以下流程來創立一個。

image-20251010133429458

  • 輸入 VP 的名字

image-20251010133415287

  • 挑選你需要的群組名稱,

  • 挑選 VC 資料,我這邊就挑選剛剛建立的「連線小學堂」也就是挑選你剛剛建立的數位憑證樣板。

image-20251010133450566

  • 挑選要驗證的數位憑證欄位
  • 因為運動補助只需要知道「入職時間」跟「英文名字」就可以,就不需要挑選其他的資料欄位。

最後就會出現一個 QR Code 看做為驗證使用。

透過 SwaggerUI : 來了解各種 API 的使用方法

接下來透過官方提供的 SwaggerUI 介面,與一些 API 來跟大家講如何找到這些變數。

發行端 SwaggerUI 參數說明

網址: 發行端 SwaggerUI

輸入 API_Key (Access Token)

image-20251010143636547

這個資料在當初註冊沙盒的時候發送兩封信件之一 : 「【數位憑證皮夾】發行端沙盒系統_帳號啟用通知」。

發行端產生 QR Code /api/qrcode/data

image-20251010144626368

這邊會需要輸入一個參數 vcUid ,請去 發行端沙盒系統 (負責建立發行數位憑證) 找到你發行的數位憑證,點下編輯即可看到相關資訊。

image-20251010145116814

這個 /api/qrcode/data 會需要有資料欄位,這邊可能會比較不容易在介面上一個個填寫。可以先用 /api/qrcode/nodata 來測試。

特別說明 - /api/vc-item-data

但是程式碼中會使用到的 APO /api/vc-item-dataSwaggerUI 沒有出現。

相關資訊如下: (以我上面資訊舉例)

  • vcId: 607861
  • vcCiD: 0028680530_line_school
curl -X 'POST' \
  'https://issuer-sandbox.wallet.gov.tw/api/vc-item-data' \
  -H 'accept: */*' \
  -H 'Access-Token: YOUR_ACCESS_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
  "vcId": 607861,
  "vcCid": "0028680530_line_school",
  "fields": [
    {
      資料省略
    },
}'

驗證端 SwaggerUI 參數說明

網址: 驗證端 SwaggerUI

關於 Authorize 的流程跟發行端相同,就跳過。

產生驗證端的 QR Code /api/oidvp/qrcode

這邊會需要有兩個參數

Microsoft PowerPoint 2025-10-10 15.17.10

  • transactionId: 這個需要一個 UUID 的字串,可以用 SwaggerUI 上面原本數值來修改一下即可。

如果資料成功,會出現以下相關資料:

{
  "transactionId": "104158f9-b1dc-4f76-847e-86f6af36d917",
  "qrcodeImage": "data:image/png;base64,...",
  "authUri": "modadigitalwallet://authorize?..."
}

其中:

  • transactionId: 就是你填寫得資料
  • qrcodeImage: base64 的圖片
  • authUri: 就是一個 deeplink 可以開啟 iOS App 數位錢包的 App 並且執行相關的驗證。

最後: 來修改程式與使用相關的參數

image-20251010142916050

接下來程式碼放在這個地方: https://github.com/kkdai/did-usecase-HR ,但是我們先透過發行端跟驗證端的 SwaggerUI 介面來跟大家分享一下,該使用哪些資訊。

各位可以查詢 .env.example 可以看到相關說明 (網址)

# 卡片序號,從發行後台取得
VC_SERNUM=YOUR_VC_SERNUM

# 卡片樣板代號,從發行後台取得
VC_UID=YOUR_VC_UID

# 發行者存取權杖,從發行後台取得
ISSUER_ACCESS_TOKEN=YOUR_ISSUER_ACCESS_TOKEN

# 驗證器參考碼
VERIFIER_SPORT_REF=YOUR_VERIFIER_REF_FOR_SPORT_SUBSIDY
VERIFIER_PARENT_REF=YOUR_VERIFIER_REF_FOR_PARENT_CHECK

# 驗證器存取權杖
VERIFIER_ACCESS_TOKEN=YOUR_VERIFIER_ACCESS_TOKEN

裡面的 VC_SERNUM 就是剛才提到的 vcId 。 其他就可以快速理解才對。

範例使用方式:

可以本地端執行測試一下

建立員工卡

image-20251010152639239

可以快速輸入資料,並且產生一張員工卡。

申請運動補助

image-20251010152718672

點選 運動補助(驗證) 就可以進入申請運動補助,並且透過掃描 QR Code 來傳輸需要的資料。

直接線上體驗?

可以直接打開這個網址來測試。

總結與未來展望

這是一個數位憑證沙盒的展示場景與 demo program ,大部分程式碼還是透過 TonnyQ 當初打造出來的樣板修改的。主要是希望讓大家可以對於數位憑證沙盒能有基礎的理解,這樣的應用場景只會是許多有創意中的起點。很期待可以看到許多有趣的想法與應用場景在未來的應用上。

[好書分享] 折疊者思維 - 做個好軍師,將領導者天馬行空的發想落實,成為不可或缺的得力助手

折疊者思維
做個好軍師,將領導者天馬行空的發想落實,成為不可或缺的得力助手
作者: 設樂悠介  原文作者: Shidara Yusuke  
譯者: 莊雅琇  
出版社:時報出版 

買書推薦網址:

前言:

這是 2025 年第 8 本讀完的書。當初買到這本書,因為在公司內部我經常擔任著協助許多專案去「折疊」的角色。所以想來看看這位作者說的角色究竟是如何扮演。

大綱

幻冬舍社長見城徹欽點新事業主管,與知名編輯箕輪厚介共同打造最高業績團隊的重要推手--設樂悠介,傾囊相授他二十年來成為最佳折疊者的實戰經驗。

看他如何從一名將編輯立為職志的新鮮人,誤打誤撞從小業務員開始,一步步用「折疊者思維」在不到四十歲即成為幻冬舍Comics的董事、編輯本部內容產業局副局長。

「折疊」與「攤開」的由來

在日本有名的日文國語辭典《廣辭苑》中提到【攤開包袱巾】(大風呂敷を広げる),是指「說話或做事不切實際,說大話的意思。」所以本書將其稱為「攤開」者,也就是把工作創意從無到有發想出來的人。

而【折疊包袱巾的人】(風呂敷畳み人)是相對的角色,作者在所主持的節目《折疊包袱巾的人廣播電台》中,稱其為「折疊」者,指讓創意聚焦、穩定執行的人。

彼得•杜拉克說:策略是平價商品,執行力乃是藝術。
學會「摺疊者思維」,累積執行力,方能在最終做到自己的想望!
折疊者:是指具體實踐工作創意,並且穩步實行的人。他可以說是領導者(攤開者)身邊的「軍師」或「得力助手」。

《折疊者思維》重點整理

本書以「折疊者」(負責具體實踐創意、穩步執行的軍師型角色)與「攤開者」(提出天馬行空想法的領導者)為核心概念,探討商務領域中折疊者的重要性、角色、心態與實務技巧。以下根據劃線內容,按章節或主題小節整理重點,聚焦於定義、角色、團隊管理、溝通、時間管理及個人經驗。

什麼是「折疊者」?

  • 折疊者源自「折疊包袱巾的人」(風呂敷畳み人),在商務中指具體實踐攤開者(提出創意者)的想法,並穩步執行的人。折疊者是領導者身邊的「軍師」或「得力助手」,負責將抽象願景轉化為可行行動。
  • 為什麼「折疊者」如此重要?許多團隊缺乏將創意具體實現的實行能力與經驗,甚至連指揮人才也不足。折疊者填補此空白,確保專案順利推進。

對「攤開者」的創意產生共鳴並樂在其中

  • 折疊者應從一開始就對攤開者的創意表示興趣,成為支持者而非評論家。尋找創意中的有趣之處,心想「說不定有潛力」,才能發自內心提出正面意見。
  • 想出創意的人最初排斥否定,因此折疊者首要工作是與攤開者一起樂在其中,從構想階段就響應支持。

先支持攤開者的想法,再適時修正

  • 反駁或建議前,先對創意表示感興趣,成為同盟。這是折疊者的首要之務,避免一開始就否定。
  • 攤開者詢問看法時,首先表達興趣;之後再提出修正,確保創意順利發展。

請努力成為世界上最了解攤開者的人

  • 折疊者是最能與攤開者近距離交流的人,應成為最了解攤開者的人。這有助推動團隊,並是折疊者應有的心態。
  • 成為攤開者堅強的夥伴,與他共享祕密:對風險高的創意表示興趣,與攤開者協力實行;若攤開者徬徨,則聆聽心聲,提供盟友支持。

折疊者要察覺且預想所有的風險

  • 折疊者負責替攤開者設想各種風險,再付諸實行,讓攤開者全心發想創意。
  • 看準機會,隨時把掌控權拿到手:折疊者可在實行過程中視情況控制攤開者,將創意融入專案。痛快之處在於「成功反擊」的那一刻(比喻為反擊,非真實衝突)。

成為宣揚攤開者熱情的傳教士與翻譯

  • 折疊者身為最理解攤開者的人,負責將攤開者的熱忱翻譯給團隊成員。
  • 除了熱忱,還需補充明確指示,讓第一線成員能付諸實行。攤開者的願景常龐大抽象,折疊者將其具體傳達,提升團隊執行力。

成為最能理解團隊成員的人

  • 折疊者也需成為最理解團隊成員的人,掌握各成員的工作情況與狀態,並視情況將意見傳達給攤開者。
  • 平時多聽團隊心聲,定期安排一對一溝通,建立「打開天窗說亮話」的關係(如成員先私下分享想法)。讓成員知曉自己被關心與理解。
  • 下令「Do」的是攤開者,傳達「How」的是折疊者:折疊者指導成員最適切方式,提升團隊效率。

選擇團隊成員最重要的是熱忱

  • 建立團隊關鍵在「人」,招募階段優先召集對專案產生共鳴的夥伴。技能重要,但「共鳴」與「熱忱」才是成敗關鍵,人會竭盡全力實現內心渴望。
  • 本來就不會有100%的完美團隊:專案規模越大,成員越多,困擾越顯(如提升表現)。年輕時可動用「犯規」手段(如強力推動),但需依賴優秀團隊。

成功是團隊成員的功勞,失敗是自己的責任

  • 上司搶功是犯規行為,折疊者不可如此。分辨好上司的關鍵:是否做出適當決定、獲得下屬信賴。
  • 一個好的折疊者,一定會成為好的攤開者:曾任折疊者者具備技能,能盡情揮灑創意。作者經驗:上司長期企劃中任折疊者,電子書業務則任攤開者;與夥伴創立事業時自告奮勇任折疊者,《新經濟》則任徹底攤開者。專案少不了折疊者,缺席時需身兼兩職,極為吃力。

專欄 我就這樣成為「折疊者」4:遇見好上司好戰友,成就了優秀的折疊者

  • 遇見好上司(如常問「你們想做什麼?」)與戰友(如箕輪),成就優秀折疊者。平時交流新事業想法,組成團隊,互相支持。

溝通1 禮貌是工作上的「高CP值武器」

  • 拜託工作後,即使瑣事也道謝,是構築和諧人際的關鍵。緊急時更不可忘,否則下次委託易遭推託(情緒影響行為)。
  • 打招呼是基本且值回票價的舉動,在公司內主動朝氣十足地打招呼,日積月累讓人際更和諧。

溝通3 明白上司真正的想法再執行

  • 收到指示後,不要只言聽計從,需發揮想像力思考「上司的目的為何」,正確解讀深意。

時間管理4 按照緩急程度列代辦清單

  • 先分成四大類,再製待辦清單。若太瑣碎,可按時間順序分「現在立刻」、「稍後再做」、「長期來看」三大類。
  • 可設多頁管理,或在單一清單用記號區隔(省換頁,一目了然,易移動分類)。

心得

先來定義一下,這邊提出的「折疊者」角色,有一點類似「專案/ 產品推動者」。需要完成產品與專案開發與推動中的所有相關的事項,並且能完成「展開者」(也就是專案發起與主要發起者)的工作。

經過不少年的工作經驗,你會發現許多工程師其實都是相當具有想像力的(我自己也是),但是往往能把一些日常瑣事完成的卻相當的少。這樣也就造成了,許多有創意的點子或是產品被扼殺在一開始、許多好的想法在許多公司內的規範下被拒絕掉了。

但是偏偏我又是一個相當鐵頭的人,只要大家告訴我不行的事情,如果我認回很重要,我就會一路往上跑道儘量能申訴的地方為止。也就是除非是公司高層告訴我不能做,或是給我一個理由不可以去執行。不然我不會去聽信一個平常操作端的人因為想要便宜行事的回覆。

「折疊者」這一本書也就是圍繞著這樣角色的一本書籍,也希望每一個想要「將事情做好而非將事情做完」的人,能夠好好的閱讀。

[好書分享] 關鍵七秒,決定你的價值

關鍵七秒,決定你的價值
國際非語言溝通專家教你練就不經思考,秒現有自信、魅力與競爭力的「行為履歷」
The Million Dollar Handshake : The ultimate guide to revolutionise how you connect and communicate in business and life
作者: 凱薩琳‧ 莫洛伊  譯者: Catherine Molloy  林吟貞  
出版社:采實文化 

買書推薦網址:

前言:

這是 2025 年第 7 本讀完的書。這一本書當初買的時候,是因為當時很盛行所謂的 電梯推廣 (elevator pitch)。所以看到這一本書的封面,就想透過這一本書來學習如何快速的可以讓不熟悉你的人有著良好的印象。

大綱

你的「行為履歷」如影隨形,七秒就決定大家對你的評價!
翻轉形象、創造優勢、贏得好口碑,從微調「行為履歷」開始!

國際非語言溝通專家凱薩琳.莫洛伊,擁有超過25年的銷售與培訓實戰經驗,
22歲時就拿下銀行業務全國的銷售冠軍,後來接下培訓的工作。
讓她思考,為什麼自己可以馬上跟他人建立有力的關係,而其他人卻不行。

於是,她開始研究肢體語言、NLP、心理學等領域,
研究指出,當人在做出決定時,
潛意識會展現「行為履歷」,直到大腦有意識前,會延遲七到九秒,
而她發現自己就是掌握了這七秒鐘,因此決定了自己價值的決勝關鍵。

無論是微表情、微動作、各種姿勢和聲音……
這些無意識的非語言溝通,就是所謂的「行為履歷」,
都影響著我們的生活、職場等生涯大事,
如果不懂掌握自己的「行為履歷」,就算你的履歷寫得再完美,也沒用!

因此,只要把握這短短的關鍵七秒,
不僅能因此提升自信、減緩緊張情緒,還能提高你的魅力值,
讓你無往不利,到哪裡都大受歡迎、強化競爭力──

1. 良好溝通的兩要素:有意識傾聽與提問

重點:

  • 主動傾聽與提問:良好的溝通建立在有意識地傾聽對方並提出適當問題的基礎上,這有助於建立連結與理解。
  • 扮演主人角色:在社交場合中,假想自己是活動主人,能減輕自身壓力,轉而專注於關心他人,提升互動品質。
  • 融入社交場合:若獨自一人,可主動尋找其他獨自參加者,或加入小團體,通過分享食物(如遞送起司托盤)或在自助餐排隊時與人交談,來開啟對話。
  • 禮貌結束對話:在社交活動中,若需離開對話,可微笑著表示享受對話但需去見其他人,這是一種不冒犯的自然方式,因為這是社交場合的常態。

2. 致勝細節藏在名片裡

重點:

  • 名片的價值:接受他人名片時應表現尊重,即使不感興趣,也應收下並小心存放(如放入名片盒或外套口袋),因為名片是建立潛在客戶或未來合作機會的工具。
  • 第一印象的重要性:你只有七秒鐘留下好印象,若首次印象不佳,需後續十二次積極互動來修補關係。多次接觸有助於逐漸建立信任與好感。
  • 文化差異的非語言溝通:不同文化對肢體語言的解讀不同。例如,澳洲點頭表示同意,搖頭表示不同意;而在某些阿拉伯國家,低頭表示不同意,抬頭表示同意;希臘點頭可能表示不同意;泰國、寮國、菲律賓則以頭後仰表示同意。理解這些差異有助於跨文化溝通。

3. 認知會影響心態

重點:

  • 自我賦能:你的思想具有強大力量,沒有人能改變你的想法或感受。通過觀看勵志影片、閱讀書籍,強化正面心態,並有意識地提升溝通技巧,為成功奠定基礎。
  • 避免自我毀滅:與自我領導相反,自我毀滅是指將失敗歸咎於他人,認為自己無法控制行為或心態。應積極掌控自己的心態與行動。
  • BLAST法則:這是一套處理客戶或員工問題的實用方法:
    1. 相信(Believe):相信對方的說法,因為這是他們的真實感受。
    2. 傾聽(Listen):積極傾聽並提問以理解需求。
    3. 行動(Action):採取行動解決問題。
    4. 滿足(Satisfy):確認對方是否滿意。
    5. 感謝(Thank):感謝對方的反饋或參與。
  • 正面肢體語言的影響:微笑等正面肢體語言具有吸引力,能讓他人對你產生好感。例如,微笑會引發他人回以笑容,增強互動的正面氛圍。
  • 目標視覺化:在年底寫一封信給自己,假設未來一年的目標已達成,具體寫下至少三件期望實現的事情(如新房子、合作新客戶、公益計畫成功)。這種視覺化能強化積極心態並促進目標實現。

4. 肢體語言很主觀,得更細心觀察

重點:

  • 七秒鐘的非語言印象:在初次見面時,通過以下七種非語言表達方式留下好印象:
    1. 面帶微笑,展現親和力。
    2. 調整積極的態度,散發正能量。
    3. 保持直挺姿勢,顯得自信且有能力。
    4. 維持適當的眼神接觸,建立信任。
    5. 使用開放式手勢,避免雙臂或雙腿交叉,顯示開放態度。
    6. 放鬆呼吸,保持冷靜與自信。
    7. 運用「百萬之握」(堅定有力的握手),傳達專業與熱情。
  • 細心觀察的必要性:肢體語言因人而異,需細心觀察對方的反應,以確保正確解讀並適當調整自己的非語言表達。

心得

人是一個很注重第一印象的動物,而這一本書強化你應該要如何做好這件事情。 有很多的方向與方法提供給讀者,都相當的實用。就算你第一次失敗了,雖然需要花費比較久的時間,但是這一本書也有給你相關的方向來解決問題。人們都希望自己被尊重,被重視,並且能有足夠表達自己的機會。能夠根據這本書講出的一些重點,也就能清楚的讓對方感受到你的誠意。

雖然我自稱是外向人,因為工作需求:但是其實我內心相當的害羞去接觸人的(是不是沒人相信)。所以每次到了社交場所都會給自己一個任務,「必須要認識OOO公司的XXX」「必須要達成XXX得推廣任務」。透過這個方式來讓自己去認識更多的人,也希望讀過這本書能讓開頭的七秒鐘能夠更加的順暢。