[數位憑證皮夾] 進階版 - 打造「訪客背書發證」:一個同時當驗證方與發行方的 DID 全鏈應用(附開發踩坑紀錄)

(圖片來源: 數位憑證皮夾官方網站) 前提: 上一篇入門版做了一個 HR 員工卡系統:同仁自己申請一張員工卡,然後拿它去申請「運動補助」跟「育兒補助」。那一篇的重點是「發卡(Issuer)」跟「驗證(Verifier)」兩個角色分開來看。 這一篇想再往前走一步:如果一個場景要同時扮演驗證方跟發行方,串成一條完整的 DID 生態鏈,會長什麼樣子?我挑的場景是「訪客背書發證」——這也是我在腦力激盪五個檢驗方應用時,覺得最能展示「全鏈」的一個。 順便,這篇會很誠實地把開發過程中踩到的三個坑寫下來,因為那些才是 TIL 真正有價值的部分。 程式碼在這裡:https://github.com/kkdai/did-usecase-visitor 線上體驗:https://did-usecase-visitor-660825558664.asia-east1.run.app 場景:員工門禁 + 訪客背書發證 這個大廳證件台有兩個模式: 員工門禁 / 活動報名:員工用數位皮夾出示員工卡,系統只驗證「是不是有效員工」,驗過就開門 / 報名成功。姓名、生日、子女數這些欄位一律不揭露,留在皮夾裡——這就是選擇性揭露。 訪客背書發證(這篇的主角):由一位在職員工出示員工卡「背書」,驗證通過之後,系統當場核發一張帶到期時間的臨時訪客通行證到訪客的皮夾。 第二個模式的價值在於,它把兩個角色接起來了: 先當 Verifier(驗員工卡)→ 驗過才當 Issuer(發訪客卡) 比起傳統的紙本訪客簿(抄身分證、押證件影本、一堆個資堆在櫃台還要人工回收),數位背書只留下「哪位員工背書」這一筆可追責的資訊,訪客資料留在訪客自己的皮夾,通行證還可以設到期時間。 架構決策:為什麼不直接改上一個專案 這次我開了一個全新的專案、部署到獨立的 Cloud Run 服務,而不是在原本的 HR 專案上加頁面。幾個考量: 靜態前端 + JSON API:原專案用 jade 樣板 server-side render,這次改成 public/ 靜態頁 + 幾支 JSON API(/api/access/qrcode、/api/access/status),前後端分得比較乾淨。 把皮夾 API 呼叫抽成 lib/wallet.js:原專案的 issuer / verifier 呼叫是內嵌在路由裡、而且重複。這次抽成三個函式:requestPresentationQRCode()、getPresentationResult()、issueCredential(),好維護也好測。 狀態改用記憶體:原專案把資料寫進單一 record.js 檔案,在 Cloud Run 這種無狀態環境上寫檔會有問題。這次用簡單的記憶體物件(重啟歸零,展示用途足夠)。 權杖沿用同一個沙盒帳號:issuer / verifier 的 access token 跟上一篇是同一組,直接重用。 驗證「持有員工卡」的部分,我先沿用既有的運動補助 verifier ref 當 fallback(VERIFIER_ACCESS_REF 沒設就用 VERIFIER_SPORT_REF),這樣不用等後台設定就能先跑起來。 踩坑紀錄一:出示成功了,畫面卻一直卡住 這是最經典的一個。手機掃碼、皮夾也完成出示了,但桌面的頁面就是不往下走,一直在輪詢。 第一步先看 Cloud Run 的日誌,發現 /api/access/status 每 3 秒回一次、每次都回「未驗證」。我在後端加了一行把驗證方原始回應印出來的 log,重新部署後再測一次,就抓到真相了: { "data": [ { "credentialType": "0028680530_line_school", "claims": [ { "ename": "english_name", "cname": "英文名字", "value": "Lub" }, { "ename": "join_company", "cname": "入職時間", "value": "2018-10-05" } ] } ], "verifyResult": true, "resultDescription": "success", "transactionId": "8cd7f37b-..." } 看到問題了嗎?回應裡的欄位是 verifyResult(camelCase),而且根本沒有 code 這個欄位。但我沿用上一篇的舊寫法,判斷式是: // 舊的(對不上現在的回應) const verified = data.code === 0 && data.verify_result === true; data.code 是 undefined、data.verify_result 也是 undefined(人家叫 verifyResult),所以永遠 false,永遠 pending。其實驗證早就成功了(verifyResult: true、resultDescription: "success"),只是我判斷的欄位名對不上——看起來沙盒...
繼續閱讀

