[{"content":"","date":"2026年6月7日","externalUrl":null,"permalink":"/zh-tw/tags/blowfish/","section":"Tags","summary":"","title":"Blowfish","type":"tags"},{"content":"幫這個網站「補上一張臉」——個人頭像、社群分享預覽圖、favicon——的時候,我本來以為是 同一個設定。並不是。在 Blowfish 主題裡,這是三件獨立的事,設定在三個不同的 地方;而那個名字聽起來最像「首頁圖片」的參數(homepageImage),三者都不是它管的。\n這篇就是我當初希望手邊先有的那張地圖。\n陷阱:腦中一個概念,實際三個設定 # 你看到的 Blowfish 參數 放在哪 從哪解析 Profile 頭像 author.image languages.toml(每語言各一份) assets/,經 resources.Get OG / 社群 分享圖 defaultSocialImage params.toml assets/,經 resources.Get 瀏覽器 favicon (沒有參數)——固定檔名 static/ 原樣複製 先講結論:homepageImage 是個假線索。 它是餵給 background / hero 首頁版型當 橫幅背景用的。如果你的首頁版型是 profile(作品集式的著陸頁),頭像不是來自 homepageImage,而是來自 author image。我是直接讀主題的 partial layouts/partials/home/profile.html 確認的——它只抓 .Site.Params.Author.image,別無其他。\n1. 頭像 —— author.image,每語言各設 # Blowfish 的 author 掛在每個語言區塊底下,所以圖片是分語言設定的(兩邊指向同一個檔即可):\n# config/_default/languages.toml [en.params.author] name = \u0026#34;SJ.Wu\u0026#34; image = \u0026#34;img/avatar.jpeg\u0026#34; # 相對於 assets/ headline = \u0026#34;Software Engineer\u0026#34; [zh-tw.params.author] name = \u0026#34;SJ.Wu\u0026#34; image = \u0026#34;img/avatar.jpeg\u0026#34; headline = \u0026#34;軟體工程師\u0026#34; 路徑相對於 assets/,所以檔案放在 assets/img/avatar.jpeg。主題會把它丟進 Hugo 的圖片管線——Fill 成 288×288 正方、品質 96——所以你只要準備一張大致正方的圖就好, 它會自動以短邊裁切。\n2. OG 圖 —— defaultSocialImage,不是頭像 # 社群分享預覽(Facebook / LinkedIn / Slack 從你的連結算出來的那張卡片)是另一張圖、 另一個參數:\n# config/_default/params.toml defaultSocialImage = \u0026#34;img/og.png\u0026#34; # 1.91:1,相對於 assets/ 關於 Blowfish 怎麼挑 OG 圖(出自 layouts/partials/head.html),有兩件事值得知道:\n單頁優先。 如果某個 page bundle 裡有一個名稱含 *featured*、*cover* 或 *thumbnail* 的資源,那張圖就成為該頁的 og:image。defaultSocialImage 只是 沒有這種資源時的後備——這正好就是首頁與多數文章想要的行為。 比例是 1.91:1。 社群標準是 1200×630。一開始就照這個比例做,事後再裁只是浪費 左右兩側。 一個實用的小技巧:要生成一張能對齊風格化頭像的 OG 圖,把頭像當參考圖丟進生圖模型, 提示詞要求「1200×630 橫幅、同樣的扁平插畫風、人物在左、右側留白」,而且圖裡不要 燒上任何文字。生圖模型寫小字幾乎都會糊;真要標題,事後在 Figma/Canva 疊上去,才能保持 銳利。\n3. favicon —— 它不會順便跟著換 # 這點最容易讓人意外:設定 author.image 跟 defaultSocialImage,對 favicon 毫無 影響。Blowfish 是從 static/ 底下一組固定檔名提供 favicon,而你放進去的同名檔會 蓋過主題預設:\nstatic/favicon.ico # 多尺寸:48 / 32 / 16 static/favicon-32x32.png static/favicon-16x16.png static/apple-touch-icon.png # 180×180 我是用 checksum 比對確認站台還停在主題預設上的——build 出來的 public/favicon.ico 跟 主題內建檔的 md5 一模一樣。在你把自己的檔案丟進 static/ 之前,出去的都是 Blowfish 的圖示。\n用一行指令生出一組「SJ」favicon # 半身頭像縮到 16×16 根本看不清,所以 favicon 我改用簡單的字母標誌。ImageMagick 從一張母圖 就能產出整組:\nFONT=/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf # 512px 母圖:圓角方形 + 置中的「SJ」 magick -size 512x512 xc:none \\ -fill \u0026#34;#1f2a37\u0026#34; -draw \u0026#34;roundrectangle 0,0 511,511 96,96\u0026#34; \\ -font \u0026#34;$FONT\u0026#34; -fill \u0026#34;#f4f1ea\u0026#34; -gravity center -pointsize 250 \\ -annotate +0-10 \u0026#34;SJ\u0026#34; master.png # 衍生尺寸 magick master.png -resize 180x180 static/apple-touch-icon.png magick master.png -resize 32x32 static/favicon-32x32.png magick master.png -resize 16x16 static/favicon-16x16.png magick master.png -define icon:auto-resize=48,32,16 static/favicon.ico 顏色直接從頭像取樣——深藍灰當底(髮色/輪廓色)、暖白當字(白 T / 背景)——讓 favicon 跟 頭像讀起來是同一套品牌。\nassets/ vs static/ —— 為什麼這個區分重要 # 這正是三個設定各自放在不同地方的安靜理由:\nassets/ 是 Hugo 的處理管線。resources.Get 從這裡讀,主題可以縮放、裁切、 加指紋(這就是為什麼 build 出來的頭像網址長得像 avatar_hu_9d0…jpeg)。頭像跟 OG 圖 放這裡,是因為 Blowfish 要轉換它們。 static/ 是原封不動複製到站台根目錄,完全不處理。favicon 必須保持精確的檔名 與位元組內容,所以該放這裡。 把 favicon 放進 assets/,主題會找不到;把頭像放進 static/,你就失去自動縮放。檔案要 配對到對的資料夾。\n驗證線上站台 # GitHub Actions 部署完之後,我不信 build、改看實際產出——還帶了一個破快取的查詢參數, 確保看到的是當下的資源:\n# OG meta 標籤,以及圖片是否真的有提供 curl -s https://sj-wu.com/ | grep -oE \u0026#39;\u0026lt;meta property=\u0026#34;og:image\u0026#34;[^\u0026gt;]*\u0026gt;\u0026#39; curl -s -o /dev/null -w \u0026#39;%{http_code} %{content_type}\\n\u0026#39; https://sj-wu.com/img/og.png # favicon 已不再是主題預設 curl -s \u0026#34;https://sj-wu.com/apple-touch-icon.png?nocache=$RANDOM\u0026#34; -o live.png 三者都回 200,OG 標籤指向正確網址,下載下來的 favicon 就是新的 SJ 標誌。\n那些讓它「看起來壞掉」的快取 # 伺服器端全都正確,卻仍然看起來是舊的——因為你前面卡著兩層快取:\n社群平台會快取 OG 預覽。 Facebook / LinkedIn 會留著舊卡片,直到你在它們的 post-inspector / 分享偵錯工具裡重新抓取。 瀏覽器對 favicon 的快取特別頑固——常常連一般重整都不會更新。在下任何「壞掉」的 結論前,先用無痕視窗,或在網址後加 ?v=2 強制重抓。 重點整理 # 頭像、OG 圖、favicon 是三個設定、三個地方——別預期改一個會連動其他。 在 profile 版型下,homepageImage 不是頭像;author.image 才是。 defaultSocialImage 只是 OG 的後備——單頁的 *featured* / *cover* / *thumbnail* 資源會蓋過它。 favicon 以固定檔名住在 static/,必須明確替換,沒有別的設定會動到它。 assets/ = 會被處理(縮放/裁切/指紋);static/ = 原樣輸出。檔案放在它該被怎麼處理的 地方。 當某樣東西「沒更新」,先懷疑快取,再懷疑設定。 ","date":"2026年6月7日","externalUrl":null,"permalink":"/zh-tw/posts/blowfish-avatar-og-favicon/","section":"技術文章","summary":"個人頭像、社群分享的 OG 圖、瀏覽器分頁的 favicon,看起來像同一張「網站圖」,但在 Blowfish 裡是三個獨立設定、放在三個不同地方。這篇講清楚哪個參數管什麼、為什麼 homepageImage 三者都不是,以及如何用一行指令生出一組 SJ favicon。","title":"Blowfish 的頭像、OG 圖與 favicon:三件事,三個參數","type":"posts"},{"content":"","date":"2026年6月7日","externalUrl":null,"permalink":"/zh-tw/tags/favicon/","section":"Tags","summary":"","title":"Favicon","type":"tags"},{"content":"","date":"2026年6月7日","externalUrl":null,"permalink":"/zh-tw/tags/hugo/","section":"Tags","summary":"","title":"Hugo","type":"tags"},{"content":"","date":"2026年6月7日","externalUrl":null,"permalink":"/zh-tw/tags/open-graph/","section":"Tags","summary":"","title":"Open Graph","type":"tags"},{"content":"我打造在生產環境運行的後端微服務，目前投入身分驗證的應用機器學習與電腦視覺 ——活體偵測與深偽（deepfake）偵測。職涯早期是 SSD 韌體工程師，因此從底層硬韌體、 分散式系統到機器學習 pipeline 都能上手。\n","date":"2026年6月7日","externalUrl":null,"permalink":"/zh-tw/","section":"SJ.Wu","summary":"我打造在生產環境運行的後端微服務，目前投入身分驗證的應用機器學習與電腦視覺 ——活體偵測與深偽（deepfake）偵測。職涯早期是 SSD 韌體工程師，因此從底層硬韌體、 分散式系統到機器學習 pipeline 都能上手。\n","title":"SJ.Wu","type":"page"},{"content":"","date":"2026年6月7日","externalUrl":null,"permalink":"/zh-tw/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"2026年6月7日","externalUrl":null,"permalink":"/zh-tw/posts/","section":"技術文章","summary":"","title":"技術文章","type":"posts"},{"content":"","date":"2026年6月7日","externalUrl":null,"permalink":"/zh-tw/tags/cloudflare/","section":"Tags","summary":"","title":"Cloudflare","type":"tags"},{"content":"","date":"2026年6月7日","externalUrl":null,"permalink":"/zh-tw/tags/dns/","section":"Tags","summary":"","title":"DNS","type":"tags"},{"content":"","date":"2026年6月7日","externalUrl":null,"permalink":"/zh-tw/tags/github-pages/","section":"Tags","summary":"","title":"GitHub Pages","type":"tags"},{"content":"GitHub Pages 站台一開始的網址是 \u0026lt;user\u0026gt;.github.io。接上自己的網域其實不難，重點是順序對； 但有幾個步驟有坑 —— 尤其註冊商是 Cloudflare 時，有一個預設值會默默讓 HTTPS 永遠發不出來。 這篇把整個流程記下來，並用 example.com 當範例，你直接換成自己的網域即可。\n延伸閱讀：用 AI 一天打造這個網站。\n先決定：apex 還是 www # 你得先選哪一種形式是 canonical（主網址）—— 也就是網址列最後會停在哪一個。\napex（example.com）—— 最短最乾淨。代價在 DNS：apex 不能用 CNAME，所以要用 A 指向 GitHub 的四個 IP（IPv6 再加 AAAA）。 www（www.example.com）—— DNS 只要一筆乾淨的 CNAME，但網址比較長。 常見做法是以 apex 為主、讓 www 轉址過去。兩邊都設好之後，GitHub 會自動處理這個轉址， 所以兩個網址都能用，而網址列顯示的是裸 apex。\n步驟一 —— repo 改動 # 有兩樣東西放在 repo 裡，跟 DNS 無關。\n一個 CNAME 檔告訴 GitHub 這個站要回應哪個網域。用 Hugo 的話，把它放在 static/， 它會原封不動被複製進建置輸出：\nstatic/CNAME → example.com 檔案裡只放 canonical 主機 —— apex 本身，不是 www。\nbaseURL。 更新它，讓產生的絕對連結、sitemap、RSS 都用新主機：\n# config/_default/hugo.toml baseURL = \u0026#34;https://example.com/\u0026#34; 如果你在 CI 建置，檢查一下 workflow 有沒有覆寫 baseURL。常見的 Pages workflow 會這樣：\nrun: hugo --gc --minify --baseURL \u0026#34;${{ steps.pages.outputs.base_url }}/\u0026#34; 這個 base_url 輸出會在你於 Pages 設好自訂網域之後自動變成你的網域，所以 workflow 不用改 —— 但 hugo.toml 裡的值對本機建置仍有意義，還是一併更新。\n把這些 push 上去，部署就會把 CNAME 發佈進線上輸出。\n步驟二 —— Cloudflare DNS # 用 Cloudflare 註冊的網域本來就在用 Cloudflare DNS，所以這步只在 DNS → Records 分頁。 以 apex 為主的設定：\nType Name Value A @ 185.199.108.153 A @ 185.199.109.153 A @ 185.199.110.153 A @ 185.199.111.153 AAAA @ 2606:50c0:8000::153 AAAA @ 2606:50c0:8001::153 AAAA @ 2606:50c0:8002::153 AAAA @ 2606:50c0:8003::153 CNAME www \u0026lt;user\u0026gt;.github.io （這些是 GitHub 公布的 Pages IP —— 建議對照 GitHub 官方文件確認，以防未來變動。）\n坑：把橘色雲朵關掉 # 這就是會咬人的那一個。Cloudflare 預設會 proxy 紀錄 —— 也就是橘色雲朵。開著 proxy 時， GitHub 沒辦法完成它簽發 Let\u0026rsquo;s Encrypt 憑證所需的 HTTP 驗證，HTTPS 就一直發不出來；而如果 你硬讓 Cloudflare 自己的 TLS 擋在前面，又會變成 redirect loop。把每一筆都設成 「DNS only」—— 灰色雲朵。 點雲朵圖示直到變灰。\n至少保持灰雲，直到 GitHub 憑證簽好、「Enforce HTTPS」可以打勾為止。如果你之後真的想用 Cloudflare 的 proxy/CDN，可以再打開 —— 但要把 Cloudflare 的 SSL/TLS 模式設成「Full」 （絕對不要用「Flexible」，會 loop）。最簡單、最不出事的做法，就是讓它維持灰雲。\n步驟三 —— GitHub Pages + HTTPS # 在站台 repo：Settings → Pages。\nCustom domain → 填 example.com → Save。GitHub 會跑 DNS check；等你的紀錄 生效後，會出現綠勾。 等憑證。這要幾分鐘到數十分鐘 —— 憑證好之前，Enforce HTTPS 是反灰不能點的。 勾 Enforce HTTPS。之後每個 http:// 請求都會 301 轉到 https://。 小撇步：先把 CNAME（步驟一）push 上去，再去填自訂網域，這樣 DNS check 跑的時候，網域 已經在部署輸出裡了。\n步驟四 —— 驗證切換 # 先看 DNS：\ndig example.com +short # 應回 GitHub 的四個 IP dig www.example.com +short # 應回 \u0026lt;user\u0026gt;.github.io. 後接 IP 接著每個入口都應該導到 canonical 的 HTTPS 網址：\ncurl -sI https://example.com | head -1 # HTTP/2 200 curl -sI https://www.example.com | head -1 # 301 -\u0026gt; https://example.com/ curl -sI http://example.com | head -1 # 301 -\u0026gt; https（Enforce HTTPS） curl -sI https://\u0026lt;user\u0026gt;.github.io | head -1 # 301 -\u0026gt; https://example.com/ 四個入口 —— apex、www、純 http、舊的 github.io —— 全部匯流到 canonical 的 HTTPS 網址。 切換就完成了。\n步驟五 —— 網域驗證（防搶綁） # 還有一個比較少人注意、但值得關掉的風險。如果你哪天把網域從這個 repo 移除、卻沒移除 DNS 紀錄，別人就可能在他自己的 Pages 站台宣告你的網域。GitHub 的網域驗證可以擋掉這件事： 驗證過的網域只能被你的帳號使用。\n它在你的帳號設定裡，不在 repo：Settings → Pages → 「Add a domain」。GitHub 會給你 一筆像這樣的 TXT 紀錄：\n_github-pages-challenge-\u0026lt;user\u0026gt;.example.com TXT \u0026lt;token\u0026gt; 把那筆 TXT 加進 Cloudflare（灰雲 —— TXT 本來也不會被 proxy），再按 Verify。用這個 確認它已生效：\ndig _github-pages-challenge-\u0026lt;user\u0026gt;.example.com TXT +short 驗證通過後，這個網域就鎖定在你的帳號上了。那筆 TXT 紀錄可以留著。\n一口氣講完順序 # repo CNAME + baseURL → push → Cloudflare A/AAAA/www 紀錄設灰雲 → Pages 自訂網域 → 等憑證 → Enforce HTTPS → 用 dig/curl 驗證 → 加上網域驗證的 TXT。 最可能浪費你一個下午的，就是那個橘色雲朵。把它轉灰。\n","date":"2026年6月7日","externalUrl":null,"permalink":"/zh-tw/posts/custom-domain-github-pages/","section":"技術文章","summary":"把剛註冊好的 apex 網域接到 GitHub Pages 站台的完整流程：repo 要改什麼、Cloudflare 的 DNS 紀錄怎麼設、哪一個 proxy 預設值會讓 HTTPS 永遠發不出來，以及如何驗證切換完成 —— 最後加上網域驗證，防止別人搶綁你的網域。","title":"把 GitHub Pages 站台搬到自訂網域（Cloudflare Registrar）","type":"posts"},{"content":"","date":"2026年6月7日","externalUrl":null,"permalink":"/zh-tw/tags/ai/","section":"Tags","summary":"","title":"AI","type":"tags"},{"content":"","date":"2026年6月7日","externalUrl":null,"permalink":"/zh-tw/tags/claude-code/","section":"Tags","summary":"","title":"Claude Code","type":"tags"},{"content":"","date":"2026年6月7日","externalUrl":null,"permalink":"/zh-tw/tags/github-actions/","section":"Tags","summary":"","title":"GitHub Actions","type":"tags"},{"content":"這個網站本身，就是我今天用 AI（Claude Code）協作一天做出來的。起點只有一份 PDF 履歷、 幾篇散落的技術筆記和演講稿；終點是這個中英雙語、push 就自動上線的個人站。這篇把流程、 紀律與踩到的坑記下來，給想做類似事情的人參考。\n延伸閱讀：AI 協作開發指南。\n技術組合 # Claude Code — AI 結對夥伴，負責大部分機械性的工作、翻譯與一致性檢查 Hugo（extended）+ Blowfish 主題 — 靜態網站產生器；Blowfish 以 Hugo Module 安裝 GitHub Actions → GitHub Pages — push 到 main 就自動建置、部署 流程 # 1. 先定位，再寫 README # 第一步不是寫程式，是想清楚定位。GitHub profile README 是一張「名片」，所以先決定 受眾（求職／海外）、主軸（後端 + 應用 ML/CV）、深度（極簡名片、細節留給網站）。 這段我用一個「逐層拷問」的流程，把每個決策分支問清楚再動手——避免做完才發現方向錯了。\n結果就是一張極簡名片：一句 tagline、分組的技術 badges、三個聯絡連結，其餘全部導流到這個網站。\n2. 用 Hugo + Blowfish 搭出網站骨架 # 用 Hugo Modules 安裝 Blowfish（hugo mod init → 匯入主題 → hugo mod get）， 之後升級主題只要 hugo mod get -u。 中英雙語用 Hugo 內建 i18n：語言定義放在 languages.toml，內容用 .en.md / .zh-tw.md 後綴。 首頁採 Blowfish 的 profile 版面，把履歷的精華（後端、ML/CV、演講、論文）放上去。 3. 用 GitHub Actions 自動部署 # 寫一支 workflow：push 到 main → 安裝 Hugo extended → hugo mod get → hugo --gc --minify → 透過官方 upload-pages-artifact / deploy-pages 發佈到 GitHub Pages。 之後寫文章只要 push，1–2 分鐘就上線。\n唯一的手動步驟：到 repo 的 Settings → Pages → Source 設成「GitHub Actions」， 第一次設定後就不用再管。\n4. 把現有文件變成文章——關鍵是「紀律」 # 這是最需要人來把關、AI 不能自己決定的部分。我把履歷、演講稿、自學筆記、工作筆記 逐一轉成雙語文章，過程中堅持幾條紀律：\n脫敏（最重要）：任何來自工作的內容，只描述領域與技術，不出現雇主名稱、 內部服務名、業務細節。例如一份內部的 Feign 開發指南，原本範例是公司的某個敏感業務， 我把整個範例領域換成中性的「電商訂單」，只保留通用的架構模式。 清掉私人連結：筆記裡指向個人知識庫的引用連結，對讀者是壞連結、還會外洩內部 ID， 全部移除。 清掉追蹤參數：貼課程／外部連結時，把網址後面一長串 gclid、utm_* 之類的 追蹤參數砍掉，只留乾淨網址。 雙語：每篇都出中英兩版。 每次部署後我都跑一個簡單的稽核（例如 grep -ri 掃公開產物有沒有殘留敏感字眼）， 確認線上乾淨才算完成。\n踩到的坑 # 主題與 Hugo 版本要對齊：Blowfish 有測試過的 Hugo 上限版本，用更新的 Hugo 會跳 相容性警告。把本機與 CI 都鎖在主題支援的版本最省事。 未來日期會被略過：Hugo 預設不建置「未來」的文章。我寫當天日期時，因為機器時區 （UTC+8）讓「當天 UTC 午夜」其實還在未來，文章就沒被建出來。解法：日期帶上明確時區 （或用前一天）。 設定檔命名：語言「定義」要放在 languages.toml；languages.en.toml 這種檔名 會被 Hugo 把 .en 當成語言限定詞而忽略內容。.\u0026lt;lang\u0026gt; 後綴只適用於 menus.\u0026lt;lang\u0026gt;.toml。 心得 # AI 把機械性的工作（搭骨架、寫 workflow、翻譯、格式整理、一致性稽核）做得又快又穩， 讓「一天上線」變得可行。但有兩件事始終是人的責任：定位的判斷，以及揭露界線的把關 ——什麼能公開、什麼要脫敏，AI 可以提醒、可以執行，但最後拍板的是你。\n","date":"2026年6月7日","externalUrl":null,"permalink":"/zh-tw/posts/building-this-site-with-ai/","section":"技術文章","summary":"用 Claude Code 搭配 Hugo + Blowfish + GitHub Actions，一天內把一份 PDF 履歷和散落的技術筆記，變成這個中英雙語、自動部署的個人網站。記錄流程、脫敏紀律與踩到的坑。","title":"用 AI 一天打造這個網站：把履歷與技術筆記自動化部署上線","type":"posts"},{"content":"","date":"2026年5月1日","externalUrl":null,"permalink":"/zh-tw/tags/neovim/","section":"Tags","summary":"","title":"NeoVim","type":"tags"},{"content":"","date":"1 May 2026","externalUrl":null,"permalink":"/tags/terminal/","section":"Tags","summary":"","title":"Terminal","type":"tags"},{"content":"","date":"2026年5月1日","externalUrl":null,"permalink":"/zh-tw/tags/tmux/","section":"Tags","summary":"","title":"Tmux","type":"tags"},{"content":"Claude Code 這類 AI 協作工具是終端原生的——它就跑在終端機裡。 所以一個順手的終端環境，等於直接放大了 AI 協作的效率：AI 負責大量產出，人則在同一個 全鍵盤環境裡快速審查、微調、把關。這篇整理我目前用 tmux + NeoVim + Yazi + LazyGit 搭配 Claude Code 的配置與工作流，以及設定時踩到的坑。\n環境版本：NeoVim v0.11.6、Yazi 26.5.6、tmux 3.6、終端機模擬器 Ghostty、主要語言 Python。 （個人化的自訂鍵位細節此處略過，只談架構、設定與工作流。）\n1. 為什麼這套組合特別適合搭配 Claude Code # 把這四套工具當成 Claude Code 的「外圍」，各自補上 AI 協作中人最需要的環節：\n工具 在 AI 協作流程中的角色 tmux 一個窗格跑 Claude Code、另一個窗格開編輯器或跑測試，並排對照 AI 的輸出與實際程式碼；session 可長駐，隨時回到同一個工作現場。 NeoVim AI 改完的檔案在這裡審查、微調、跑測試（neotest）；LSP 立即標出型別／lint 問題，快速驗證 AI 的改動是否站得住。 LazyGit 把關 AI 產出的關鍵一站：用 TUI 逐個 hunk 檢視 diff、選擇性 stage、寫 commit——AI 改一批，人在這裡一個一個確認再進版控。 Yazi 在專案與檔案樹間快速導航，找到要交給 Claude Code 處理的路徑。 核心理念：讓 AI 與人待在同一個全鍵盤、全終端的環境，不用在 GUI 與終端之間來回切換， 審查與迭代的循環越短，AI 協作的效益越高。NeoVim 是編輯中樞，tmux 在最外層提供窗格管理， 並負責 Ghostty ↔ TUI 之間的終端能力協商（這也是踩坑最多的地方，見 §6）。\n2. NeoVim # 設定採 Lua-based，以 lazy.nvim 作為套件管理器。目錄結構：\n~/.config/nvim/ ├── init.lua # 進入點：bootstrap lazy.nvim + 載入設定 ├── lazy-lock.json # 套件版本鎖定 └── lua/ ├── config/ │ ├── options.lua # vim.opt.* 基本設定 │ └── keymaps.lua # 鍵位對應（個人化，略） └── plugins/ # 每個檔案一組外掛 spec ├── lsp.lua ├── completion.lua ├── telescope.lua ├── treesitter.lua ├── colorscheme.lua ├── motion.lua ├── autopairs.lua ├── testing.lua ├── lazygit.lua └── yazi.lua 2.1 進入點 init.lua # -- Bootstrap lazy.nvim local lazypath = vim.fn.stdpath(\u0026#34;data\u0026#34;) .. \u0026#34;/lazy/lazy.nvim\u0026#34; if not vim.loop.fs_stat(lazypath) then vim.fn.system({ \u0026#34;git\u0026#34;, \u0026#34;clone\u0026#34;, \u0026#34;--filter=blob:none\u0026#34;, \u0026#34;https://github.com/folke/lazy.nvim.git\u0026#34;, \u0026#34;--branch=stable\u0026#34;, lazypath, }) end vim.opt.rtp:prepend(lazypath) require(\u0026#34;config.options\u0026#34;) require(\u0026#34;config.keymaps\u0026#34;) require(\u0026#34;lazy\u0026#34;).setup(\u0026#34;plugins\u0026#34;, { change_detection = { notify = false }, }) 重點：\n首次啟動會自動 git clone lazy.nvim（bootstrap），不需手動安裝。 require(\u0026quot;lazy\u0026quot;).setup(\u0026quot;plugins\u0026quot;, ...) 會自動載入 lua/plugins/ 底下所有檔案。 change_detection.notify = false：關閉「設定檔被外部修改」的提示——這點搭配 Claude Code 特別有感， 因為 AI 會頻繁改檔，否則通知會一直跳。 2.2 基本選項 config/options.lua # local opt = vim.opt opt.number = true -- 顯示行號 opt.relativenumber = true -- 相對行號（搭配 j/k 跳行） opt.expandtab = true -- Tab 轉空白 opt.shiftwidth = 4 -- 縮排寬度 4 opt.tabstop = 4 -- Tab 顯示寬度 4 opt.smartindent = true -- 智慧縮排 opt.wrap = false -- 不自動換行 opt.cursorline = true -- 高亮當前行 opt.scrolloff = 8 -- 游標上下保留 8 行 opt.signcolumn = \u0026#34;yes\u0026#34; -- 固定顯示 sign 欄（避免診斷圖示跳動） opt.termguicolors = true -- 24-bit 真彩色 opt.updatetime = 250 -- 更新延遲（影響 diagnostic / CursorHold） opt.clipboard = \u0026#34;unnamedplus\u0026#34; -- 與系統剪貼簿共用 2.3 外掛總覽 # 檔案 外掛 用途 colorscheme.lua rebelot/kanagawa.nvim 配色（dragon 主題） lsp.lua mason + mason-lspconfig + nvim-lspconfig + conform.nvim LSP / 格式化 completion.lua nvim-cmp + LuaSnip 自動補全 telescope.lua telescope.nvim 模糊搜尋 treesitter.lua （停用，見下方） 語法解析 motion.lua hop.nvim + nvim-surround + substitute.nvim 快速跳轉 / 環繞 / 交換 autopairs.lua nvim-autopairs 自動補括號 testing.lua neotest + neotest-python 測試執行 lazygit.lua lazygit.nvim Git TUI 整合 yazi.lua yazi.nvim 檔案總管整合 幾個值得一提、且與 AI 協作直接相關的設定取向：\nLSP（lsp.lua）：用 mason 自動安裝 pyright（型別 / 補全）與 ruff（lint / format）。 採 NeoVim 0.11 新 API：vim.lsp.config() + vim.lsp.enable()，不再用 lspconfig.setup() （避免 deprecated 警告，見 §6）。conform.nvim 讓 Python 在存檔時自動以 ruff_format 格式化——AI 產出的程式碼存檔即統一風格。LSP 的即時診斷，也是審查 AI 改動最快的一道關。 測試（testing.lua）：neotest + neotest-python（runner = pytest）——AI 改完馬上在編輯器內 跑測試驗證，是 AI 協作裡最重要的回饋迴路。 補全（completion.lua）：nvim-cmp，來源優先序 nvim_lsp → luasnip →（次要）buffer / path。 搜尋（telescope.lua）：find_files、live_grep、document_symbols、buffer 切換——快速定位要交給 AI 的位置。 移動（motion.lua）：hop.nvim（兩字元跳轉）、nvim-surround（環繞符號）、substitute.nvim（交換）。 Treesitter（treesitter.lua）：NeoVim 0.11 已內建常用 parser，直接停用 nvim-treesitter 外掛、改用內建 vim.treesitter： { \u0026#34;nvim-treesitter/nvim-treesitter\u0026#34;, enabled = false }, 3. Yazi（檔案總管） # 設定檔 ~/.config/yazi/yazi.toml，目前極簡：\n[manager] show_hidden = false # 預設不顯示隱藏檔（可在 yazi 內按 . 暫時切換） 使用方式：終端機直接 yazi 獨立執行；或透過 yazi.nvim 外掛從 NeoVim 內呼叫 （open_for_directories = true 讓開啟目錄時也用 Yazi）。在 AI 協作時，常用它快速瀏覽專案結構、 確認 AI 新增 / 異動了哪些檔案。\nYazi 在 tmux 內執行時，終端能力協商需要額外設定，否則會出現亂碼鍵入問題，見 §6。\n4. tmux # tmux 是讓「Claude Code 與編輯器並存」的關鍵：一個窗格跑 Claude Code、另一個窗格編輯與跑測試， session 長駐不中斷。設定檔 ~/.tmux.conf：\n# 啟用滑鼠支援（可用滑鼠點擊切換 Pane 與調整大小） set -g mouse on # 視窗索引從 1 開始（方便對應鍵盤左手位置） set -g base-index 1 # Vi 風格快捷鍵 setw -g mode-keys vi # 分區窗格索引也從 1 開始 setw -g pane-base-index 1 # 自動重新編號視窗（刪除視窗後號碼自動遞補） set -g renumber-windows on # 允許 DCS/CSI passthrough，避免 yazi 等 TUI 的終端能力探測序列洩漏成鍵盤輸入 set -g allow-passthrough on # 告訴 tmux Ghostty 支援的能力，讓 yazi 不需自行 probe set -g default-terminal \u0026#34;tmux-256color\u0026#34; set -as terminal-features \u0026#34;,xterm-ghostty:RGB:hyperlinks:usstyle\u0026#34; set -as terminal-features \u0026#34;,*:RGB\u0026#34; # 新視窗 / split 繼承當前 pane 的工作目錄 bind c new-window -c \u0026#34;#{pane_current_path}\u0026#34; bind \u0026#39;\u0026#34;\u0026#39; split-window -v -c \u0026#34;#{pane_current_path}\u0026#34; bind % split-window -h -c \u0026#34;#{pane_current_path}\u0026#34; 重點整理：\n設定 作用 mouse on 滑鼠切換窗格 / 調整大小 base-index 1 / pane-base-index 1 視窗與窗格皆從 1 起算 renumber-windows on 關閉視窗後自動遞補編號 allow-passthrough on 關鍵：讓 TUI 的終端探測序列正確 passthrough default-terminal \u0026quot;tmux-256color\u0026quot; 正確的 terminfo terminal-features 宣告 RGB / hyperlinks / 底線樣式能力 bind c / \u0026quot; / % 加 -c \u0026quot;#{pane_current_path}\u0026quot; 新窗格繼承當前目錄 5. LazyGit # LazyGit 是審查 AI 產出的把關站：AI 改一批，人在 LazyGit 用 TUI 逐個 hunk 檢視 diff、 選擇性 stage、再寫 commit，確保進版控的每一段都看過。\n設定檔 ~/.config/lazygit/config.yml 目前為空（沿用 LazyGit 預設）。 使用方式：終端機直接 lazygit 獨立執行；或透過 lazygit.nvim 外掛從 NeoVim 內呼叫。 若日後要自訂（例如 pager、commit 範本、custom commands），在此 yml 補上即可。\n6. 安裝 / 還原步驟 # 在新機器上重建環境的大致順序：\n安裝四套工具（NeoVim ≥ 0.11、Yazi、tmux、LazyGit），以及 Claude Code。 還原設定檔： ~/.config/nvim/ ~/.config/yazi/yazi.toml ~/.tmux.conf ~/.config/lazygit/config.yml 啟動 NeoVim，lazy.nvim 會自動 bootstrap 並安裝所有外掛；mason 會自動安裝 pyright、ruff。 tmux 重新載入設定：tmux source-file ~/.tmux.conf（或重開 session）。 用 lazy-lock.json 鎖定的 commit 還原套件版本：NeoVim 內執行 :Lazy restore。 健康檢查：\nnvim --headless -c \u0026#34;checkhealth\u0026#34; -c \u0026#34;qa\u0026#34; 7. 踩到的坑與解法 # 坑 1：Yazi 在 tmux 內出現亂碼 / 鍵盤被插入怪字元 # 現象：在 tmux 內開 Yazi（或其他 TUI），畫面出現奇怪字元，或鍵盤輸入被插入無意義序列。\n原因：Yazi 啟動時會送出終端能力「探測序列」（DCS / CSI），tmux 預設不會把這些序列正確傳遞給外層終端（Ghostty），導致回應序列被當成鍵盤輸入。\n解法（已寫進 .tmux.conf）：\nset -g allow-passthrough on set -g default-terminal \u0026#34;tmux-256color\u0026#34; set -as terminal-features \u0026#34;,xterm-ghostty:RGB:hyperlinks:usstyle\u0026#34; set -as terminal-features \u0026#34;,*:RGB\u0026#34; allow-passthrough on：允許 DCS/CSI passthrough。 直接用 terminal-features 宣告 Ghostty 支援的能力，讓 Yazi 不需自行 probe，從源頭避免探測序列外洩。 坑 2：NeoVim 0.11 的 LSP deprecated 警告 # 現象：用傳統 require(\u0026quot;lspconfig\u0026quot;).pyright.setup{} 會在 0.11 跳出 deprecated 警告。\n原因：NeoVim 0.11 推出內建 LSP 設定 API，舊的 lspconfig.setup() 流程被標記為過時。\n解法（已寫進 lsp.lua）：改用新 API——\nvim.lsp.config(\u0026#34;pyright\u0026#34;, { capabilities = ..., on_attach = ... }) vim.lsp.config(\u0026#34;ruff\u0026#34;, { capabilities = ..., on_attach = ... }) vim.lsp.enable({ \u0026#34;pyright\u0026#34;, \u0026#34;ruff\u0026#34; }) nvim-lspconfig 仍保留，但只拿它的 server 預設值，不呼叫它的 setup()。\n坑 3：nvim-treesitter 與 0.11 內建 parser 衝突 / 多餘 # 現象：裝了 nvim-treesitter 反而與內建 parser 重複，徒增維護成本。\n原因：NeoVim 0.11 已內建 python、markdown、lua 等常用語言的 treesitter parser。\n解法：直接停用 nvim-treesitter（enabled = false），改用內建 vim.treesitter。\n坑 4：貼上時內容被刪除動作覆蓋 # 現象：剛複製（yank）的內容，做了刪除動作後再貼上就不見了。\n原因：NeoVim 的 unnamed register 會同時被刪除（d / x）覆蓋。\n解法：改用 yank 專用暫存器 \u0026quot;0（只記錄 yank 內容、不受刪除影響），把貼上動作綁到從 \u0026quot;0 取值，貼上才穩定。\n坑 5：clipboard 與系統剪貼簿不同步 # 現象：NeoVim 內複製的內容無法貼到其他應用程式（例如想把程式碼貼給 AI 對話）。\n解法（已寫進 options.lua）：\nopt.clipboard = \u0026#34;unnamedplus\u0026#34; 注意：Linux 上仍需安裝剪貼簿提供者（如 xclip / xsel / wl-clipboard），否則 unnamedplus 無作用。\n坑 6：signcolumn 抖動 # 現象：LSP 診斷圖示出現 / 消失時，文字會左右跳動。\n解法（已寫進 options.lua）：固定顯示 sign 欄——\nopt.signcolumn = \u0026#34;yes\u0026#34; 附錄：套件版本鎖定 # NeoVim 外掛版本由 ~/.config/nvim/lazy-lock.json 鎖定（git commit 級別）。 換機或還原時用 :Lazy restore 可重現完全相同的套件版本。主要外掛：\nlazy.nvim、kanagawa.nvim、mason.nvim / mason-lspconfig.nvim / nvim-lspconfig、 nvim-cmp（+ cmp-nvim-lsp / cmp-buffer / cmp-path / LuaSnip / cmp_luasnip）、 conform.nvim、telescope.nvim（+ plenary.nvim）、hop.nvim、nvim-surround、 substitute.nvim、nvim-autopairs、neotest（+ nvim-nio / neotest-python）、 lazygit.nvim、yazi.nvim。\n","date":"2026年5月1日","externalUrl":null,"permalink":"/zh-tw/posts/terminal-dev-environment/","section":"技術文章","summary":"Claude Code 是終端原生的 AI 協作工具。用 tmux + NeoVim + Yazi + LazyGit 把它包進一個全鍵盤、全終端的工作流，讓人專注在『審查與把關 AI 的產出』，並記錄設定時踩到的坑。","title":"為 AI 協作打造的終端機開發環境：NeoVim + Yazi + tmux + LazyGit × Claude Code","type":"posts"},{"content":"","date":"2026年5月1日","externalUrl":null,"permalink":"/zh-tw/tags/%E7%B5%82%E7%AB%AF%E6%A9%9F/","section":"Tags","summary":"","title":"終端機","type":"tags"},{"content":"本指南整理我導入 AI 協同開發的實作經驗，幫助開發者快速理解 Agent.md（CLAUDE.md / AGENTS.md）與 Skills 這兩個功能，並善用 meta-prompt（讓 AI 幫你生成設定檔）把它們導入實際工作流。\n1. 核心概念 # Agent.md = 專案永久記憶（always loaded）\nSkills = 按需求提取的專家技能（on-demand loaded）\nAgent.md Skills 載入時機 每次對話自動載入 呼叫時才載入 內容性質 專案架構、規範、指令 特定任務的 SOP 更新頻率 隨專案演進 隨工作流程優化 Token 影響 固定消耗 按需消耗 跨平台使用\nAgent.md（CLAUDE.md）：Claude Code、Codex 與多數工具都通用。 SKILL.md 遵循 Agent Skills Standard，30+ 工具通用（Claude Code、Codex、Cursor、Gemini CLI、GitHub Copilot 等）。一份 SKILL.md 寫一次，到處都能用。 2. Agent.md / CLAUDE.md 建置 # 2.1 結構骨架 # 一份開發專案的 CLAUDE.md 建議包含以下 section（每個 section 都有明確職責）：\n## Conversation — 語言偏好與回覆風格 ## Architecture Overview — 分層架構、目錄結構（表格） ## Technology Stack — 語言版本、框架、核心依賴 ## Build and Development Commands — 建置/執行/測試指令（code block） ## Configuration — 環境配置、profiles、環境變數 ## Development Notes — 架構慣例、命名規範、常見模式 ## Key Business Domains — 核心業務領域 ## Testing Strategy — 測試框架、Mock 策略、已知陷阱（❌/✅ 對比） 2.2 Meta-Prompt 模板 # 不需要手寫 CLAUDE.md！以下 prompt 可直接貼進 AI 工具，讓它掃描 codebase 自動產出（Claude Code 和 Codex 也都能用 /init 初始化專案）：\n請你掃描整個 codebase，然後依照以下結構產出 markdown： 1. Conversation — 開發者的語言偏好、回覆風格 2. Architecture Overview — 專案類型、目錄結構、各模組用途（用表格） 3. Technology Stack — 語言版本、框架、主要依賴 4. Build and Development Commands — 建置、執行、測試的常用指令 5. Configuration — 環境配置方式（profiles、環境變數） 6. Development Notes — 架構慣例、命名規範、常見模式 7. Key Business Domains — 核心業務領域簡述 8. Testing Strategy — 測試框架、Mock 策略、已知陷阱 要求： - 目錄結構用表格呈現（模組名 | 用途） - 指令用 code block 加註解 - 陷阱/踩坑經驗用 ❌/✅ 對比格式 - 總長度控制在 200-400 行 - 只寫 AI 協作時真正需要的資訊，不要寫顯而易見的內容 2.3 重點呈現技巧 # 以一個 Java 微服務專案為例，幾個讓 CLAUDE.md 更好用的做法：\nArchitecture Overview — 用表格列出各微服務模組，一目瞭然：\n| 服務名稱 | 用途 | |---------|------| | account-service | 帳戶管理（用戶帳戶、餘額、交易記錄） | | order-service | 訂單管理（訂單建立、狀態追蹤） | | ... | ... | Testing Strategy — 用 ❌/✅ 對比記錄踩坑經驗，AI 每次都會遵守：\n// ❌ 錯誤：null 會被 MyBatis Plus 跳過，DB NOT NULL 欄位報錯 config.setRemark(null); // ✅ 正確：空字串會被包含在 INSERT SQL 中 config.setRemark(\u0026#34;\u0026#34;); Build Commands — code block 加註解，AI 需要建置時直接複製：\n# Use project Java version source ~/.sdkman/bin/sdkman-init.sh \u0026amp;\u0026amp; sdk use java \u0026lt;version\u0026gt; # Build all modules mvn clean install # Run specific test class mvn test -Dtest=OrderControllerTest 3. Skill 建置 # 3.1 SKILL.md 結構 # 每個 Skill 是一個 .claude/skills/{name}/SKILL.md（或 .codex/skills/{name}/SKILL.md）檔案，結構很簡單：\n--- name: skill-name # 必填：呼叫名稱 description: 一段描述... # 必填：用途說明（也是自動觸發的比對依據） --- # Skill 標題 這裡放 Markdown 格式的指令內容。 可以用 $ARGUMENTS 接收使用者傳入的參數。 可選：在 references/ 子目錄放範本檔案，SKILL.md 內可引用。\n解剖範例 — 一個 code-review skill 的 frontmatter：\n--- name: code-review description: 對後端系統程式碼進行全面 Code Review，涵蓋架構規範符合性、 命名規範、回傳包裝、安全性、業務邏輯、分散式事務、性能 （N+1 查詢、快取）、Null 安全及測試覆蓋。當完成功能開發、準備提交 PR、 或需要程式碼品質審查時使用，輸出 🔴🟡🟢 分級問題報告。 --- description 越精確，自動觸發就越準確。\n最簡範例 — grill-me skill（僅數行 body）：\n--- name: grill-me description: Interview the user relentlessly about a plan or design until reaching shared understanding. Use when user wants to stress-test a plan. --- Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree. If a question can be answered by exploring the codebase, explore instead. 進階範例 — 含 references 子目錄 + MCP 整合：\n.claude/skills/task-flow/ ├── SKILL.md # 主指令（含多個 Step） └── references/ ├── api-change-template.md # API 改動說明 HTML 範本 └── test-report-template.md # 自測報告 HTML 範本 SKILL.md 內可引用 references/api-change-template.md 來確保輸出格式一致——這是 Skill 管理範本檔的推薦做法。Skill 內整合的 MCP 工具也可替換成你習慣的工具（如 Notion、Obsidian 等支援 MCP 的工具）。\n3.2 用 skill-creator 產生 Skill（6 步流程） # 不需要手寫 SKILL.md！Claude Code 和 Codex 都內建了 /skill-creator：\n輸入 /skill-creator 用自然語言描述需求（例如：「我需要一個 skill，在我提到任務編號時自動從任務系統讀取卡片、分析需求、產出 API 文件和測試報告」） skill-creator 互動詢問細節（觸發條件？輸出格式？參數？） 自動產出 .claude/skills/{name}/SKILL.md 用 /skill-name 測試 跨工具通用：同一份 SKILL.md 直接被 Codex、Cursor 等工具讀取 4. Claude Code 使用教學 # 4.1 設置 CLAUDE.md # 放在專案根目錄，Claude Code 啟動時自動讀取。 支援子目錄放額外 CLAUDE.md（就近覆蓋），例如 services/order-service/CLAUDE.md 可放該服務特有的規範。 不需要任何額外設定，放了就生效。 4.2 用 /skill-creator 產生 skill # 實際操作流程（對話範例）：\n你：/skill-creator AI：你想建立什麼類型的 skill？請描述它的用途。 你：我需要一個 code review skill，針對後端系統的架構規範、 安全性、業務邏輯、分散式事務進行全面審查 AI：好的，我有幾個問題： 1. 審查目標怎麼指定？路徑還是 PR？ 2. 輸出格式偏好？ 3. 需要哪些審查維度？ 你：[回答...] AI：[產出 .claude/skills/code-review/SKILL.md] 4.3 呼叫 Skill # 明確呼叫（斜線命令）：\n/code-review services/order-service /write-test OrderServiceImpl.createOrder /task-flow TASK-1234 自動觸發（根據 description 文字比對）：\n在對話中提到任務編號 → 自動觸發 task-flow skill 提到「做個 code review」→ 自動觸發 code-review skill 提到「需要新 API」→ 自動觸發 new-api skill 關鍵：description 要寫清楚觸發條件和邊界。\n4.4 一組常見的團隊 Skills 範例 # 實務上可以針對團隊的重複性工作各做一個 skill。以下是一組示意：\nSkill 用途 觸發時機 code-review 全面 Code Review（多維度、🔴🟡🟢 分級報告） 提交 PR 前 write-test 撰寫 JUnit 5 單元測試（Given/When/Then） 需要新增測試 task-flow 任務完整工作流（分析→spec→API 文件→自測報告） 提到任務編號 new-api 新增 API 端點骨架（Controller→Service→VO→DTO→ErrorCode） 需要新 API new-service 建立新微服務完整骨架 需要新服務 new-feign-client 新增 Feign Client（自動查服務註冊中心，如 Eureka） 跨服務呼叫 review-security 安全性專項審查（IDOR、敏感資源存取、Token 驗證） 重要操作審查 review-transaction 分散式事務審查（事務邊界、undolog） 多服務寫入 annual-review 全面安全掃描（OWASP Top 10、P0–P3 分級） 安全審計 grill-me 計畫/設計深度訪談（逐層拆解決策樹） 壓力測試方案 5. Codex 使用教學 # 5.1 差異對照表 # 項目 Claude Code Codex 專案記憶檔 CLAUDE.md AGENTS.md 層級覆蓋 子目錄 CLAUDE.md 子目錄 AGENTS.md + override 檔 大小限制 無明確限制 32 KiB 預設 Skills 格式 .claude/skills/*/SKILL.md 相同（通用標準）.codex/skills/*/SKILL.md Skill 產生器 /skill-creator 內建 $skill-creator 內建 自動觸發設定 description 文字比對 agents/openai.yaml 可額外設定 5.2 要點 # Skills 完全通用，不需修改。用 Claude Code 的 /skill-creator 產生後，Codex 直接讀取同一份 SKILL.md。 Codex 有 agents/openai.yaml 可設定 allow_implicit_invocation: false 關閉自動觸發。 AGENTS.md 的內容結構跟 CLAUDE.md 類似，但兩者不通用（檔名不同、各自讀各自的）。 建議做法：維護一份 CLAUDE.md 作為主版本，需要時複製為 AGENTS.md 並微調。 6. 實戰案例：用 grill-me Skill 產生本指南 # 這份指南本身就是透過 /grill-me skill 產生的，以下是這次體驗的分析。\n流程回顧：\n輸入 /grill-me + 大綱草稿 grill-me 逐層拆解決策分支（目標受眾 → 範圍 → 格式 → 內容深度） 每輪 2–3 個問題，附帶建議答案 所有分支解決後自動整合為完整計畫 優點：\n強迫思考：每個「我以為想清楚了」的部分都被追問出盲點（如 AGENTS.md vs CLAUDE.md 不通用）。 決策留痕：一連串 Q\u0026amp;A 形成完整的設計決策記錄。 減少返工：在動手寫之前就解決了格式、比重、範例呈現方式等爭議。 自動研究：grill-me 過程中主動查了 agentskills.io 確認 Skills 通用性。 缺點 / 注意事項：\n耗時：數輪問答約 15–20 分鐘，簡單任務不需要這麼重的流程。 Token 消耗：深度訪談 + 網路搜尋 + codebase 探索，Token 用量較高。 適用場景：適合「方向不明確」或「影響範圍大」的任務；明確的 bug fix 或小功能直接做即可。 結論：grill-me 適合用在「寫之前需要想清楚」的場景，如技術文件、架構設計、新功能規劃。\n7. 外部學習資源 # Claude Code 官方文件 Claude Code Skills Codex 官方文件 Codex AGENTS.md Agent Skills 開放標準 The Complete Guide to Building Skills for Claude Claude Code Skills：讓 AI 變身專業工匠 ","date":"2026年3月31日","externalUrl":null,"permalink":"/zh-tw/posts/ai-collaboration-guide/","section":"技術文章","summary":"如何用 Agent.md（CLAUDE.md / AGENTS.md）與 Skills 提升 AI 協同開發效率：核心概念、meta-prompt、skill-creator，以及 Claude Code 與 Codex 的差異。","title":"AI 協作開發指南：Agent.md + Skills 實戰手冊","type":"posts"},{"content":"","date":"2026年3月31日","externalUrl":null,"permalink":"/zh-tw/tags/codex/","section":"Tags","summary":"","title":"Codex","type":"tags"},{"content":"","date":"2026年3月31日","externalUrl":null,"permalink":"/zh-tw/tags/skills/","section":"Tags","summary":"","title":"Skills","type":"tags"},{"content":"","date":"1 March 2026","externalUrl":null,"permalink":"/tags/computer-vision/","section":"Tags","summary":"","title":"Computer Vision","type":"tags"},{"content":"","date":"2026年3月1日","externalUrl":null,"permalink":"/zh-tw/tags/fastapi/","section":"Tags","summary":"","title":"FastAPI","type":"tags"},{"content":"一套生產環境的身分驗證服務,判斷人臉驗證提交的影像來自真實活人,還是攻擊樣本或 深偽(deepfake)。\n做了什麼 # 多段式判決流程:品質閘門 → 活體閘門 → 模型共識,回傳 accept / review / reject 並附上原因。 結合物理活體訊號(多張影像的 RGB 光反應檢查)與多模型深度學習集成 進行深偽偵測。 以 HTTP API 形式提供同步驗證。 技術 # 模型 — 卷積網路(EfficientNet / Xception / CLIP 系列)搭配梯度提升模型, 以共識方式整合。 技術棧 — Python、PyTorch、ONNX、FastAPI;完整的訓練 → 評估 → 部署 pipeline,含自動重訓與報告產出。 成果 — 多模型在保留測試集 AUC ≥ 0.99;並調校至可在 CPU 上符合 KYC 延遲需求。 ","date":"2026年3月1日","externalUrl":null,"permalink":"/zh-tw/projects/kyc-liveness-detection/","section":"專案","summary":"一套生產環境的電腦視覺服務，判斷人臉驗證的影像來自真實活人，還是攻擊樣本／深偽。","title":"KYC 活體與深偽偵測","type":"projects"},{"content":"","date":"1 March 2026","externalUrl":null,"permalink":"/tags/machine-learning/","section":"Tags","summary":"","title":"Machine Learning","type":"tags"},{"content":"","date":"2026年3月1日","externalUrl":null,"permalink":"/zh-tw/tags/onnx/","section":"Tags","summary":"","title":"ONNX","type":"tags"},{"content":"","date":"2026年3月1日","externalUrl":null,"permalink":"/zh-tw/tags/pytorch/","section":"Tags","summary":"","title":"PyTorch","type":"tags"},{"content":"我做過的一些東西。內容僅描述到領域與技術層級。\n","date":"2026年3月1日","externalUrl":null,"permalink":"/zh-tw/projects/","section":"專案","summary":"我做過的一些東西。內容僅描述到領域與技術層級。\n","title":"專案","type":"projects"},{"content":"","date":"2026年3月1日","externalUrl":null,"permalink":"/zh-tw/tags/%E9%9B%BB%E8%85%A6%E8%A6%96%E8%A6%BA/","section":"Tags","summary":"","title":"電腦視覺","type":"tags"},{"content":"","date":"2026年3月1日","externalUrl":null,"permalink":"/zh-tw/tags/%E6%A9%9F%E5%99%A8%E5%AD%B8%E7%BF%92/","section":"Tags","summary":"","title":"機器學習","type":"tags"},{"content":"","date":"2026年2月11日","externalUrl":null,"permalink":"/zh-tw/tags/feign/","section":"Tags","summary":"","title":"Feign","type":"tags"},{"content":"","date":"2026年2月11日","externalUrl":null,"permalink":"/zh-tw/tags/java/","section":"Tags","summary":"","title":"Java","type":"tags"},{"content":"","date":"11 February 2026","externalUrl":null,"permalink":"/tags/microservices/","section":"Tags","summary":"","title":"Microservices","type":"tags"},{"content":"","date":"2026年2月11日","externalUrl":null,"permalink":"/zh-tw/tags/spring-cloud/","section":"Tags","summary":"","title":"Spring Cloud","type":"tags"},{"content":"在 Spring Cloud 微服務裡，跨服務呼叫常用 OpenFeign。傳統（legacy）寫法的問題是：服務端 Controller 和消費端的 Feign Client 各寫一份——HTTP 路徑、請求/回應物件分散在兩處，介面一改就容易兩邊 drift，DTO 也常被複製貼上。\n這篇分享一個改善作法：把 Feign API 介面、VO、DTO 全部集中在一個共享模組，當作服務之間的單一契約。服務端 implements 這個介面作為 Controller，消費端直接 @Autowired 注入即可呼叫。介面只有一份，兩邊都依賴它，自然不會 drift。\n以下以一個電商「訂單服務」為完整範本（order 領域）。\n1. 架構概覽 # ┌─────────────────────────────────────────────────────────────────┐ │ common-api（共享模組） │ │ ├── api/ Feign API 介面定義（@FeignClient） │ │ ├── vo/ 請求物件（Value Object） │ │ └── dto/ 回應物件（Data Transfer Object） │ └────────────────────────────┬────────────────────────────────────┘ │ Maven 依賴 ┌───────────────────┼───────────────────┐ ▼ ▼ ┌─────────────────────┐ ┌─────────────────────────┐ │ order-service │ │ 消費端微服務 │ │ （服務端實作） │ │ （呼叫端） │ │ ├── controller/ │ │ │ │ │ implements API │◄──── Feign ───│ @Autowired │ │ ├── service/ │ HTTP Call │ OrderApi api; │ │ └── service/impl/ │ │ │ └─────────────────────┘ └─────────────────────────┘ 核心設計：API 介面定義在 common-api，服務端 implements 該介面作為 Controller，消費端直接 @Autowired 注入即可透過 Feign 呼叫。\n2. 完整開發步驟 # Step 1：在 common-api 定義 VO（請求物件） # 目錄：common-api/src/main/java/com/example/commonapi/order/vo/\nVO 用於封裝 API 的請求參數，規範：\n實作 Serializable，定義 serialVersionUID 使用 Lombok 四件組：@Data @Builder @AllArgsConstructor @NoArgsConstructor 使用 javax.validation 註解做參數驗證 範例：CreateOrderVO.java\npackage com.example.commonapi.order.vo; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import java.io.Serializable; /** * 建立訂單請求 VO */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public class CreateOrderVO implements Serializable { private static final long serialVersionUID = 1L; @NotBlank(message = \u0026#34;顧客ID不能為空\u0026#34;) private String customerId; @NotBlank(message = \u0026#34;商品ID不能為空\u0026#34;) private String productId; @NotNull(message = \u0026#34;數量不能為空\u0026#34;) private Integer quantity; @NotBlank(message = \u0026#34;金額不能為空\u0026#34;) private String amount; /** 備註，可為空 */ private String remark; @NotNull(message = \u0026#34;下單時間不能為空\u0026#34;) private Long requestTime; } 範例：UpdateOrderStatusVO.java\npackage com.example.commonapi.order.vo; import com.example.commonapi.order.enums.OrderStatusEnum; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import java.io.Serializable; @Data @Builder @AllArgsConstructor @NoArgsConstructor public class UpdateOrderStatusVO implements Serializable { private static final long serialVersionUID = 1L; @NotBlank(message = \u0026#34;OrderId 不可為空\u0026#34;) String orderId; @NotNull(message = \u0026#34;訂單狀態不可為空\u0026#34;) OrderStatusEnum status; } Step 2：在 common-api 定義 DTO（回應物件） # 目錄：common-api/src/main/java/com/example/commonapi/order/dto/\nDTO 用於封裝 API 的回應資料，規範與 VO 相同。\n範例：OrderResultDTO.java\npackage com.example.commonapi.order.dto; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; /** * 訂單處理結果 DTO */ @Data @Builder @AllArgsConstructor @NoArgsConstructor public class OrderResultDTO implements Serializable { private static final long serialVersionUID = 1L; /** 是否成功受理 */ private boolean accepted; /** 結果說明 */ private String message; /** 訂單ID，用於追蹤 */ private String orderId; } Step 3：在 common-api 定義 Feign API 介面 # 目錄：common-api/src/main/java/com/example/commonapi/order/api/\n這是整個架構的核心——用 @FeignClient 定義介面，服務端實作它，消費端注入它。\n範例：OrderApi.java\npackage com.example.commonapi.order.api; import com.example.commonapi.order.dto.OrderResultDTO; import com.example.commonapi.order.vo.CreateOrderVO; import com.example.commonapi.order.vo.UpdateOrderStatusVO; import com.example.common.Result; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; /** * 訂單服務 API * 提供建立訂單與狀態更新功能 */ @FeignClient(name = \u0026#34;order-service\u0026#34;, path = \u0026#34;/api/order\u0026#34;) public interface OrderApi { @PostMapping(\u0026#34;/createOrder\u0026#34;) Result\u0026lt;OrderResultDTO\u0026gt; createOrder( @RequestBody @Validated CreateOrderVO vo); @PostMapping(\u0026#34;/updateOrderStatus\u0026#34;) Result\u0026lt;Void\u0026gt; updateOrderStatus( @RequestBody @Validated UpdateOrderStatusVO vo); } @FeignClient 參數說明：\n參數 說明 範例 name 目標服務在 Eureka 註冊的名稱 \u0026quot;order-service\u0026quot; path Controller 層的統一路徑前綴 \u0026quot;/api/order\u0026quot; 方法註解說明：\n註解 說明 @PostMapping 定義 HTTP 端點路徑 @RequestBody 參數以 JSON body 傳遞 @Validated 啟用參數驗證（觸發 VO 中的 @NotBlank 等） Step 4：在服務端建立 Controller 實作 API 介面 # 目錄：order-service/src/main/java/.../controller/\nController 直接 implements API 介面，不需要再寫 @RequestMapping、@PostMapping 等路徑註解——全部繼承自介面定義。\n範例：OrderController.java\npackage com.example.orderservice.controller; import com.example.commonapi.order.api.OrderApi; import com.example.commonapi.order.dto.OrderResultDTO; import com.example.commonapi.order.vo.CreateOrderVO; import com.example.commonapi.order.vo.UpdateOrderStatusVO; import com.example.common.Result; import com.example.common.ResultUtil; import com.example.orderservice.service.OrderService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RestController; @Slf4j @RestController @RequiredArgsConstructor public class OrderController implements OrderApi { private final OrderService orderService; @Override public Result\u0026lt;OrderResultDTO\u0026gt; createOrder(CreateOrderVO vo) { log.info(\u0026#34;收到建立訂單請求: customerId={}, productId={}, amount={}\u0026#34;, vo.getCustomerId(), vo.getProductId(), vo.getAmount()); OrderResultDTO result = orderService.createOrder(vo); return ResultUtil.success(result); } @Override public Result\u0026lt;Void\u0026gt; updateOrderStatus(UpdateOrderStatusVO vo) { log.info(\u0026#34;收到訂單狀態更新請求: orderId={}, status={}\u0026#34;, vo.getOrderId(), vo.getStatus().getCode()); return orderService.updateOrderStatus( vo.getOrderId(), vo.getStatus()); } } 重點：\n@RestController 即可，不需要 @RequestMapping 路徑由介面的 @FeignClient(path=...) + @PostMapping(...) 組合決定 使用 @RequiredArgsConstructor 搭配 private final 做建構子注入 Step 5：在服務端建立 Service 介面與實作 # Service 介面 — order-service/src/main/java/.../service/\npackage com.example.orderservice.service; import com.example.commonapi.order.dto.OrderResultDTO; import com.example.commonapi.order.vo.CreateOrderVO; import com.example.commonapi.order.enums.OrderStatusEnum; import com.example.common.Result; public interface OrderService { OrderResultDTO createOrder(CreateOrderVO vo); Result\u0026lt;Void\u0026gt; updateOrderStatus(String orderId, OrderStatusEnum status); } Service 實作 — order-service/src/main/java/.../service/impl/\npackage com.example.orderservice.service.impl; import com.example.commonapi.order.dto.OrderResultDTO; import com.example.commonapi.order.vo.CreateOrderVO; import com.example.orderservice.service.OrderService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @Slf4j @Service @RequiredArgsConstructor public class OrderServiceImpl implements OrderService { @Override public OrderResultDTO createOrder(CreateOrderVO vo) { // 實作業務邏輯... } @Override public Result\u0026lt;Void\u0026gt; updateOrderStatus(String orderId, OrderStatusEnum status) { // 實作業務邏輯... } } Step 6：在消費端微服務注入使用 # 6.1 啟動類配置 @EnableFeignClients # 消費端的 Spring Boot 啟動類必須掃描到 com.example.commonapi 才能自動裝配 Feign Client。\n範例：ShopServiceApplication.java\npackage com.example.shopservice; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.openfeign.EnableFeignClients; @EnableEurekaClient @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) @EnableDiscoveryClient @EnableFeignClients(basePackages = { \u0026#34;com.example.shopservice\u0026#34;, // 本服務的 Feign Client \u0026#34;com.example.commonapi\u0026#34; // ← 關鍵！掃描 common-api 中的 API 介面 }) public class ShopServiceApplication { public static void main(String[] args) { SpringApplication.run(ShopServiceApplication.class, args); } } 6.2 在 Service 中注入 API 介面 # 直接用建構子注入 OrderApi，像呼叫本地方法一樣使用：\npackage com.example.shopservice.service.impl; import com.example.commonapi.order.api.OrderApi; import com.example.commonapi.order.dto.OrderResultDTO; import com.example.commonapi.order.vo.CreateOrderVO; import com.example.common.Result; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class CheckoutServiceImpl implements CheckoutService { private final OrderApi orderApi; // ← Feign 自動注入 private void someMethod() { // 構建請求 CreateOrderVO orderVO = CreateOrderVO.builder() .customerId(\u0026#34;12345\u0026#34;) .productId(\u0026#34;SKU-001\u0026#34;) .quantity(2) .amount(\u0026#34;1000\u0026#34;) .requestTime(System.currentTimeMillis()) .build(); // 呼叫遠端服務（透過 Feign，看起來像本地呼叫） Result\u0026lt;OrderResultDTO\u0026gt; response = orderApi.createOrder(orderVO); // 處理回應 if (response.getSuccess() \u0026amp;\u0026amp; response.getData() != null) { OrderResultDTO result = response.getData(); if (result.isAccepted()) { // 後續處理... } } } } 3. 檔案總覽 # 層級 檔案 路徑 VO CreateOrderVO.java common-api/.../order/vo/ VO UpdateOrderStatusVO.java common-api/.../order/vo/ DTO OrderResultDTO.java common-api/.../order/dto/ API 介面 OrderApi.java common-api/.../order/api/ Controller OrderController.java order-service/.../controller/ Service OrderService.java order-service/.../service/ Service Impl OrderServiceImpl.java order-service/.../service/impl/ 消費端使用 CheckoutServiceImpl.java shop-service/.../service/impl/ 啟動類 ShopServiceApplication.java shop-service/ 4. 關鍵注意事項 # Package 結構慣例 # com.example.commonapi.{domain} ├── api/ API 介面（@FeignClient） ├── vo/ 請求物件 └── dto/ 回應物件 {domain} 依功能領域命名，如 order、product、notify VO 和 DTO 都放在 common-api，讓服務端和消費端都能使用 命名慣例 # 類型 命名規則 範例 API 介面 {Domain}Api OrderApi 請求物件 {Action}{Domain}VO CreateOrderVO 回應物件 {Domain}{Action}DTO OrderResultDTO Controller {Domain}Controller OrderController Service {Domain}Service OrderService Service Impl {Domain}ServiceImpl OrderServiceImpl 驗證 # 在 VO 中使用 javax.validation 註解（@NotBlank、@NotNull 等） 在 API 介面方法參數加 @Validated 啟用驗證 Controller 不需要再加 @Validated，繼承自介面即可 序列化 # VO 和 DTO 都必須實作 Serializable 定義 serialVersionUID 確保版本相容 金額這類需要精度的欄位，避免用浮點數（可用 String 或 BigDecimal），防止精度問題 時間戳使用 Long（毫秒級，13 位數） 回傳值 # 所有 API 方法統一回傳 Result\u0026lt;T\u0026gt; 包裝器 無回傳資料時使用 Result\u0026lt;Void\u0026gt; 服務端使用 ResultUtil.success(data) / ResultUtil.error(...) 構建回應 @FeignClient 配置 # name 必須與目標服務在 Eureka 的註冊名稱一致 path 對應目標服務 Controller 的統一路徑前綴 不要在 API 介面上加 @RequestMapping（只在方法級別用 @PostMapping） 消費端必要配置 # @EnableFeignClients 必須包含 \u0026quot;com.example.commonapi\u0026quot; 才能掃描到 common-api 中的 API 介面：\n@EnableFeignClients(basePackages = { \u0026#34;com.your.service\u0026#34;, // 本服務 \u0026#34;com.example.commonapi\u0026#34; // common-api 的 API }) 遺漏此配置會導致 NoSuchBeanDefinitionException。\n","date":"2026年2月11日","externalUrl":null,"permalink":"/zh-tw/posts/feign-api-shared-module/","section":"技術文章","summary":"把 Feign 介面、VO、DTO 集中在一個共享模組：服務端 implements 介面當 Controller、消費端直接注入呼叫，消除 legacy 寫法中介面與實作各寫一份、容易 drift 的重複。","title":"用共享模組統一 Feign API 契約：Spring Cloud 微服務介面共享實戰","type":"posts"},{"content":"","date":"2026年2月11日","externalUrl":null,"permalink":"/zh-tw/tags/%E5%BE%AE%E6%9C%8D%E5%8B%99/","section":"Tags","summary":"","title":"微服務","type":"tags"},{"content":"","date":"2025年5月19日","externalUrl":null,"permalink":"/zh-tw/tags/devops/","section":"Tags","summary":"","title":"DevOps","type":"tags"},{"content":"","date":"2025年5月19日","externalUrl":null,"permalink":"/zh-tw/tags/redis/","section":"Tags","summary":"","title":"Redis","type":"tags"},{"content":"開發與維護一套支撐客戶管理與交易系統的 Spring Cloud 微服務平台。\n重點 # 模組化與解耦 — 重構舊有模組,降低服務間耦合、提高聚合。 效能 — 優化熱點路徑,提升併發吞吐。 分散式交易 — 以 Seata 維持跨服務一致性,並用 Redis 做快取與協調。 框架升級 — 將服務從 Spring Boot 2.x 升級至 3.1。 品質 — 將測試覆蓋率從 0% 提升至約 40%,並把測試階段導入 CI/CD。 維運 — 以 Grafana 儀表板監控線上狀況,快速回饋與處理問題。 ","date":"2025年5月19日","externalUrl":null,"permalink":"/zh-tw/projects/backend-microservices/","section":"專案","summary":"客戶管理與交易平台的 Spring Cloud 微服務 — 重構、效能與工程實踐。","title":"後端微服務平台","type":"projects"},{"content":"","date":"27 September 2024","externalUrl":null,"permalink":"/tags/database/","section":"Tags","summary":"","title":"Database","type":"tags"},{"content":"","date":"2024年9月27日","externalUrl":null,"permalink":"/zh-tw/tags/liquibase/","section":"Tags","summary":"","title":"Liquibase","type":"tags"},{"content":"","date":"2024年9月27日","externalUrl":null,"permalink":"/zh-tw/tags/spring-boot/","section":"Tags","summary":"","title":"Spring Boot","type":"tags"},{"content":"本文整理自我在 JCConf 2024 的分享。\n在現代軟體開發中，程式碼的版本控制已經是標配，但資料庫架構（Schema）的變動管理卻往往是開發流程中的痛點。本文將分享如何透過 Liquibase 這款強大的工具，協助客戶將資料庫遷移（Database Migration）自動化，並無縫整合進專案架構中。\n為什麼需要資料庫版本控制？ # 在深入工具之前，我們必須先釐清兩個容易混淆的概念：\n資料遷移（Data Migration）：側重於資料在不同系統、格式或儲存技術間的轉移。 資料庫遷移（Database Migration）：則是對關聯式資料庫進行版本控制，包含 Schema 的更新或 Rollback 操作。 在實際專案中，開發者常面臨多個測試環境版本不一、人為執行 SQL 風險高，以及資安考量下 DDL/DML 權限需嚴格區隔等挑戰。導入自動化版控工具，正是為了解決這些穩定性與安全性的問題。\n工具選型：Liquibase vs. Flyway # 在 Java 生態系中，Liquibase 與 Flyway 是兩大主流選擇。它們都支援主流資料庫與 CI/CD 流程，但核心理念有所不同：\n特性 Liquibase Flyway 腳本格式 支援 XML、YAML、JSON、SQL，具跨資料庫靈活性。 以純 SQL 為主，簡單直覺。 Rollback 社群版即支援基礎 Rollback。 僅企業版支援 Rollback。 學習曲線 功能強大但配置相對複雜。 基於慣例命名，輕量且易上手。 進階功能 具備資料庫 Diff 功能，可比對差異。 整合 Spring Properties 較佳。 考量到客戶對跨環境腳本的靈活性需求，以及對 Rollback 功能的重視，Liquibase 往往是更全面的選擇。\nLiquibase 核心運作機制 # Liquibase 的核心在於 Changelog 與 Changeset：\nChangeset：定義每一次的資料庫變動。 Changelog：將多個 Changeset 組織成檔案（如 changelog-master.yaml），作為執行入口。 Tracking Table：Liquibase 會在資料庫建立 DATABASECHANGELOG 表，記錄已執行的 ID、作者與 MD5SUM 校驗碼，確保變更不會重複執行且內容未經非法竄改。 實戰分享：將 Liquibase 整合進 Spring Boot 專案 # 在導入現有專案時，我們的目標是「簡化客戶端施行複雜度」，讓服務啟動時自動完成變更，而不需要額外學習 CLI 操作。\n1. 架構設計與權限隔離 # 為了符合資安規範，我們利用容器技術中的 InitContainer：\nInitContainer：使用具備 DDL 權限的帳號執行 update 指令。 Main Container：主服務啟動後改用僅具 DQL 權限的帳號，並執行 validate 再次確認版本正確性。 2. 客製化擴充功能 # Spring Boot 提供的 SpringLiquibase 預設僅支援 update 功能。為了應付更複雜的情境，我們可以透過繼承或擴充，加入 changelogSync 與 rollback 等指令支援。同時，實作 Interceptor（攔截器） 機制，可以在變動前後輸出類似 SQL Plus 的 LOG 紀錄，方便追蹤與審核。\n提升開發效率：JpaBuddy 的妙用 # 手寫 XML 或 YAML 的 Changeset 雖然嚴謹，但效率較低。透過 IntelliJ IDEA 的 JpaBuddy 插件，開發者可以直接根據撰寫好的 JPA Entity 自動生成 Liquibase 腳本。這大幅降低了開發者的負擔，也減少了人為手誤的機會。\n結語 # 自動化資料庫版控不只是安裝一個工具，更是一場開發流程的革新。從需求訪談、架構設計到最後的實作，Liquibase 提供了極高的靈活性與安全性保障。希望這份導入心得能幫助你在未來的專案中，更優雅地處理資料庫的版本難題。\n補充資訊與資源 # GitHub Repo：JCConf 2024 Liquibase Demo 常用指令：update、rollback、diff、changelogSync ","date":"2024年9月27日","externalUrl":null,"permalink":"/zh-tw/posts/liquibase-database-migration/","section":"技術文章","summary":"JCConf 2024 分享整理：如何用 Liquibase 將資料庫遷移自動化，並無縫整合進 Spring Boot 專案，兼顧穩定性與資安。","title":"從導入到整合：使用 Liquibase 實現自動化資料庫版本控制的心路歷程","type":"posts"},{"content":"","date":"2024年9月27日","externalUrl":null,"permalink":"/zh-tw/tags/%E8%B3%87%E6%96%99%E5%BA%AB/","section":"Tags","summary":"","title":"資料庫","type":"tags"},{"content":"核心目標： 追求「人機合一」的境界，透過工具與流程的極致優化，讓開發速度不再受到物理操作的限制。\n1. 核心觀念與前置準備 # 在進入極速開發之前，必須先建立正確的開發節奏與品質意識：\nCoding Dojo 與刻意練習： 透過反覆實作（如 Tennis Kata）來精進編碼技能。 TDD（Test-Driven Development）： 遵循「測試先行、小步快跑、持續重構」的循環，確保功能正確的同時也維持程式碼品質。 Clean Code 原則： 程式碼需具備易於理解、簡潔、可維護且功能清晰的特性，這是維持開發速度的基石。 2. 開發工具的極致運用 # 「工欲善其事，必先利其器」，現代 IDE 的功能與自定義快捷鍵是提升效率的關鍵：\n善用 IDE 功能： 善用程式碼智慧補齊與自動重構（重命名、提取函式等）來減少人工錯誤。 Vim 與 IdeaVim： 透過 Vim 指令操作來減少對滑鼠的依賴，讓手不離開鍵盤。 自定義 .ideavimrc： 把常用的 IDE 動作（Action）綁定到順手的鍵位，例如重命名、提取變數／方法、重排與格式化等，都可以用一個快捷鍵一氣呵成。 3. 實踐練習：Tennis Kata 與限時挑戰 # Workshop 中透過 Tennis Kata 這個經典案例進行練習，實現網球比賽的計分系統（Love、15、30、40、Deuce、Advantage）。\nShow Me The Code： 進行限時 5 分鐘的極速開發挑戰，測試在極短時間內能產出的測試與功能程式碼。 4. 結語：通往心流之路 # 真正的極速開發並非盲目求快，而是達到「眼到手到」的境界。透過對工具的熟悉與對 TDD 節奏的掌握，開發者可以節省體力耗費、避免低級的人工錯誤，進而專注於更高層次的系統設計與邏輯思考。\n練習錄影清單 # 當時的練習錄影整理在這份 YouTube 清單：\n91 極速開發練習清單（YouTube） ","date":"2023年11月14日","externalUrl":null,"permalink":"/zh-tw/posts/speed-coding-workshop/","section":"技術文章","summary":"追求「人機合一」：透過 TDD 節奏、刻意練習與 IDE／Vim 的極致運用，讓開發速度不再受物理操作拖累。","title":"91 極速開發 Workshop 參加心得","type":"posts"},{"content":"","date":"14 November 2023","externalUrl":null,"permalink":"/tags/productivity/","section":"Tags","summary":"","title":"Productivity","type":"tags"},{"content":"","date":"2023年11月14日","externalUrl":null,"permalink":"/zh-tw/tags/tdd/","section":"Tags","summary":"","title":"TDD","type":"tags"},{"content":"","date":"2023年11月14日","externalUrl":null,"permalink":"/zh-tw/tags/vim/","section":"Tags","summary":"","title":"Vim","type":"tags"},{"content":"","date":"2023年11月14日","externalUrl":null,"permalink":"/zh-tw/tags/%E7%94%9F%E7%94%A2%E5%8A%9B/","section":"Tags","summary":"","title":"生產力","type":"tags"},{"content":"","date":"24 December 2022","externalUrl":null,"permalink":"/tags/notes/","section":"Tags","summary":"","title":"Notes","type":"tags"},{"content":"這是我當初自學 Spring Boot 的整理筆記，內容對應 Hahow 課程 《Java 工程師必備！Spring Boot 零基礎入門》。 放上來當作自己的速查表，也希望對入門的人有幫助。\nSpring IoC # IoC（Inversion of Control）：將物件的控制權交由外部的 Spring 容器管理。\n優點：\nLoose coupling（鬆耦合） Lifecycle management（生命週期管理） More testable（方便測試） DI（Dependency Injection）：透過外部容器取得物件。\n@Component：加在 class 上，把 class 變成由 Spring 容器管理的 bean。 @Autowired：加在變數上，從 Spring 容器取得 bean（依賴注入）。 Bean 的注入 # @Autowired：根據變數的類型，在 Spring 容器中尋找符合類型的 bean。 @Qualifier：當同時有兩個 class 實作相同介面時，用 @Qualifier(\u0026quot;{bean}\u0026quot;) 指定 bean。 （bean 名稱為 class 首字大寫轉小寫。） Bean 的創建 # @Configuration + @Bean @Configuration：表示這個 class 用來設定 Spring。 @Bean：只能加在帶有 @Configuration 的 class 的方法上。 前述 @Component 的方式創建 bean。 Bean 的初始化 # 使用 @PostConstruct。 實作 InitializingBean 介面的 afterPropertiesSet() 方法。 @PostConstruct 的條件：方法必須是 public、回傳 void、且無參數傳入。\nBean 的生命週期 # 創建 → 初始化 → 拿來使用。 創建時若依賴其他 bean，Spring 會回頭先創建並初始化被依賴的 bean。 避免循環依賴。 Spring Boot 設定檔 # application.properties，搭配 @Value(\u0026quot;${key:default_value}\u0026quot;) 讀取設定值。\nSpring AOP # AOP（Aspect-Oriented Programming，切面導向程式設計）：\npom.xml：引入 spring-boot-starter-aop。 @Aspect + @Component：加在 class 上，宣告這個 class 是一個切面。 常用切面註解：@Before / @After / @Around，加在宣告的方法上。 AOP 常見應用：\n權限驗證：Spring Security 統一的 Exception 處理：@ControllerAdvice Log 記錄 Spring MVC # @RequestMapping # 用法：加在 class 或方法上，小括號內填寫 URL 路徑。 用途：將 URL 路徑對應到方法上。 註記：class 上一定要加 @Controller / @RestController。 @Controller / @RestController # 用法：只加在 class 上。 用途：把該 class 變成 bean，並可使用 @RequestMapping（等於 @Component 加強版）。 RESTful API # 目的：簡化工程師之間的溝通成本。 設計的 API 符合 REST 風格，就是 RESTful API。 它不是一個規範，使用最適當的作法即可。 取得請求參數 # 1. @RequestParam\n用法：只能加在方法的參數上。 用途：取得 URL 裡的參數（query parameter）。 設定：name（或 value）指定參數名、required（預設 true）、defaultValue（提供預設值）。 URL 上多傳的參數會被 Spring Boot 忽略。 @RequestMapping(\u0026#34;/test1\u0026#34;) public String test1(@RequestParam Integer id, @RequestParam(defaultValue = \u0026#34;Nick\u0026#34;) String name) { System.out.println(\u0026#34;id: \u0026#34; + id); System.out.println(\u0026#34;name: \u0026#34; + name); return \u0026#34;Hello test1\u0026#34;; } 2. @RequestBody\n用法：只能加在方法的參數上。 用途：取得 request body 裡的參數（將 JSON 轉為 Java Object）。 @RequestMapping(\u0026#34;/test2\u0026#34;) public String test2(@RequestBody Student student) { System.out.println(student); return \u0026#34;Hello test2\u0026#34;; } 3. @RequestHeader\n用法：只能加在方法的參數上。 用途：取得 request header 裡的參數。 設定：name（或 value，常用，因為 header 常有橫線 -）、required、defaultValue。 常見的 request header：\nRequest header 意義 常見的值 Content-Type request body 的格式 application/json（最常用）、application/octet-stream（上傳檔案）、multipart/form-data（上傳圖片） Authorization 用於身份驗證 @RequestMapping(\u0026#34;/test3\u0026#34;) public String test3(@RequestHeader String info) { System.out.println(\u0026#34;info: \u0026#34; + info); return \u0026#34;Hello Test3\u0026#34;; } 4. @PathVariable\n用法：只能加在方法的參數上。 用途：取得 URL 路徑中的值。 @RequestMapping(\u0026#34;/test4/{id}\u0026#34;) public String test4(@PathVariable Integer id) { System.out.println(\u0026#34;id: \u0026#34; + id); return \u0026#34;Hello Test4\u0026#34;; } 上述四種可以混用。\n回應狀態 # Spring Boot 預設返回的 HTTP 狀態碼：正常執行完方法返回 200；噴出 exception 返回 500。\nResponseEntity\u0026lt;?\u0026gt;：作為方法的返回類型，可自定義回傳的 HTTP response 細節。\n@RequestMapping(\u0026#34;/test\u0026#34;) public ResponseEntity\u0026lt;String\u0026gt; test() { return ResponseEntity.status(HttpStatus.ACCEPTED) .body(\u0026#34;Hello World\u0026#34;); } @ControllerAdvice：加在 class 上，把它變成 bean，並可在內部使用 @ExceptionHandler（底層由 Spring AOP 實作）。\n@ControllerAdvice public class MyExceptionHandler { @ExceptionHandler(RuntimeException.class) public ResponseEntity\u0026lt;String\u0026gt; handle(RuntimeException exception) { return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) .body(\u0026#34;RuntimeException:\u0026#34; + exception.getMessage()); } } 攔截器（Interceptor） # 用途：決定要不要允許這次的 HTTP request 通過、進到 Controller 執行對應方法。\n@Configuration public class MyConfig implements WebMvcConfigurer { @Autowired private MyInterceptor myInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(myInterceptor).addPathPatterns(\u0026#34;/**\u0026#34;); } } @Component public class MyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println(\u0026#34;執行 MyInterceptor 的 preHandle 方法\u0026#34;); response.setStatus(401); return false; } } REST 風格 # 用 HTTP method 表示動作：\nHTTP method 對應的資料庫操作 說明 POST Create（新增） 新增一個資源 GET Read（查詢） 取得一個資源 PUT Update（修改） 更新一個已存在的資源 DELETE Delete（刪除） 刪除一個資源 用 URL 路徑描述資源之間的階層（/）關係：\nHTTP method + URL 說明 GET /users 取得所有 user GET /users/123 取得 user id 為 123 的 user GET /users/123/articles 取得該 user 寫的所有文章 GET /users/123/articles/456 取得該 user 寫的、article id 為 456 的文章 GET /users/123/videos 取得該 user 錄的所有影片 response body 返回 JSON 或 XML 格式。\n驗證請求參數：@Valid / @Validated / @NotNull… # 使用 @RequestBody：要在該參數上加 @Valid，class 裡的驗證註解才會生效。 使用 @RequestParam / @RequestHeader / @PathVariable：要在 controller 上加 @Validated，驗證註解才會生效。 註解 詳細資訊 @NotNull 不能為 null @NotBlank 不能為 null 且不能為空白字串（用於 String） @NotEmpty 不能為 null 且 size \u0026gt; 0（用於 List、Set、Map 等集合） @Min(value) 值必須 \u0026gt;= value（用於數字） @Max(value) 值必須 \u0026lt;= value（用於數字） RestTemplate # 用途：發起一個 REST 風格的 HTTP 請求（GET、POST、PUT、DELETE），並可將收到的 response body JSON 字串轉換成 Java Object。\nSpring JDBC # NamedParameterJdbcTemplate 依 SQL 語法分兩類：\n1. update 方法（INSERT / UPDATE / DELETE）\n@Autowired private NamedParameterJdbcTemplate namedParameterJdbcTemplate; @DeleteMapping(\u0026#34;/students/{studentId}\u0026#34;) public String delete(@PathVariable Integer studentId) { String sql = \u0026#34;DELETE FROM student WHERE id = :studentId\u0026#34;; Map\u0026lt;String, Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(\u0026#34;studentId\u0026#34;, studentId); namedParameterJdbcTemplate.update(sql, map); return \u0026#34;執行 Delete sql\u0026#34;; } 2. query 方法（SELECT）\n用 SELECT * 的缺點：花費額外網路流量、無法提升查詢速度。 query 永遠回傳一個 List：取數據前記得先判斷是否有資料。 RowMapper：把查出來的數據轉成 Java Object，用 resultSet.getXXX(column) 取值。 ResultSetExtractor：和 RowMapper 用途類似，可組合不同 row 之間的數據。 Controller-Service-Dao 三層式架構 # Controller：接收前端的 HTTP request，並驗證請求參數。 Service：負責業務邏輯。 Dao：負責和資料庫溝通。 注意：\nClass 命名以 Controller / Service / Dao 結尾，表示所屬層級。 三層的 class 都變成 bean，用 @Autowired 注入。 Controller 不能直接呼叫 Dao，只能 call Service，再由 Service 呼叫 Dao。 Dao 只執行 SQL、存取資料庫，不能加任何業務邏輯。 單元測試（Unit Testing） # 目的：自動化測試程式的正確性。\n特性：一次只測一個功能點（一個 method 或一個 API）；可自動化；各測試互相獨立；結果穩定、不受外部服務影響。\n注意：測試程式放在 test 資料夾內；測試 class 以原 class 名加 Test 結尾；package 結構與原 class 一致。\nJUnit 5 用法 # 在方法上加 @Test 即可生成一個單元測試（只能在 test 資料夾下使用）。 方法名稱可隨意取，用來表達這個 test case 要測的功能點——這是精華所在。 常用 Assert：\n用法 用途 assertNull(A) 斷言 A 為 null assertNotNull(A) 斷言 A 不為 null assertEquals(expected, actual) 斷言相等（用 equals() 判斷） assertTrue(A) 斷言 A 為 true assertFalse(A) 斷言 A 為 false assertThrows(exception, method) 斷言執行 method 時會噴出 exception 其他常用註解：\n@BeforeEach / @AfterEach：每次 @Test 前/後各執行一次。 @BeforeAll / @AfterAll：所有 @Test 前/後各執行一次（方法須為 public static void）。 @Disabled：忽略該 @Test。 @DisplayName：自定義顯示名稱。 用 JUnit 5 測試 Spring Boot 程式 # 在測試 class 上加 @SpringBootTest，運行時 Spring Boot 會啟動容器、創建所有 bean（所有 @Configuration 也都會執行，效果等同運行整個程式）。\n@Transactional：\n在 test 資料夾：可加在方法或 class 上，單元測試結束後 rollback 所有資料庫操作，恢復原狀。 在 main 資料夾：程式運行中途發生錯誤時，rollback 已執行的資料庫操作。 Controller 層測試：MockMvc # 模擬真實的 API 呼叫：在測試 class 加 @AutoConfigureMockMvc、注入 MockMvc。\n@Test public void getById() throws Exception { // 設定發起的 Request 與相關參數（URL、請求參數、header） RequestBuilder requestBuilder = MockMvcRequestBuilders.get(\u0026#34;/students/3\u0026#34;); // mockMvc.perform 發起 Http Request // .andDo / .andExpect / .andReturn 處理並驗證 response // jsonPath 參考：https://jsonpath.com/ mockMvc.perform(requestBuilder) .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath(\u0026#34;$.id\u0026#34;, equalTo(3))); } Mock 測試（Mockito） # 目的：避免為了測一個單元，而去建構整個 bean 的依賴。 作法：創造一個假的 bean，替換掉 Spring 容器中原有的 bean。 @MockBean：產生假的 bean，未定義的方法預設返回 null。 // 模擬方法的返回值 Mockito.when(...).thenReturn(...); Mockito.doReturn(...).when(...); // 模擬拋出 exception Mockito.when(...).thenThrow(...); // 方法不返回 void Mockito.doThrow(...).when(...); // 方法返回 void 也可記錄方法的使用次數與順序。 限制：不能 mock static 方法、private 方法、final class。 @SpyBean：只替換其中幾個方法，其餘仍是正常的 bean。 其他心得：\nRun test with coverage 可看覆蓋範圍，但不要為了提升覆蓋率而寫測試，要思考哪些場景沒考慮到，別被數字迷惑。 若測試中 @SpyBean 過多，表示功能切分得不夠好。 進一步可以走測試驅動開發（TDD）的流程。 IntelliJ 實用技巧（Mac） # 功能 快捷鍵 萬用鍵（顯示意圖動作） ⌥ + Enter 跳轉到定義 ⌘ + 滑鼠左鍵 全域搜尋 ⌘ + ⇧ + F 回到前一次游標位置 ⌘ + ⌥ + ← 註解／取消註解 ⌘ + / 移除多餘的 import ⌃ + ⌥ + O 調整排版（格式化） ⌘ + ⌥ + L 快速查詢（Search Everywhere） 連按兩下 ⇧ 列出近期開過的檔案 ⌘ + E 自動產生（Generate） ⌘ + N 選取多行（column selection） ⌥ + 拖曳滑鼠 其他：Endpoints 工具視窗可找到所有 url-mapping 方法；連點視窗兩下可放大；split to right 切割分頁；File → New → Module from Existing Sources → 選 pom.xml 可同時載入多個 Spring 專案。\nMaven 生命週期 # clean：刪除 target 資料夾。 compile：編譯程式。 test：運行單元測試。 package：打包成 .jar（放在 target/）。 install：把 .jar 放到 Local repository（~/.m2/repository）。 deploy：把 .jar 上傳到 remote repository。 ","date":"2022年12月24日","externalUrl":null,"permalink":"/zh-tw/posts/spring-boot-notes/","section":"技術文章","summary":"自學 Spring Boot 的整理筆記：IoC/DI、AOP、Spring MVC、RESTful API、參數驗證、Spring JDBC、三層式架構，到 JUnit 5 與 Mockito 單元測試。","title":"Spring Boot 零基礎入門學習筆記","type":"posts"},{"content":"","date":"2022年12月24日","externalUrl":null,"permalink":"/zh-tw/tags/%E5%AD%B8%E7%BF%92%E7%AD%86%E8%A8%98/","section":"Tags","summary":"","title":"學習筆記","type":"tags"},{"content":"","externalUrl":null,"permalink":"/zh-tw/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"我是一位在台北的軟體工程師。日常工作是後端微服務——Spring Boot / Spring Cloud 服務、分散式交易、快取、監控與 CI/CD;目前則投入身分驗證領域的應用機器學習與電腦 視覺:把人臉活體偵測與深偽(deepfake)偵測做成生產環境的服務。\n在轉入後端之前,我有數年的 SSD 韌體經驗(FTL、垃圾回收、平均抹寫、不預期斷電復原), 這段經歷讓我習慣從硬體層往上理解整個系統。\n技術 # 後端 — Java、Spring Boot、Spring Cloud、分散式交易(Seata)、 MySQL / PostgreSQL、Redis、訊息佇列 雲端與 DevOps — Kubernetes、OpenShift、Docker、GitLab CI/CD、Grafana 機器學習 / 電腦視覺 — Python、PyTorch、ONNX、FastAPI;CNN 與梯度提升模型、 完整的訓練 → 評估 → 部署 pipeline 其他 — C/C++(SSD 韌體)、以 Liquibase 做資料庫版控、TDD 演講 # JCConf 2024 — 〈使用 Liquibase 協助客戶資料遷移:從導入到整合進專案的心路歷程〉 著作 # Gaze tracking for smart consumer electronics — IEEE International Conference on Consumer Electronics (ICCE),2014。 High speed gaze tracking with visible light — International Conference on System Science and Engineering (ICSSE),2013。 學歷 # 碩士,電機工程學系 — 國立臺灣師範大學 學士,應用電子科技學系(電機工程學系) — 國立臺灣師範大學 證照 # TOEIC 835(2023) NVIDIA DLI — Fundamentals of Deep Learning for Computer Vision(2019) ","externalUrl":null,"permalink":"/zh-tw/about/","section":"SJ.Wu","summary":"我是一位在台北的軟體工程師。日常工作是後端微服務——Spring Boot / Spring Cloud 服務、分散式交易、快取、監控與 CI/CD;目前則投入身分驗證領域的應用機器學習與電腦 視覺:把人臉活體偵測與深偽(deepfake)偵測做成生產環境的服務。\n在轉入後端之前,我有數年的 SSD 韌體經驗(FTL、垃圾回收、平均抹寫、不預期斷電復原), 這段經歷讓我習慣從硬體層往上理解整個系統。\n技術 # 後端 — Java、Spring Boot、Spring Cloud、分散式交易(Seata)、 MySQL / PostgreSQL、Redis、訊息佇列 雲端與 DevOps — Kubernetes、OpenShift、Docker、GitLab CI/CD、Grafana 機器學習 / 電腦視覺 — Python、PyTorch、ONNX、FastAPI;CNN 與梯度提升模型、 完整的訓練 → 評估 → 部署 pipeline 其他 — C/C++(SSD 韌體)、以 Liquibase 做資料庫版控、TDD 演講 # JCConf 2024 — 〈使用 Liquibase 協助客戶資料遷移:從導入到整合進專案的心路歷程〉 著作 # Gaze tracking for smart consumer electronics — IEEE International Conference on Consumer Electronics (ICCE),2014。 High speed gaze tracking with visible light — International Conference on System Science and Engineering (ICSSE),2013。 學歷 # 碩士,電機工程學系 — 國立臺灣師範大學 學士,應用電子科技學系(電機工程學系) — 國立臺灣師範大學 證照 # TOEIC 835(2023) NVIDIA DLI — Fundamentals of Deep Learning for Computer Vision(2019) ","title":"關於","type":"page"}]