[GCP 帳單與 Vertex AI] 破解單一專案 Gemini 費用拆分難題:Vertex AI 動態計費標籤 (Labels) 實戰記

痛點:同一個專案內的 Gemini API 費用如何精準分攤? 在開發企業級 LLM 服務或是經營多租戶 (Multi-tenant) 平台時,最常被財務與維運團隊問到的問題就是: 「我們同一個 GCP 專案內接了許多不同的業務與 LINE Bot,每天的 Gemini Key 費用都會統統出現在 Gemini API 的範圍,我們有辦法根據不同的 Gemini Key 或不同的使用者來拆分費用嗎?」 直接回答你的問題: 在 Google Cloud 帳單(Cloud Billing)報告中,無法直接「根據不同的 API Key 金鑰名稱」來分開顯示費用。 Google Cloud 的帳單報表最小的歸屬維度是到「專案 (Project)」、「服務 (Service)」和「SKU (產品細項)」,系統並不會把個別的 API Key 字串當作獨立的計費項目。對帳單系統來說,同一個專案內不論你建了 10 把還是 100 把 API Key,通通都會被揉在一起算成一筆 Gemini API 的總帳。 山不轉路轉:Vertex AI 的「請求標籤 (Labels)」救星 如果因為架構限制非得塞在同一個專案,最推薦的做法就是:切換至 Vertex AI 呼叫,並使用「請求標籤 (Labels)」。 如果你目前使用的是 Google AI Studio 的 API Key,它在單一專案內是無法傳遞計費標籤的。但如果你將程式碼改為呼叫 Vertex AI 的 Gemini API(一樣在同一個專案內),Vertex AI 支援在每次發送請求時,動態帶入自訂的 labels(標籤)。 原理與流程 在每次發送請求(例如呼叫 generateContent)時,於 API Request 中帶入特定的 Metadata: { "contents": { ... }, "labels": { "client_id": "info_helper", "api_key_group": "marketing_team" } } 這些自訂標籤會直接被傳遞到 GCP 的帳單系統。之後當你到 GCP 帳單報告中,在「分組依據 (Group by)」選擇你設定的標籤鍵(例如 client_id),就能在同一個專案內,把不同標籤(代表不同服務、客戶或使用者)的費用算得一清二楚! 專案實戰改造:全面導入 Labels 機制 為了完成這個需求,我們盤點了目前 LINE Bot 專案的 API 呼叫架構,並進行了以下重構。 1. 專案 API 呼叫盤點 經由掃描,我們發現專案中絕大部分都是使用 Vertex AI 進行呼叫(17 個 Client 中有 14 個使用 vertexai=True),只有少數例外: Vertex AI 呼叫:包括 GitHub 摘要、多個 Google Maps Grounding 工具、文字摘要、圖片分析、語音轉文字等(共 11 個檔案、19 處呼叫點)。 Gemini API Key 呼叫:main.py 的 Live API、batch_service.py 的 Batch 服務,以及 tts_tool.py 的 TTS...
繼續閱讀

[AI 實戰][Gemini Live Translate] 打磨 macOS 會議翻譯 App:自動重連、懸浮字幕、會議記錄匯出全面進化

寫在前面:第二回合,換一把利器 在上一篇文章中,我們用 AGY CLI (Antigravity) 從零打造了一個 macOS 即時會議翻譯 App:透過 ScreenCaptureKit 擷取 Zoom / Google Meet 的音訊,送入 Gemini Live API 進行即時翻譯,並在 App 視窗中顯示繁體中文雙語字幕。 App 上線後,開發者在實際開會中發現了幾個讓人皺眉的問題,同時也有了更多功能想法。這一次,我們換上了 Anthropic 的 Claude Code,在終端機中展開了第二回合的深度打磨。 以下記錄這場協作的完整對話流程,還原每一個關鍵決策點。 階段一:揭露隱藏危機 — 10 分鐘後自動停住的 WebSocket 謎團 App 看似完美運作,但開發者在一場真實的長會議後帶著疑問回來了: User: 查一下這個程式碼,為什麼大概即時翻譯大概十多分鐘就會停住,幫我查看可能會有的原因。 閱讀了全部五個 Swift 原始檔,並結合內建的 Gemini Live API 技能文件,精準指出問題根源: Gemini Live API 的 WebSocket 連線有約 10 分鐘的 Session 上限。時間一到,伺服器會主動關閉連線並送出 GoAway 信號。然而原始程式碼對這個情境毫無防備,造成三層失效: 斷線後不重連:didCloseWith 回呼只更新了 UI 上的狀態文字,沒有任何重連邏輯。 音訊靜默丟棄:isConnected = false 之後,每一個來自麥克風的音訊 chunk 都被 guard isConnected else { return } 無聲地吃掉,使用者毫無察覺。 isRunning 狀態錯誤:主視窗的「翻譯中」圖示仍在閃爍,但實際上什麼都沒有發生。 隨即重構了 GeminiLiveConnection.swift,在不改變任何外部介面的前提下,引入三道防線: GoAway 信號提前偵測:在 parseServerResponse() 中解析伺服器發出的 goAway JSON 訊息,不等連線真正斷掉,就提前主動重連,實現無縫接續翻譯。 指數退避自動重連:斷線後以 2s → 4s → 6s 的間隔遞增重試,最多 10 次;重連成功後計數歸零,確保下一次斷線同樣有完整的重試機會。全程靜默,使用者無感。 30 秒 Ping 保活:連線成功後啟動 Timer,每 30 秒向伺服器發送 sendPing,防止中間網路設備因閒置而切斷連線。 修改完成後,開發者執行了 bash build_app.sh 並推送至 GitHub: Build 成功,commit 完成,push 上去了。之後即時翻譯應該可以長時間持續運作,Session 到期時自動靜默重連,使用者不會察覺中斷。 階段二:功能訪談 — 五個問題鎖定兩大優先開發方向 穩定性解決之後,開發者開始思考 App 能走得更遠: User: 幫我仔細研究後給我三個新功能推薦 仔細閱讀程式碼後,先提出一個定向問題,而非直接給出清單: 這三個新功能的方向你比較傾向哪一類? A. 實用性 / UX — 讓日常使用更方便(字幕匯出、快捷鍵、會議記錄) B. 翻譯品質 — 更準確、更自然(多語言偵測、說話人辨識) C. 系統整合 — 與其他工具串接(Notion、Zoom overlay) User: A 三個 UX 功能隨即出爐:懸浮字幕視窗、會議記錄自動匯出、全域快捷鍵。開發者的回應很直接: User: 1 跟 2 都要 接下來是一場簡短的需求訪談,一次只問一個最關鍵的問題: 懸浮視窗要顯示幾行?→...
繼續閱讀

[Gemini API 實戰] 打造 MemeFinder:用 Gemini 視覺與語意嵌入,做一個「打字就找到梗圖」的原生 Mac 選單列小工具

緣起:聊天打到一半,那張梗圖到底在哪? 每個重度聊天的人手機與電腦裡都存了一堆迷因圖,但真正要用的時候——對話進行到一半、想丟一張「謝謝再聯絡」或「我就爛」——卻怎麼也翻不到。檔名是 IMG_4821.jpg,相簿沒有分類,搜尋更是無從下手。 我先看到一個很棒的開源專案 ShiQu1218/MemeTalk,它用 Python + Streamlit + SQLite 打造了一套本地迷因語意搜尋系統,會掃描你本機的迷因資料夾、用 OCR 與向量嵌入建立索引,再做多路召回。功能完整,但偏研究取向、要開瀏覽器跑 Streamlit。 我想要的是更貼近「日常順手工具」的東西: 一個原生 Mac App,一個搜尋框,打我想找的內容,就跳出相關的梗圖,點一下直接複製到剪貼簿。 於是有了 MemeFinder。這篇文章紀錄它從零到「選單列常駐 + 全域快捷鍵」的開發過程,以及途中幾個很有代表性的坑。 系統設計與架構 核心概念很單純:指定一個本機迷因資料夾 → 用 Gemini 幫每張圖建立索引 → 打字做語意搜尋 → 點圖複製。 技術選型上我做了三個關鍵決定: 原生 SwiftUI App,而不是 Electron。剪貼簿複製圖片、全域快捷鍵、選單列常駐,這些用 AppKit 都是一級公民。 Gemini 負責兩件事:用視覺模型 gemini-3-flash-preview 讀出圖中文字、生成繁中描述與情緒標籤;用 gemini-embedding-2 把這些語意轉成 768 維向量。 語意向量 + 關鍵字混合搜尋。純關鍵字對中文召回太差;語意向量才能做到「打相關敘述就找到圖」。 系統架構流向 graph TD A[使用者指定迷因資料夾] -->|掃描 jpg/png/webp| B[Indexer 索引器] B -->|每張圖| C[Gemini 視覺模型 gemini-3-flash-preview] C -->|OCR文字 + 描述 + 標籤 + 情緒| D[Gemini 嵌入 gemini-embedding-2] D -->|768 維向量| E[本機索引檔 index.json] F[使用者打字查詢] -->|⌃⌘M 選單列浮窗| G[Gemini 嵌入查詢字串] G -->|cosine 相似度 + 關鍵字加權| E E -->|排序結果| H[縮圖牆] H -->|點圖| I[NSPasteboard 複製到剪貼簿] 整個專案刻意拆成兩個 Swift Package target: Target 類型 內容 MemeFinder library 邏輯、模型、服務、ViewModel(全部有單元測試) MemeFinderApp executable SwiftUI 畫面 + 選單列殼(薄殼,依賴上面的函式庫) 這個拆分不是裝飾——它直接決定了測試能不能順利跑,後面「踩坑二」會講到為什麼。 核心實作 1. 用 Gemini 視覺模型自動標註迷因圖 索引時,每張圖會送進視覺模型,要求它只輸出 JSON:圖中文字、繁中描述、標籤、情緒。responseMimeType 設成 application/json 來穩定輸出格式: public static func annotateRequest(apiKey: String, imageData: Data, mimeType: String) -> URLRequest { let prompt = """ 你是迷因圖標註助手。請閱讀這張圖,輸出 JSON,欄位: ocr_text(圖中所有文字), description(用繁體中文描述畫面與梗), tags(3-8 個繁體中文關鍵字陣列), emotion(單一情緒詞)。只輸出 JSON。 """ let...
繼續閱讀

[Gemini API 實戰] Gemini Batch API 與 Webhook 實戰記:打造 LINE Bot 附近餐廳大數據一鍵深度分析

異步處理的利器:Gemini Batch API & Webhooks 在開發基於 LLM 的應用程式時,我們常常需要處理大量的數據分析任務——例如一次性分析數十家餐廳的評論、對大量文章進行分類、或是批次生成翻譯。如果採用傳統的同步 API(即時呼叫),不僅會面臨嚴重的 Rate Limit (速率限制) 阻塞,更會因為網路連線逾時(Timeout)與極高的運算成本而宣告失敗。 為了打破這個限制,Google 推出了 Gemini Batch API 與 Webhook API: Gemini Batch API:允許開發者將大量的請求打包成一個 JSONL 檔案一次性上傳。Gemini 會在後台進行非同步的排程運算,不佔用您日常的即時 API 額度(Rate Limits),且其運算成本通常只有即時 API 的一半,是處理非緊急大數據的完美選擇。 Webhook API:傳統的 Batch 任務需要我們在本機不斷寫輪詢(Polling)去檢查狀態。而透過 Webhook,當 Gemini 完成 Batch 運算後,會主動向您指定的 URL 發送一個 HTTP POST 回呼,即時通知任務已完成,讓系統架構變得更加優雅與節能。 這篇文章將紀錄我們如何將這兩項強大的 API 整合進我們的 LINE Bot 餐廳分析助手,實現在行動端一鍵對特定餐廳進行深度評論與招牌菜大數據分析的開發經歷。 系統設計與優化架構 原本的餐廳分析功能是當用戶發送位置時,Bot 會列出附近餐廳,並提供一個通用的「深度評論分析 (Batch)」按鈕,點下去會一次性把附近所有餐廳送去分析。然而這帶來了不好的 UX:分析所有餐廳耗時過長,且用戶往往只想針對他感興趣的某一家特定餐廳進行深挖。 因此,我們將功能優化為動態 Quick Reply 按鈕: 用戶傳送定位,Bot 透過 Google Maps Grounding 搜尋附近餐廳。 用戶端獲得餐廳純文字列表後,Bot 自動以 Gemini 擷取評分最高的前 3 家餐廳名稱。 產生 3 個客製化的 Quick Reply 按鈕(例如:🍴 分析 鼎泰豐)。 用戶點擊特定餐廳按鈕後,Bot 立即回覆「處理中」以避免 LINE 逾時,並在背景提交該單一餐廳的 Batch 任務,待 Gemini 運算完畢後主動推播專屬大數據報告。 系統架構流向 graph TD A[用戶傳送定位] -->|Location Message| B[Google Maps Grounding 搜尋] B -->|餐廳純文字列表| C[Gemini-2.5-flash 擷取前三名餐廳] C -->|動態生成 Quick Reply| D[LINE Bot 回覆 3 個客製化分析按鈕] D -->|用戶點擊特定分析| E[FastAPI Background Task] E -->|立即 Reply ACK| F[LINE 聊天室訊息] E -->|打包 JSONL 並上傳| G[Gemini Batch API 提交] G -->|運算完成 Webhook/Polling 回呼| H[主動 Push 深度報告給用戶] 核心實作 1. 使用 Gemini 從 Grounding 文字中精準提取餐廳名 在 tools/maps_tool.py 中,地圖搜尋返回的是一段富含格式與說明的純文字。我們使用 Gemini-2.5-flash 的...
繼續閱讀

[I/O Extended Taipei] 在 Gemini API 家族中建構應用程式:從呼叫 API,到架構一個會自己完成工作的系統

(活動:Google I/O Extended 2026 Taipei / 簡報:SpeakerDeck) 前情提要:Gemini API 已經不是「多打一個 prompt」而已 如果你對 Gemini API 的印象還停留在「選一個 model,送一段 prompt,拿回一段文字」,那你看到 2026 年這一輪更新時,很可能會突然意識到一件事: Gemini API 已經從單純的 API 介面,變成一個可以拿來搭應用、搭代理、搭非同步流程的完整平台。 這篇內容整理自我在 Google I/O Extended 2026 Taipei 的分享「在 Gemini API 家族中建構應用程式」。LINE 台灣開發者關係部技術總監 Evan Lin 在現場反覆強調的核心觀察是:開發者現在真正該思考的,不再只是 「我要用 Pro 還是 Flash?」,而是 「我要怎麼把模型、檢索、代理、回呼與成本控制串成一套系統?」。 換句話說,重點正在從 call API,轉向 design system。 先看全景圖:2026 Gemini API 家族到底多了什麼? 如果把 2026 年的 Gemini API 當成一張 capability map 來看,大致可以拆成三層。 第一層:核心模型 Gemini 3.5 Pro:最強推理能力,適合複雜規劃、進階分析與多步驟任務。 Gemini 3.5 Flash:主力模型,速度、成本與能力最平衡,適合多數產品流量。 Flash-Lite:高頻率、低成本場景的意圖判斷器與前置分類器。 Gemini Embedding 2:不只文本,也能支援多模態向量化需求。 第二層:關鍵能力模組 Retrieval:File Search、Google Search Grounding、URL Context。 Agent / Async:Agents API、Webhook、Deep Research agent。 Infrastructure:Context caching、Batch API、Live API。 第三層:系統設計方式 這一層反而最重要。因為當上面那幾個能力被做成平台服務之後,很多以前得自己補的「中間層」突然不見了: 不一定要自己搭一套 RAG pipeline。 不一定要自己養 agent loop。 不一定要用 polling 卡住主伺服器等結果。 核心觀察:Gemini API 的升級不只是「模型變強」,而是 Google 把原本屬於應用層的麻煩事,往平台層往下吃掉了。這會直接改變我們設計 AI 系統的方式。 架構轉折點:三個工具,三次思維切換 這場分享裡最值得反覆消化的,是這三個工具背後代表的架構變化。 1. File Search:從手刻 RAG,轉向 Managed RAG 以前講到企業知識問答,大家直覺就是: 切 chunk。 做 embedding。 存進 vector DB。 寫 retrieval code。 再自己補 citation 與權限控管。 現在 File Search 出現後,開發者可以把更多力氣放在「文件怎麼治理、權限怎麼分、回答怎麼呈現」,而不是一直重複寫那套基礎設施。 更重要的是,它不是只會查文字。 為什麼這次的 File Search 特別值得注意? 圖文同空間:PDF 裡的截圖、圖表、圖文混排,不再只是附件,而是模型可理解的內容。 Metadata 過濾:可以依部門、系統、文件類型做過濾,這對企業內部知識檢索非常重要。 精確引用:能回到具體頁數與 grounding metadata,讓回答更能被信任。 這代表一件很實際的事:很多企業過去花在 LangChain、向量庫與 chunking 策略上的時間,現在可以大幅往 權限設計、UX、內容治理 轉移。...
繼續閱讀