作者: tm731531

  • Jujutsu vs Git:從底層原理到 AI Agent Team 時代的版本控制選擇

    重點摘要

    • Git 的本質是一個 content-addressed key-value 資料庫,所有的 branch、rebase、reflog 都是建構在這個底層上的抽象
    • Jujutsu(jj)不是 Git 的替代品,而是把 Git 的 content-addressed 哲學「再往上推一層」 — 從檔案快照擴展到操作快照
    • Git 的核心限制不是指令不爽,而是它要求人類的同步注意力 — merge 衝突會 block 整個世界等你處理
    • jj 允許異步注意力 — 衝突變成 commit 的一種狀態,rebase 永遠不會停,你可以明天再一次處理完
    • Agent Team 有三種任務類型,jj 的價值差很多:Review(只讀)完全不需要 jj;Debug(試驗假設)是 jj 最被低估的甜蜜點;Dev(平行開發)在合併階段需要 jj
    • 最優的使用模式是 git 為主、jj 只在 Agent Team 合併階段出場 — 由 orchestrator agent 承擔 jj 學習成本,人類使用者和 subagent 全程保持 git 心智
    • 決策準則:默認 Git,三個訊號任一觸發時考慮 jj — 平行 Agent 撞車、review round 批次修正、Agent 實驗成本太高

    最近 GitHub 上 jj-vcs/jj(Jujutsu)越來越熱,社群討論開始出現「Git 要被取代了嗎」的聲音。我花了幾個小時認真研究這個工具,過程中意外地被迫重新理解 Git 的底層設計 — 因為只有懂 Git 真正是什麼,才看得懂 jj 想解決什麼。這篇文章就是這趟理解的完整紀錄,從 Git 底層原理開始,一路推到 AI Agent Team 時代的務實使用模式。

    結論先說:Jujutsu 和 Git 不是互斥關係。jj 的儲存後端其實就是 Git,你可以隨時切回純 Git 指令。真正該問的不是「要不要換」,而是「你的工作流是否已經撞到 Git 的設計假設」。而這個答案,必須先從 Git 的底層長什麼樣講起。

    先搞懂一件事:Git 其實是一個資料庫

    很多人用 Git 很多年,但沒意識到一件事:Git 本身就是一個資料庫,而且是一個設計極其精巧的資料庫。整個 .git/ 目錄就是一個 key-value store,只是它用檔案系統當儲存引擎而已。

    你現在去任何 git repo 跑 ls .git/objects/,會看到一堆兩位數的資料夾:00/ 01/ ... ff/,每個資料夾裡面有一堆 hash 開頭的檔案。這就是 Git 的資料庫。它叫 Object Database,key 是 SHA-1 hash,value 是壓縮過的內容。所有其他概念 — commit、branch、tag、stash、reflog — 全部都是建構在這個 key-value store 之上的抽象。

    四種物件,解構整個 Git

    • Blob:儲存單一檔案的內容,不包含檔名。SHA1(content) = hash,內容相同 hash 必然相同。同樣內容只存一份(自動去重)。
    • Tree:儲存一個資料夾的結構,內容是一張表 — 列出這個資料夾有哪些檔案(指向 blob)和哪些子資料夾(指向另一個 tree)。Tree 可以遞迴指向其他 tree。
    • Commit:儲存一個時間點的快照,內容只有幾行 — 指向根目錄的 tree hash、前一個 commit 的 hash、作者時間、commit message。Commit 本身不含任何檔案內容,它只是一個指標鏈的進入點。
    • Tag:annotated tag 的容器,對理解本質不重要,跳過。

    一個 commit 怎麼「記錄整個資料夾」

    你 commit 時,Git 為每個檔案內容計算 hash 建 blob,為每個資料夾建 tree(一張表,列出那個資料夾裡所有 blob 和 sub-tree 的 hash + 檔名),最後建 commit,只包含 root tree hash + parent hash + 作者 + 訊息。結果是一個指標鏈:commit → tree → sub-tree → blob每個 commit 都完整地、不可變地描述了那一刻整個資料夾的狀態,不是 diff,是完整快照。

    但每次存完整快照不會爆嗎?

    這就是 Git 設計最美的地方。答案是:不會爆,因為 content-addressed 帶來自動去重。你 commit 了 1000 次,但 99% 的檔案沒動,那 99% 的檔案永遠只有一個 blob,因為內容一樣 hash 一樣,儲存只有一份。改一個檔案的成本 = 1 個新 blob + 2-3 個新 tree + 1 個新 commit,其他全部重用。實際效果:Linux kernel 有 100 萬+ commits,整個 .git/ 也就 5 GB 左右。

    Git 從來不儲存 diffgit diff 是每次問的時候走訪兩個 commit 的 tree DAG、現場跑 Myers 演算法算出來的。這個設計的好處是:讀任何一個歷史版本都是 O(1),不用還原一串 diff 才能看到那個版本的樣子。這一切都是「content-addressed immutable store + pointer DAG」這一個設計決策的延伸。Linus 2005 年花兩週寫出 Git 核心,就是這個設計。後來所有東西都是在這個底層上加糖。

    Jujutsu 的核心創新:把 Git 的哲學再推一層

    現在你懂了 Git 的底層,就能看懂 jj 在做什麼。jj 沒有推翻 Git 的設計,它做的事情是:把 Git 的 content-addressed DAG 從「檔案狀態」擴展到「操作狀態」 — 同樣的哲學,應用在不同的層級。

    雙層身份:commit hash + change-id

    Git 只有一個 ID — commit hash,由內容決定。這導致 rebase 後 hash 全變,你找不到「原本那個 commit」。jj 對每個 commit 同時維護兩個 ID:commit hash(跟 Git 一樣)+ change-id(隨機產生的 128-bit ID,只在 commit 誕生時生成一次,rebase/amend/split/squash 都不變)。這個設計的意義是:身份認同不綁在 hash 上,綁在 change-id 上。你可以隨便改歷史,change-id 永遠追蹤得到「同一個概念上的改動」。這就是為什麼 jj 可以把「歷史重寫」當日常操作,而 Git 必須當儀式感操作。

    Op Log:第二層 content-addressed DAG

    jj 的另一個核心是 operation log。每個 jj 操作都產生一個 operation 物件,包含 operation id(hash)、parent operation、執行的指令、操作前後 repo 整體狀態的快照。這些 operation 本身也組成一個 DAG,content-addressed 儲存。注意這裡的模式跟 Git 完全一樣:不可變物件 + 指標 DAG + 內容定址。jj 只是把 Git 用來管檔案狀態的那套機制,再蓋一層管操作狀態。

    結果是 jj undo 變成一步 pointer 移動 — 直接把 repo view 指回前一個 operation 的「操作前快照」就好,不用反向計算任何東西。而且 undo 本身也是一個 operation,可以被 undo,可以無限回退。

    指令層面的六個差異

    面向 Git Jujutsu (jj)
    Staging Area working dir → index → commit(三層) working dir 就是 commit(兩層,無 git add)
    Branch 概念 一等公民,必須 checkout/rebase/merge 可選的 bookmark,每個 commit 有穩定 change-id
    Undo 機制 reflog(補救機制,90 天 TTL) jj undo(第一等操作,可無限回退)
    Conflict 處理 停止世界,當場解完才能繼續 衝突直接存進 commit,可延後解
    Stash 必要指令(stash push/pop/apply) 不存在,jj new 直接開新 commit
    歷史重寫 rebase -i 是儀式感操作 jj split / jj squash 是日常

    最重要的差異:同步注意力 vs 異步注意力

    到這裡為止,都還是「指令差異」和「設計哲學」的層面。但 jj 真正的價值,不在指令爽不爽,而在它如何消耗人類的注意力。這個差異是質變,不是量變。

    Git 是同步注意力模型 — merge/rebase 遇到衝突會停止世界,直到人類出現、看懂、解決、繼續。它要求你在「不可預期的時刻」立刻在場。這個模型在單人或 2-3 人團隊下完全沒問題,因為衝突頻率低、你本來就在鍵盤前。

    jj 是異步注意力模型 — rebase 遇到衝突不停下來,衝突被存進 commit 變成一種有效狀態,rebase 繼續往下跑。你可以明天處理、或一次批次處理所有衝突、或開另一個 Agent 專門解衝突。jj 允許人類「異步在場」 — 系統自己跑,有問題幫你記下來,你有空再來看。

    Git 的設計誕生於 2005 年,當時的假設是「careful human curator」 — 每個 commit 都是人類慎重思考後的決定,每個 merge 都有人類在場。但 Claude Code、Cursor、Copilot Agent 這類 AI 工具改變了「誰在產生 commit」這件事。Agent 產出的是高頻、探索性、非線性、大量並行的中間態。你不可能一邊看 3 個 Agent 平行工作,一邊在它們合併時即時解衝突,一邊決定每個中間態要不要保留。人類同步注意力的頻寬根本跟不上 Agent 的產出速度。

    jj 在這裡的價值不是「比 Git 更爽」,而是它讓 Git 做不到的事變可能 — 讓 Agent Team 真的平行跑到底,明天早上來一次處理所有衝突。這不是量變(爽度提升),是質變(能做的事情不同)。

    Workspace 隔離:平行的真正解法

    要澄清一個常見誤解:jj 本身不能解決「兩個 Agent 同時寫同一個檔案」這種檔案系統 race condition。檔案系統的併發是 OS 管的,任何 VCS 都介入不了。平行 Agent 的正確架構是每個 Agent 給一間房 — Git 用 git worktree add,jj 用 jj workspace add。每個 workspace 是獨立的 working directory,三個 Agent 在不同資料夾各寫各的,完全不會互相污染。

    Git 和 jj 在 workspace 隔離這一步的行為相同。差異出現在下一步 — 把三個 Agent 的輸出合併回 main 時。Git 會序列化 blocking(解一個、繼續、解下一個、繼續),你的注意力被三次阻塞切碎;jj 則是三個動作一氣呵成,最後一次性 batch 處理所有衝突。Workspace 隔離是必要條件,jj 的優勢出現在合併階段的併發模型

    Agent Team 的三種任務類型:jj 的價值差很多

    這是這篇文章最重要的一個區分,也是大部分「jj 推坑文」忽略的。你叫 Agent Team 做不同類型的事,jj 的價值完全不同 — 甚至有些情境 jj 根本沒有用。決定性的問題是:這些 agent 需要寫檔案才能完成任務嗎?

    類型 1:Review(只讀) — 完全不需要 jj

    場景:叫 Agent Team 做 code review、security audit、dead code 掃描、架構審閱。這些 agent 只讀檔案,產出報告,不寫任何東西

    這種場景 jj 沒有價值:沒有 commit、沒有 merge、沒有 undo 需求。三個 agent 同時讀同一個檔案完全沒問題,OS 的 file handle 對讀不是互斥資源。純 git 就好,甚至不需要 workspace 隔離。

    常見誤區:很多人把「開 Agent Team」跟「需要 jj」畫等號,導致在純 review 場景也導入 jj 的學習成本。這是沒必要的。

    類型 2:Debug(試驗假設) — jj 最被低估的甜蜜點

    場景:叫 Agent Team debug 複雜 bug。Debug 的本質是「形成假設 → 驗證 → 推翻 → 再假設」的循環,而驗證通常需要動手改 code — 加 log、改條件、插 assert、暫時跳過某段邏輯。

    Git 下的 debug 痛點:加了 10 個 print() 驗證完要全部刪掉,漏刪一個就污染 production;改了某個函式測試假設,後來發現方向錯,手動改回去容易漏;想試兩個假設 A 和 B 哪個對,兩個互相覆蓋,不能同時存在。

    這是 jj 最強的場景,**比一般開發還強**。原因很微妙:development 的產出是「最終正確的 code」,中間過程 git 可以容忍(最終 squash 成 clean commit 就好)。但 debug 的產出是「對問題的理解」,這個理解存在於中間過程本身,不存在於最終狀態。

    Git 丟掉中間過程 = 丟掉 debug 的精華。jj 保留所有中間過程 — 每個假設是一個 commit、jj undo 可以乾淨撤銷、多個假設可以平行存在於 op log,等於保留 debug 的完整價值。debug agent 可以試 5 個假設、留下完整推理紀錄、報告說「我試了 A/B/C/D,D 可行因為…」,你事後可以 jj op restore 跳回任何一個假設狀態看 agent 那時候看到什麼。

    結論:如果你常叫 AI debug 複雜問題,jj 的價值比你想的大很多。這是 op log 從「檔案歷史」升級為「推理紀錄」的唯一場景。

    類型 3:Dev(平行開發) — jj 在合併階段是必要品

    場景:叫 Agent Team 平行開發多個模組。多個 agent 會產出多個改動分支,最後要合併回 main。這是大家熟悉的場景 — 前面講的「同步 vs 異步注意力」、「workspace 隔離」、「conflict as commit」全部都在這個場景發揮。

    但注意:jj 在這個場景的價值集中在合併階段,不是 agent 工作期間。agent 自己工作時,Git 和 jj 沒差。差異出現在「把 N 個平行輸出 reconcile 回主線」這一步。沒有 jj 的非阻塞合併,你的 Agent Team 上限會被 git 的序列化 blocking 硬生生壓到小規模 — 即使記憶體還有餘裕。

    三種使用模式:該怎麼在日常工作裡擺 jj

    理解完上面的分析,最後的問題是:你該怎麼把 jj 放進你的日常工作?有三種模式,各自適合不同情境。

    模式 1:純 git(現狀)

    適合:單人或「1 RD + 1 Reviewer」工作流、穩定專案、強烈依賴 IDE Git 整合、團隊只認 git。這個模式沒什麼好討論,就是你現在的樣子,對大多數人完全夠用。

    模式 2:jj 為主,git 只是後端

    安裝 jj colocated mode,日常指令都打 jj,幾乎不打 git。這個模式的問題是:邊界仍然是 git 世界。IDE Git panel 顯示的是 jj 上次匯出的狀態,你在本地 jj rebase 一下,push 後 GitHub 還是看到 commit hash 全變,change-id 根本沒傳出去。你本地很爽,對外還是 git 的世界。

    這個模式只適合「單人、沒有協作壓力、不在意 IDE 整合退化」的使用者。對多數人代價太大。

    模式 3:git 為主,jj 只在 Agent Team 合併階段出場

    這是我研究完之後推薦的模式,也是最被低估的一個。玩法是:

    • 你日常全程用 git — 習慣、IDE、CI、PR 流程完全不變
    • Subagent 也用 git — 他們在各自 workspace 裡用 git commit,根本不知道 jj 存在
    • Orchestrator agent(Claude Code)在合併階段用 jj — 只需要學 5 個指令:workspace addgit importrebaselogworkspace forget
    • 一次性設定:jj git init --colocate,之後你完全忘記它存在

    這個模式最精妙的地方:學習成本完全落在 orchestrator agent 身上,不是你身上。你的認知負擔幾乎為零,只需要記住一個 trigger:「要開 Agent Team 時,叫 Claude」。其他時間 jj 完全不存在。

    失敗零成本:任何一個環節出問題,你都可以回退到純 git。subagent 的 commit 是正常 git commit,無論是否用 jj 都能 git cherry-pick;合併失敗就 git merge 手動解;jj 整個壞掉就刪掉 .jj/,git repo 毫髮無傷。沒有 lock-in,沒有 point of no return

    Review Round 批次修正:寫之前切 vs 寫之後切

    另一個 jj 爆發的場景是 review round 工作流。很多團隊的 QA/review 會產出幾十個 issue,你拿到一份清單要一次修完。典型的 commit message 長這樣:fix(fee): R5-12/13/03/04/10/16/17 remaining module items — 塞了 7 個獨立的 issue 修正在一個 commit。

    為什麼塞一起?因為 Git 下的三個選擇都不理想:每個 issue 一個 commit 要 git add -p 切七次太累;全部一個 commit 未來想單獨 backport 做不到;開 7 個 branch 管理成本爆炸。結果大多數人選「全部一個 commit」,歷史變成粗顆粒。

    jj 的解法:打開 editor,一次改完 7 個 issue,不用管 commit 切分。改完後 jj split,互動畫面讓你把每個 hunk 分配到哪個 commit,七次後自動產生 7 個獨立 commit。Git 的 add -p 是「寫之前」切,jj 的 split 是「寫之後」切。這讓你把「commit 切分」這個決策延後到資訊最充足的時候(全部改完後),而不是在你還在思考邏輯的時候被迫處理。

    決策準則:你該用哪個模式?

    純 git 就夠的訊號

    • 單人或「1 RD + 1 Reviewer」工作流 — 沒有平行檔案衝突
    • Agent Team 只做 review / audit(agent 不寫檔案) — jj 沒有價值
    • 穩定專案、強依賴 IDE Git 整合
    • 老 Git 肌肉記憶還在運作,且團隊只認 git

    考慮模式 3(git + jj for Agent Team merge)的訊號

    1. Agent Team 在共享檔案上撞車 — 你為了避免 merge 衝突開始序列化 Agent,生產力被 VCS 綁住
    2. Review round 產生批次修正 — 你寫過大量 fix: XXX-01/02/03/04/05 這種塞一堆 issue 的 commit,事後想拆卻沒工具
    3. AI 常常需要做 debug 試驗 — 你希望 AI 大膽試很多假設,但現在因為回退成本太高,限制了 agent 的自由度

    三個訊號都沒中?純 git 就夠了,別為了新鮮感切工具。

    我的具體建議:安全試用路徑

    1. 選一個個人實驗性專案,執行 jj git init --colocate(一次性,30 秒)
    2. 繼續用 git 做 99% 的事 — 你的肌肉記憶完全不變
    3. 下次真的要開 Agent Team 時,讓 orchestrator agent 用 jj 的 workspace + 非阻塞 rebase 處理合併階段
    4. 如果是 debug 場景,給 agent 「大膽試驗」的權限,利用 op log 保留推理紀錄
    5. 一個月後評估:有沒有真的遇到 jj 幫上忙的場景?沒有就卸載,git repo 完全不受影響

    跨專案的架構原則

    這次研究讓我提煉出一個跨專案適用的原則,寫進我的領域腦系統:

    VCS 的選型要跟「人類注意力的可用模式」匹配,不是跟「指令爽不爽」匹配。Git 假設人類可以同步在場,jj 允許人類異步在場。AI Agent 工作流打破了 Git 的第一個假設,但是否影響到你,取決於你的團隊形狀和工作型態,不取決於你對 AI 的熱情。

    更深一層的 insight:Git 的偉大不在於它的指令設計,而在於 content-addressed DAG 這個設計模式。這個模式你會在 IPFS、Nix、Docker layers、區塊鏈到處看到 — 懂 Git 底層等於懂了這一整類系統的通用結構。jj 的聰明,是看到這個模式可以往上再推一層,應用到操作本身,而不只是檔案狀態。

    AI 時代最大的認知陷阱是「新工具一定比舊工具好」,但真實世界是新舊工具的取捨點會隨場景變化。把場景描述清楚,比把工具吹捧清楚重要得多。下次你看到任何新的版本控制、儲存系統、分散式工具時,問自己這三個問題就好:它的不可變單位是什麼?怎麼定址?DAG 結構長怎樣?答案對上 content-addressed DAG 模式,你就已經懂 80% 了

  • Hacker News 每日精選 – 2026-04-14

    “`html




    Hacker News 每日科技洞察

    🚀 Hacker News 每日科技洞察:AI 能力邊界與供應鏈安全的嚴峻挑戰

    今日科技圈呈現出一種極端的對比:一方面是 AI 模型在複雜邏輯(如駕駛飛機)上的嘗試,以及開發工具不斷進化以提升效率;另一方面,供應鏈攻擊與網路惡意行為(如 WordPress 外掛後門與按鈕劫持)正威脅著數位生態的安全。理解這些技術進步與安全隱憂,是每一位開發者與技術決策者保持競爭力的關鍵。

    🤖 AI / 機器學習

    ✈️ Claude 能開飛機嗎?

    這篇文章探討了大型語言模型(LLM)在極其複雜且具備物理邏輯要求的任務中的表現。透過模擬飛行情境,測試 Claude 是否能理解並執行高度精確的操作指令。這不僅是技術測試,更是對 AI 邁向具身智能(Embodied AI)的重要思考。

    閱讀原文

    🛠️ 開發工具

    🚀 GitHub 推出 Stacked PRs 功能

    GitHub 正式引入了「堆疊式拉取請求」(Stacked PRs)的概念,這對於處理具有依賴關係的連續開發任務非常有幫助。透過這種方式,開發者可以更流暢地管理一系列相關的變更,而不需要等待前一個 PR 合併後才能開始下一個。這將極大提升大規模團隊的開發效率。

    閱讀原文

    ⚛️ TanStack Start 現在支援 React Server Components (RSC)

    TanStack 生態系再次進化!TanStack Start 現在已正式支援 React Server Components,這意味著開發者可以更深入地利用伺服器端渲染的優勢,減少客戶端 JavaScript 的負擔,並提升應用程式的載入速度與 SEO 表現。

    閱讀原文

    🔍 當形式化驗證(Lean)證明程式正確,但我卻找到了 Bug

    這是一個令人深思的案例,作者分享了即便使用了強大的 Lean 形式化證明工具,最終仍發現程式中存在錯誤的經驗。這提醒了我們,證明工具的有效性取決於模型的正確性,任何對系統假設的疏忽都可能導致災難性的後果。

    閱讀原文

    🔓 開源專案

    📊 分散式 DuckDB 實例 (Openduck)

    DuckDB 以其高效的分析能力著稱,而這個開源專案嘗試將其擴展到分散式架構中。這對於需要處理大規模數據集、但又希望保有 DuckDB 易用性的開發者來說,是一個非常令人興奮的研究方向。

    閱讀原文

    🎮 WiiFin:為 Nintendo Wii 打造的 Jellyfin 客戶端

    這是一個極具情懷的開源專案,讓老舊的 Nintendo Wii 也能透過 WiiFin 連結到現代的 Jellyfin 媒體伺服器。它成功地讓過時的硬體在當代數位娛樂生態中重新煥發生機。

    閱讀原文

    🛡️ 其他 (安全、創意與觀點)

    ⚠️ 惡意攻擊者買下 30 個 WordPress 外掛並植入後門

    這是一起極其嚴重的供應鏈攻擊案例。攻擊者透過購買現有的 WordPress 外掛,隨後在所有外掛中植入後門程式碼。這再次證明了「軟體供應鏈」是目前數位安全中最脆弱的一環,依賴第三方套件時必須更加審慎。

    閱讀原文

    🖼️ DaVinci Resolve 發佈照片編輯功能

    Blackmagic Design 擴展了其強大的色彩科學能力,將 DaVinci Resolve 的範疇從影片編輯延伸到了照片編輯。對於專業攝影師與調色師來說,這提供了一個能無縫銜接工作流的新工具。

    閱讀原文

    🚫 Google 針對「回退按鈕劫持」實施新的垃圾內容政策

    Google 宣布將打擊一種惡意的 UX 劫持行為,即當使用者點擊瀏覽器的「回退」按鈕時,網站會攔截並將使用者導向錯誤的頁面。這項政策旨在維護搜尋引擎的品質與使用者體驗的完整性。

    閱讀原文

    🧠 有時權力人士也會做蠢事

    一篇關於權力與人性誤判的深刻評論。文章探討了當個人掌握巨大權力時,為何往往會做出缺乏邏輯或損害大局的愚蠢決策,這對於理解組織管理與社會運作具有啟發意義。

    閱讀原文

    💡 今日觀點

    觀察今日的熱門話題,我們可以發現一個核心矛盾:技術工具正變得越來越「聰明」且「自動化」,但隨之而來的風險也變得更加「隱蔽」且「系統化」。

    「當我們依賴 AI 進行決策、依賴第三方套件構建架構、依賴形式化證明確保安全時,我們實際上是在建立一個更複雜的信任鏈。一旦鏈條中的任何一環(無論是程式碼、供應鏈還是人的判斷)出現斷裂,造成的影響將遠超以往。」

    🛠️ 給讀者的行動建議:

    • 強化供應鏈審核: 不要盲目信任第三方套件,定期進行依賴項掃描與安全性檢查。
    • 擁抱現代開發流: 關注如 Stacked PRs 與 RSC 等新技術,它們能有效降低開發摩擦力。
    • 保持懷疑精神: 無論是 AI 的輸出還是數學工具的證明,都要保留人工審核(Human-in-the-loop)的機制。



    “`

  • Vibe Coding 協助物聯 AI 開發:從 RTSP 撞牆到 Telegram Bot 上線的完整實戰

    這是一篇關於 Vibe Coding(憑感覺邊聊邊寫)在物聯 AI 開發上的實戰紀錄。主題聽起來很酷,但實情是:我只是想解決一個很平凡的問題 —— 怎麼把家裡長照白板上的磁鐵打點,自動變成醫生回診時能看的資料。

    這篇會誠實記錄這一晚跟 AI 協作的過程、做對的決策、撞到的牆、以及學到的方法論。不是成功故事,中間我還沒跑出一份真正可用的資料。但我覺得過程本身比結果更值得寫下來。

    重點摘要

    • Vibe Coding 不是憑空想像,而是即時驗證 —— 每個假設都在對話中被一條 curl / 一行 ffmpeg 打臉或確認
    • Gemma 4 跟 Gemini 3 Flash Preview 在同一張模糊中文手寫白板上差距巨大:前者一直幻覺亂編項目,後者誠實回 null
    • Schema-constrained Prompt + 信心度分級是物聯 AI 應用的關鍵 —— 寧缺勿錯,比準度更重要
    • 最後撞到的不是模型牆,是光學物理牆 —— RTSP 1080p 遠距離下磁鐵只有 3~5 像素,任何 VLM 都無解
    • 與 AI 協作最大的價值是迭代速度:從想法 → curl 驗證 → 決策 → 下一步,一晚走完通常要一週的探索

    什麼是 Vibe Coding?為什麼它適合物聯 AI 開發

    Vibe Coding 是一種開發風格,核心精神是:不先寫規格、不先畫架構,先跟 AI 對話,在對話中讓問題結構自己浮現。聽起來很鬆散,但在物聯 AI 開發(IoT + AI 結合)的情境下,這種作法反而比傳統瀑布式規劃更有效。

    原因很簡單:物聯 AI 專案有太多無法事先確定的變數 —— 攝影機實際畫質、現場光線、模型對中文手寫的容忍度、API 認證流程的細節、現場使用者的真實習慣。這些東西你寫再多規格也寫不清,只能邊試邊改。

    傳統作法是先買硬體、接好線、寫一堆程式、跑起來才發現 OCR 讀不到。Vibe Coding 作法是:在對話中模擬整條管線,每一步都先驗證再決定下一步。成本極低,迭代極快。

    實戰情境:家母的長照交班白板

    先講背景。家裡照顧阿嬤的居服員會在客廳牆上用一塊大白板做交班記錄:夜間活動、睡前狀況、昨夜睡眠、早/午/晚餐、活動、外出、陪伴狀況、排泄狀況、洗澡,共 12 個項目。每一項下面有幾個選項方格,居服員把一顆彩色磁鐵貼在正確的選項上。

    用意是交班:早班居服員進門就知道夜間情況,中班看早上發生什麼事,晚班接手時看到整天脈絡。白板是「人看的 UI」,極度務實。

    我這邊的需求是:這些資料要每月匯整,讓神經內科醫師看到阿嬤這個月的生活趨勢 —— 是不是睡眠惡化、食慾變差、活動量下降。這關係到藥物調整。原本我每天晚上八點親自去拍照記錄到 iDempiere,但我很清楚一件事:

    靠人不穩定。我不能保證自己每天都拍照,請妹妹拍她都不拍。任何依賴人的環節最終都會斷掉。

    所以目標很明確:零人工依賴的自動化管線。RTSP 攝影機 → 抽幀 → VLM 識別白板狀態 → 寫進 iDempiere。今晚就是要從零對話出這套設計。

    第一步:用 ffmpeg 抓一張給 AI 看

    對話一開始,AI 想直接讀 RTSP 流,但我的 Ubuntu 上沒裝 ffmpeg,只有 VLC,而且 VLC 一直被 satip 模組攔截。試了幾個旗標都失敗。最後決定直接 apt-get install ffmpeg,然後:

    ffmpeg -y -rtsp_transport tcp \
      -i "rtsp://:@192.168.0.53:554/stream1" \
      -frames:v 1 -update 1 -q:v 2 /tmp/tapo/whiteboard_hd.jpg

    一張 1920×1080 的 frame 落地。第一個教訓:TAPO 有 stream1(主碼流,1080p)跟 stream2(子碼流,較低畫質),白板 OCR 一定要用 stream1,子碼流字跡會糊到任何模型都讀不出來。

    把第一張 frame 丟給 AI 看,AI 發現:

    • 畫面是廣角客廳全景,白板只佔左後方約 25%
    • 白板有角度(側視),字有透視變形
    • 阿嬤正躺在沙發上睡 —— 這畫面本身就能當「夜間睡眠」的訊號來源
    • 時間戳在左上角,直接可當記錄時間

    這是 Vibe Coding 的第一個價值:「看到了才知道要問什麼」。事先想像不到的細節(阿嬤就在沙發上、一顆鏡頭可同時驅動多個欄位),第一張實拍 frame 全部揭露。

    Gemma 4 vs Gemini 3 Flash Preview:一張圖的殘酷對比

    我手上的 Gemini API Key 支援 Google AI Studio,我問 AI 能不能用我已經在跑的 gemma-4-26b-a4b-it(我本來拿它做 HN 文章摘要)。AI 坦白說:不確定 Gemma 4 這個 MoE 版本有沒有 vision 能力,最好的方式是直接寫 script 試。

    寫完測下去,兩個結論:

    1. Gemma 4 確實吃圖 —— 這是好消息
    2. 但它嚴重幻覺 —— 它把日期讀成「4月10日」(實際 4/14)、項目名自創「睡眠品質/活動量/食事/盥洗」(白板上根本沒這些)、備註整段編造「各位家屬提醒…」。所有項目都回「綠」,明顯是在套公版答案

    切換到 gemini-3-flash-preview(Gemini 3 系列的 flash preview 版本),同一張圖,結果判若兩人:

    項目Gemma 4Gemini 3 Flash Preview
    項目名自創 5 個通用項目讀出 13 個具體項目
    備註段落純幻覺讀出「為了照護過程順暢請大家配合…飲食攝入小於 500ml 請 Line 通知…」
    品質差距低解析度下直接編造雖有 OCR 誤差但是「在讀真的東西」

    Gemini 3 Flash Preview 讀出的備註段落跟我白板上實際寫的紅字幾乎一致 —— 它不是在猜,是真的看到了。雖然還有誤差(把「外出狀況」讀成「外服藥記」、把「排泄」讀成「排便」),但那些是視覺相似字誤差,不是幻覺。

    這是第二個教訓:「同一張圖丟不同模型」是物聯 AI 應用最該做的前測。同價位的模型在模糊中文手寫這種邊緣任務上差距可以是幾倍級的。不要以為「我手上這把就夠用」。

    iDempiere REST API:從文件陷阱到 10 分鐘跑通

    OCR 能讀不代表能寫。寫入端是我自己建的 iDempiere,一張叫 Z_momSystem 的表。AI 不知道 schema,我也懶得翻 UI,直接讓它打 REST API 取。

    這裡踩第一個坑:iDempiere REST API 的認證流程文件跟實作不一致。我的 brain 筆記寫兩步驟(POST 拿暫時 token → PUT 帶 clientId 換 session token),但 AI 試了之後發現:直接在 POST 時一次帶齊所有 ID 也可以成功:

    POST /api/v1/auth/tokens
    {
      "userName": "",
      "password": "",
      "parameters": {
        "clientId": 1000000,
        "roleId": 1000000,
        "organizationId": 0,
        "warehouseId": 0,
        "language": "zh_TW"
      }
    }
    → 直接回 session token

    四個參數缺一不可,少 roleId 就回 Missing roleId parameter。這種小細節沒人會寫在文件裡,只能撞過才知道。AI 在對話中撞完就直接更新我的 brain file:

    [source: 長照 OCR 專案 2026/4/14] 驗證過 POST 一次帶齊 clientId/roleId/organizationId/warehouseId 可直接拿到可用 token,不用走 PUT 兩步。

    這是 Vibe Coding 第三個價值:踩到的坑立刻回饋到未來。不寫記錄的話,下個專案又會踩一次同樣的坑。

    Schema-Constrained Prompt:讓模型不能亂編

    認證跑通後,AI 直接查 AD_Column + AD_Ref_List,把 Z_momSystem 的 40 個欄位跟 12 個清單型欄位的所有合法選項抓齊:

    欄位合法選項(id → 中文)
    NightActivity 夜間活動C=完成 / N=未完成 / S=抗拒
    BeforeSleepStatus 睡前狀況X=亢奮 / N=正常 / S=疲倦
    LastNightSleep 昨夜睡眠G=良好 / S=斷續 / N=差
    Breakfast/Lunch/DinnerN=正常 / 10000001=少 / 10000009=多 / 10000002=拒食
    ExcretionStatus 排泄狀況10000006=正常 / 10000007=便秘 / 10000008=稀

    拿到完整選項清單後,我跟 AI 共同寫出 Prompt v2,核心設計有四塊:

    1. 嚴格列出 12 個欄位名稱 + 每個欄位的合法 enum,告訴模型「只能從清單裡選,不要自由發揮」
    2. 明確定義磁鐵規則:磁鐵貼在選項上 = 選中;磁鐵在左側待命區 / 沒看到磁鐵 = 未填(null)
    3. 強制輸出信心度:high / medium / low,並規定「寧可標 low 也不要亂猜」
    4. 禁止事項明確化:不要輸出清單外的項目、不要用清單外的值、不要把空白當選中

    這個 Prompt 模式我叫它 Schema-Constrained Prompt:輸出 schema 先於輸入內容確定。模型的自由度被壓縮到只剩「讀」,不能「想」。對資料入庫型應用來說,這是我目前看過最能壓住幻覺的作法。

    信心度機制:為什麼寧缺勿錯比高準度更重要

    我跟 AI 討論一個核心問題:如果 OCR 讀錯一個欄位,後果是什麼?答案是 —— 醫生根據錯誤資料調藥

    這條紅線決定了整個系統的預設行為:

    • High confidence → 直接寫入 iDempiere
    • Medium confidence → 寫入但標記為待確認,月底回診前我掃一次
    • Low confidence → 不寫,保留原圖,等我手動補

    這樣最差情況是「資料不全」而不是「資料錯誤」。對臨床情境來說,null 永遠比錯值安全

    最後的物理牆:磁鐵只有 3 像素

    所有準備工作做完,Prompt v2 在手、schema 對齊、認證跑通。我說「跑」,ffmpeg 抓一張新的 frame、裁切、送 Gemini 3 Flash Preview。結果回來:

    {
      "date": "4月10日",
      "patient_name": "",
      "items": {
        "NightActivity": {"value": null, "confidence": "high"},
        "BeforeSleepStatus": {"value": null, "confidence": "high"},
        ... 12 個項目全部 null ...
      },
      "overall_notes": "白板所有項目的磁鐵均位於最左側待命區,表示尚未填寫狀態。"
    }

    問題是 —— 我明明告訴 AI,最上面兩項的磁鐵是貼在選項上的。模型卻全讀成 null。這代表什麼?

    我們一起算了一下畫素:在當前 TAPO 攝影機位置,1080p 畫面中白板只佔約 25%,每個選項方格大概 40×20 像素,磁鐵大約 3~5 像素寬。

    任何 VLM 都讀不出 3~5 像素的物體位置。不是模型問題,是物理極限。

    這是整晚最重要的一個教訓。我原本以為換到 Gemini 3 就能解決一切,但真正的瓶頸不在模型,在光學。你沒辦法用演算法放大不存在的資訊。

    有意思的是,這個結論不是我預設的,是對話中算出來的。如果我沒跟 AI 一起 debug、一起看 crop、一起數像素,我可能還在抱怨「模型不夠強」,然後浪費錢去升級 model tier。實際上答案是「把鏡頭移近到 1.2 公尺」或「把磁鐵換大到 3 公分以上」—— 兩個都是硬體側的改動。

    Vibe Coding 在物聯 AI 開發上的六個心得

    整晚下來,我想整理出一些可以帶到下個專案的方法論:

    1. 第一張真實 frame 永遠比規格重要

    物聯 AI 專案第一件事不是畫架構圖,是用 ffmpeg / curl 抓一張真實資料給 AI 看。看到之後你才會發現「原來畫面裡還有沙發」、「原來字有角度」、「原來磁鐵只有 3 像素」—— 這些事事先想不到。

    2. 同一任務丟多個模型前測

    不要假設「手上這把就夠」。Gemma 4 跟 Gemini 3 Flash Preview 在同一張圖上差距巨大,如果我沒前測直接開始寫 production code,下游全部要重做。前測成本只有一個 Python script。

    3. Schema 先於 Prompt

    Prompt 不應該從「我想知道什麼」開始寫,應該從「我資料庫裡有什麼欄位、合法值是什麼」開始寫。把 enum 硬塞進 prompt,模型就只能從那個清單選。幻覺空間瞬間縮到 0。

    4. 信心度是一等公民

    每個欄位都要輸出信心度,而且模型要被明確告知「寧缺勿錯」。信心度不是錦上添花,是讓系統可以在「自動化」跟「人工審核」之間無縫切換的基礎設施。

    5. 踩到的坑要立刻寫回知識庫

    iDempiere REST API 的認證細節、TAPO stream1/stream2 的差異、VLM 對 3 像素物體的物理極限 —— 這些東西踩過一次就要寫下來。否則半年後你或你的下一個 AI 助手會原地再踩一次。

    6. 永遠先問「這個瓶頸是模型還是物理」

    當 AI 應用做不出效果時,工程師的直覺是「換更大的模型」。但物聯 AI 應用有一半以上的瓶頸是光學 / 聲學 / 感測器物理,不是模型。在升級 model tier 之前,先算一下原始訊號的解析度夠不夠。

    一晚的進度清單:完成、撞牆、下一步

    分類項目狀態
    環境ffmpeg + venv + google-genai SDK
    模型選型gemini-3-flash-preview 確認為白板 OCR 唯一可用
    API 認證iDempiere REST auth flow + brain 更新
    SchemaZ_momSystem 40 欄位 + 12 清單欄位合法值
    Promptv2 schema-constrained + 信心度
    實拍 OCR12 項全 null(物理極限)
    下一步鏡頭移近至白板前 ~1.2m 或把磁鐵換大🔜

    尾聲:AI 協作不是減少思考,是加速思考

    這一晚最大的收穫不是程式,是幾個原本需要好幾天才能驗證的判斷,在對話中幾個小時就走完了:

    • 「Gemma 4 能不能做 vision OCR」—— 10 分鐘驗證完
    • 「iDempiere REST API 認證怎麼打」—— 15 分鐘跑通
    • 「Z_momSystem schema 長怎樣」—— 5 分鐘 curl 查完
    • 「Prompt 用什麼結構壓住幻覺」—— 30 分鐘寫完 + 測完
    • 「OCR 失敗的根本原因是什麼」—— 從「模型不夠強」修正到「物理像素不夠」

    如果自己慢慢摸,這些決策可能得花一個禮拜,還未必會發現最後那個「像素物理牆」的結論。

    但 Vibe Coding 也不是萬靈丹。它的前提是你自己要會即時判斷 AI 給的答案對不對。如果我不知道「1080p 遠距離下一顆磁鐵只有幾像素」該怎麼算,我不會發現光學才是真兇,可能還會繼續跟 AI 討論要換什麼模型。AI 負責快速生成假設跟驗證工具,但關掉討論的那個人還是我

    下一步很清楚:明天白天我會去把攝影機搬到白板正對面 1.2 公尺處,如果還不夠,就把磁鐵換成 3 公分的大顆粒。然後再跑一次 Prompt v2,看看準度能衝到 8/12 還是 12/12。

    等驗證完整條管線跑通,我會再寫第二篇:零人工依賴的長照自動化 —— 從 OCR 到 iDempiere 到每月回診的完整管線。這一篇先停在「撞到物理牆」這裡,因為這個結論本身就夠值得記下來了。

    後續實戰:從撞到物理牆到 Telegram Bot 上線

    前一篇的結尾停在「撞到物理牆」,隔天白天我接著實戰。這一段記錄從 PTZ 鏡頭控制、模型賽馬、到最後關鍵 pivot 的完整過程。

    後續重點摘要

    • ONVIF PTZ 跑通:TAPO C200 用 RTSP 同帳密就能從 ONVIF 控制 pan/tilt,存 preset 在「看媽媽」跟「看白板」間自動切換
    • 但物理牆還在:C200 沒有 ONVIF zoom,白板在畫面裡的絕對大小被鏡頭距離鎖死
    • 模型賽馬揭露真相:Gemma 4 / Gemini 2.5 Flash 會套預設答案假裝在看;只有 Gemini 3 Flash Preview 跟 2.5 Pro 真的「在看」
    • 關鍵 pivot:放棄 RTSP 自動化,改走「手機拍照 + Telegram Bot」,把 human 設計進 loop 當 verifier
    • 最後上線:Telegram Bot + Gemini 3 Flash Preview + iDempiere REST API + systemd service,免費版每日 20 次額度剛好夠 production 用

    第一回合:ONVIF PTZ 成功,但沒有 zoom

    既然 TAPO C200 是 PTZ 型號(有 pan/tilt 馬達),自然想法就是「平常看媽媽、每天 20:00 轉過去拍白板、拍完轉回來」。代價幾乎為零:不改位置、不買硬體、不影響監護。

    TAPO 的本地 API 很頭痛,新版韌體要另外設定「Camera Account」。試了 pytapo 套件一直認證失敗。最後發現 ONVIF 標準協定可以直接用 RTSP 同帳密,port 2020:

    from onvif import ONVIFCamera
    cam = ONVIFCamera("192.168.0.53", 2020, "", "")
    ptz = cam.create_ptz_service()
    media = cam.create_media_service()
    token = media.GetProfiles()[0].token
    
    # Save current position as preset
    req = ptz.create_type("SetPreset")
    req.ProfileToken = token
    req.PresetName = "mom"
    ptz.SetPreset(req)
    
    # Move to whiteboard preset
    req2 = ptz.create_type("GotoPreset")
    req2.ProfileToken = token
    req2.PresetToken = "2"   # whiteboard preset token
    ptz.GotoPreset(req2)

    30 分鐘內建好兩個 preset:mom (pan=0.165, tilt=-0.714) 看沙發、whiteboard (pan=0.2, tilt=-0.3) 看白板。自動切換、抽幀、切回,全程 10 秒內。

    但查 PTZ 能力的時候發現一個殘酷事實:

    AbsoluteZoomPositionSpace: []

    C200 的數位 zoom 不對 ONVIF 開放。pan/tilt 能改變「看哪裡」,但不能改變「看多近」。白板在畫面裡的絕對大小還是被物理距離鎖死,磁鐵還是只有幾個像素。

    這是今天的第一個重要教訓:即使有 PTZ,沒有 zoom 的 PTZ 解決不了光學採樣率不足的問題。pan/tilt 的價值在於切換主題,不是放大細節。

    第二回合:四個模型的殘酷賽馬

    既然 PTZ 轉過去也只是切換主題、不會變清楚,我決定做一件更有意義的事:在同一張困難的 RTSP 照片上,把所有可能的模型跑一遍,看哪個真的「在看」。

    測試方法:請媽媽的居服員確認白板上 12 個項目的真實狀態(4 個 box 1、2 個 box 2、6 個 null),這是 ground truth。同一張 RTSP 照片丟給四個模型,計算準度。

    模型命中行為模式
    Gemini 2.5 Pro8/12 (67%)6/6 null 正確、off-by-one 錯誤,真的有在讀像素
    Gemini 3 Flash Preview6/12非常保守,看不清就全回 null(這在醫療情境反而安全)
    Gemini 2.5 Flash4/12預設 box 1,剛好對 4 個 box 1 項目,是幻覺而不是識別
    Gemma 4 (26B-A4B-IT)3~4/12兩次跑結果不一樣:第一次全 box 2、第二次全 box 1

    最有意義的發現:跑同一張圖兩次讓偽答案現形。Gemma 4 第一次答「10 項 box 2 + 1 項 box 1 + 1 項 null」,第二次答「12 項 box 1」。兩次完全不同,同一張靜態圖,同一個 prompt。這證明 Gemma 4 根本沒在看磁鐵,只是在套一個預設樣板。

    便宜模型「預設 box 1」這個偏誤特別危險,因為它剛好能騙過不注意的測試者:真實世界裡大部分項目本來就選 box 1(例如「完成/正常/穩定」通常是第一格),所以「全答 box 1」天然就有 30~50% 命中率。

    Gemini 2.5 Pro 的 8/12 表現是最高的,錯誤都是「off-by-one」(看到磁鐵、算錯格子),至少是在做真正的視覺識別。但 2.5 Pro 免費版不開放 vision,要付費才能用。

    Gemini 3 Flash Preview 的 6/12 看起來輸給 2.5 Pro,但它的錯誤類型完全不一樣:它的「錯」是把有磁鐵的項目讀成 null,不會產生錯誤的 value。這在醫療場景反而是最安全的行為 —— 寧缺勿錯永遠優於硬填。

    第三回合:投票沒用、upscale 沒用

    既然 Gemini 2.5 Pro 有 67% 準度,同一張圖跑 3 次取多數票應該能把 67% 拉高吧?

    結果投票後掉到 6/12。為什麼?因為 2.5 Pro 的錯誤不是隨機噪音,是系統性偏誤:某個項目 3 次都答同一個錯的答案。投票對系統性錯誤完全沒用,它只會把多數的錯誤鎖定成最終答案。

    接著試 pre-upscale:先用 ffmpeg 把白板區域裁出來、用 Lanczos 4 倍放大、加 unsharp,再丟 Gemini。直覺上放大應該有用,對吧?

    ffmpeg -i wb.jpg -vf "crop=720:820:480:30,scale=iw*4:ih*4:flags=lanczos,unsharp=7:7:1.5" wb_up.jpg

    結果:2.5 Pro 從 8/12 掉到 6/12,3 Flash Preview 從 6/12 掉到 2/12。放大反而讓準度下降。

    事後想通:Lanczos 是重採樣,不是超解析。它只是讓相鄰像素平滑插值,沒有增加資訊量。原本 3 像素的磁鐵放大成 12 像素,但那 12 像素是模糊的平均值,模型反而更難從中辨認形狀。

    要真的增加資訊量必須用 AI 超解析(Real-ESRGAN 這類),而不是幾何插值。但那會在機器上增加 PyTorch + 模型的負擔。與其走這條,不如直接面對問題:RTSP 這條路本身就有上限

    關鍵 Pivot:把 human 設計進 loop

    這是今天最重要的決策點。

    我原本堅持 RTSP 全自動,理由是「靠人不穩定,請妹妹拍她都不拍」。這個理由本身沒錯 —— 強制依賴他人的流程一定會斷。但我把「靠他人」跟「靠自己」混為一談了。我自己每天可以拍照,不穩定的只是「依賴別人」這一環

    更深層的領悟:為什麼執著於「100% 自動」?因為我下意識想把人完全排除。但對一個給醫生回診參考的月報系統來說,人的角色不該是全被排除,而應該是最後一道驗證。理由:

    • OCR 永遠有錯,醫療資料的錯誤比缺失代價更高
    • 家屬(我)本來就是最終看月報的人,有修正權限跟動機
    • 拍照的動作本身就是「我已經確認白板內容正確」的確認點
    • 沒有必要為了系統 purity 去承擔「誤導醫生」的風險

    這是整個專案最有價值的 design pattern 轉變 —— 從「zero-human automation」到「human-as-verifier」。具體分工:

    角色職責
    我(家屬)看白板、判斷、拍清楚的照片、事後在 iDempiere 修正 OCR 誤判
    Bot + Gemini 3把照片轉成結構化資料寫進 iDempiere、附上原檔
    iDempiere永久保存、提供 UI 讓月底回診時查閱

    最可怕的情境 —— OCR 幻覺亂寫進 DB 誤導醫生 —— 被「我本來就要檢查 iDempiere」這個 policy 擋住了。系統可以不完美,因為人在迴圈裡。

    Telegram Bot 架構

    決定走 human-in-the-loop 後,剩下的工程問題是「怎麼把手機拍的照片快速送到 server」。選項比較:

    方案優點缺點
    Telegram Bot零 app 安裝、即時回饋、從任何地方都能傳要建 bot token
    Syncthing 資料夾同步純本機、無雲端要裝 app + 手動移動檔案
    自架 HTTP 上傳頁不用 app要開 browser、每次登入

    Telegram 明顯最低摩擦,而且我本來就用 Telegram 傳照片 debug。一個 @BotFather 建 bot、5 分鐘拿到 token、寫一個 Python 長輪詢腳本就搞定。

    Bot 的最終能力:

    使用者動作Bot 行為
    📸 傳白板照片Gemini 3 Flash Preview OCR → 找今天紀錄 → update 或 create → 附加照片 → drain 文字佇列
    📝 傳 LINE 居服員回報文字[SHIFT|HH:MM] 前綴 → prepend 到今天紀錄的 Description 欄位
    🕑 今天還沒紀錄時傳文字存 pending 佇列,等照片來時自動塞入
    /pending, /clear, /id管理指令

    Shift 標記:用時段而不是時間點

    iDempiere 既有資料的 Description 欄位有一個我沒注意到的約定格式:

    [NIGHT|20:13]今天把東西往外丟
    [DAY|17:56]攻擊居服員

    格式是 [SHIFT|HH:MM]事件,新的在最上面,\n 分隔。照顧者三班制:

    • DAY(早班):07:00-17:59
    • NIGHT(晚班):18:00-23:59
    • GRAVEYARD(大夜):00:00-06:59

    Bot 根據當下時間自動產生正確的 shift 標記。這個設計好處是醫生看月報時能直接對應到「哪一班回報的事件」,這對失智照護尤其重要 —— 同一個病人,不同班次的表現可能完全不同(例如日落症候群)。

    Find-or-Create:同一天一筆紀錄

    一個設計決策:同一天傳多次照片要建新紀錄還是更新舊的?

    選項:

    • (a) 每次都 create 新的 → iDempiere 一天可能有多筆,報表聚合需要 GROUP BY 日期
    • (b) 同天 find-or-update → 資料乾淨,但要實作 OData filter 查當天紀錄
    • (c) 帶時序保留所有版本 → 最完整但最複雜

    選 (b)。理由是月底回診時醫生看的是「今天的狀態」,不需要看一天之內的修改歷史。實作核心:

    def idempiere_find_today(token):
        date_str = datetime.now().strftime("%Y-%m-%d")
        r = requests.get(
            f"{IDEMPIERE_URL}/models/z_momsystem",
            headers={"Authorization": f"Bearer {token}"},
            params={"$filter": f"DateDoc eq '{date_str}' and Name eq '{PATIENT_NAME}'", "$top": 1},
        )
        recs = r.json().get("records", [])
        return recs[0]["id"] if recs else None

    還有一個巧妙設計:OCR 回 null 的欄位不進 PUT payload。這樣早上拍一次只填前 6 項、晚上拍一次只改後 6 項,早上的資料不會被晚上的 null 洗掉。整個合併邏輯在 Python 端就搞定,iDempiere 端只接受「明確填入」的欄位。

    iDempiere 附件上傳的小坑

    iDempiere REST API 的附件端點是 POST /models/{table}/{id}/attachments,payload 是 {"name": "...", "data": "base64..."}。踩了一個坑:重複檔名會回 409

    解法:上傳時把當下的 HHMMSS 塞進檔名 suffix。同一張照片重複上傳也不會衝突:

    ts = datetime.now().strftime("%H%M%S")
    fn = f"{orig.stem}_up{ts}{orig.suffix}"

    Systemd Service 自動重啟

    最後收尾:bot 用 systemd service 跑,開機自動啟動 + 崩潰自動重啟。關鍵是設定 Restart=on-failureRestartSec=10

    [Unit]
    Description=Tapo Caregiver Telegram Bot
    After=network-online.target
    Wants=network-online.target
    
    [Service]
    Type=simple
    User=tom
    WorkingDirectory=/home/tom/tapo-caregiver
    ExecStart=/home/tom/tapo-caregiver/venv/bin/python telegram_bot.py
    Restart=on-failure
    RestartSec=10
    Environment=PYTHONUNBUFFERED=1
    
    [Install]
    WantedBy=multi-user.target

    成本:免費版剛好夠用

    這個專案我原本因為 debug 爆量升級了 Google AI Studio 付費版。但實際跑 production 的時候算了一下用量:

    • 晚上 8 點拍一次照片 = 1 次 Gemini 3 Flash Preview 呼叫
    • LINE 回報文字 = 0 次(純文字不進 OCR,只是 prepend 到 Description)
    • 一天總用量:1~2 次

    Gemini 3 Flash Preview 的免費 tier 是 每天 20 次GenerateRequestsPerDayPerProjectPerModel-FreeTier, limit: 20, model: gemini-3-flash),實際從先前的 429 錯誤訊息確認過。

    也就是說 完整上線後可以馬上取消付費,免費額度就夠日常跑。debug 階段的付費只是為了不被 quota 卡住開發速度。

    兩天下來的反思

    1. 物理限制不是模型問題,不要用演算法去試

    RTSP 畫面中磁鐵 3~5 像素這件事,不管換什麼模型、不管怎麼放大、不管怎麼 prompt,都解決不了。我浪費了兩輪嘗試才認清:先驗證訊號源的解析度是否足夠,再決定上面要疊什麼演算法

    2. 便宜模型會偽裝在看

    Gemma 4 和 Gemini 2.5 Flash 在困難任務上會套預設答案(全 box 1 或全 box 2),這種偽答剛好能騙過不注意的準度統計。同一張圖跑兩次是揭穿偽答最便宜的方法 —— 真的在看的模型答案應該高度一致,套公版的會變。

    3. 投票救不了系統性偏誤

    Gemini 2.5 Pro 跑 3 次投票準度從 8/12 掉到 6/12,因為它的錯誤是系統性的(3 次都答同一個錯的答案)。投票只對隨機噪音有用,對系統性偏誤沒用。想救系統性偏誤,要用不同模型交叉比對,而不是同一個模型多次跑。

    4. Human-in-the-loop 不是妥協,是正確設計

    我原本想做 100% 自動化是出於「系統純粹」的直覺。但對醫療情境來說,把人排除才是風險來源。正確問題不是「怎麼不靠人」而是「人在哪個環節最有價值」。答案是:人當最終 verifier,機器做勞力密集的 OCR + 結構化 + 儲存。

    5. 工具思維 vs 平台思維

    我原本想把 RTSP 攝影機變成一個「全知白板記錄系統」,這是平台思維,野心大但難。pivot 之後變成「Telegram bot 幫我打字少打一些」,這是工具思維,目標小但能跑。工具思維的上線速度 ≫ 平台思維的完美程度

    6. 讓 AI 自己跑模型賽馬

    過程中我讓 AI 自動跑一圈 Gemma 4 / 2.5 Flash / 2.5 Pro / 3 Flash Preview,把命中率、錯誤類型、各自的 observation 都列出來。我只要看那張表就能判斷用哪個。這種「讓 AI 幫你比較 AI」的 meta-loop 是 vibe coding 很爽的一點 —— 選型決策從「讀文件猜性能」變成「直接實測算分」。

    最終上線狀態

    組件狀態
    Telegram bot(@your_bot_name,白名單限制)✅ LIVE
    Gemini 3 Flash Preview OCR (schema-constrained + box index + layout map)
    iDempiere REST find-or-create + attachment upload
    Description shift tag (DAY/NIGHT/GRAVEYARD) 自動 prepend
    pending notes queue(文字先到、照片後到的情境)
    systemd service 開機自動啟動
    PTZ ONVIF 控制(暫時不用,因為 pivot 到手機路線)✅ 備用

    每日 SOP:

    1. 晚上 8 點我拍一張白板照片
    2. Telegram 分享給 @your_bot_name
    3. Bot 跑 OCR + 寫 iDempiere + 回覆結果(10~20 秒)
    4. 看到有讀錯的,打開 iDempiere 手動改
    5. 居服員白天在 LINE 的回報,我看到時直接貼到 bot,自動 prepend 到今天的 Description
    6. 月底回診時點開 iDempiere 給醫生看趨勢

    總結:這篇文章的「二階 vibe coding」

    這篇文章本身也是 vibe coding 的產物 —— 我叫 AI 把兩天的工作回溯成一篇技術文,自己只負責審核跟指正敏感資料。這種「AI 幫你跟 AI 協作的過程寫回顧」也算是一種 meta-loop:決策的當下有 AI 幫忙驗證,事後有 AI 幫忙紀錄。我負責方向跟價值判斷,剩下的雜活都給 AI。

    兩天下來最重要的一句話:「vibe coding 不是減少思考,是加速思考。」 AI 幫你快速驗證假設、生成原型、寫測試、跑實驗、甚至幫你寫反思文章。但你要知道該往哪個方向走、什麼時候該 pivot、什麼時候該停手。AI 是踏板車,你是駕駛。

  • Hacker News 每日精選 – 2026-04-13

    “`html




    科技趨勢週報:從軟體經濟學到硬體底層的效率革命

    🚀 科技趨勢週報:從軟體經濟學到硬體底層的效率革命

    今日的科技趨勢呈現出明顯的「雙極化」現象:一邊是工程管理層面對於軟體開發經濟效益的深度反思,另一邊則是研究者在計算底層運算邏輯與硬體架構(如 ROCm 挑戰 CUDA)上的極致追求。理解這些從高層管理到微觀運算的交集,是每一位技術領導者與開發者保持競爭力的關鍵。


    🤖 AI/機器學習與硬體架構

    Taking on CUDA with ROCm: ‘One Step After Another’ 🛠️

    挑戰 NVIDIA CUDA 的之路:AMD ROCm 的進程

    • 深入探討 AMD 如何透過 ROCm 生態系,逐步在 AI 運算領域挑戰 NVIDIA 的壟斷地位。
    • 強調這不是一蹴而就的過程,而是「一步一腳印」的軟硬體整合工程。
    • 對於在意硬體供應鏈多元化的企業來說,這是一個極具參考價值的觀察點。

    原文連結:Read more at EE Times

    🛠️ 開發工具與軟體設計

    Show HN: boringBar – macOS 的任務列風格 Dock 替代方案 🖥️

    boringBar:為 macOS 打造的新型任務列工具

    • 這是一款旨在取代傳統 Dock 的任務列風格工具,提供更簡潔的操作介面。
    • 適合追求極簡主義、希望優化工作流的 Mac 重度使用者。
    • 目前正處於開發者展示階段,吸引了大量社群關注。

    原文連結:Visit boringBar.app

    Bring Back Idiomatic Design (2023) 🎨

    回歸慣用設計:找回程式碼的直覺美感

    • 探討現代軟體工程中,過度設計(Over-engineering)如何導致設計感的喪失。
    • 呼籲開發者回歸「慣用設計」(Idiomatic Design),讓程式碼符合語言與框架的原生邏輯。
    • 這對於提升代碼的可讀性與可維護性具有深遠影響。

    原文連結:Read the essay

    A perfectable programming language 🧬

    一種「可完美化」的程式語言理論

    • 探討如何透過數學驗證與邏輯結構,建立一種理論上可以達到「完美狀態」的語言。
    • 對於開發高安全性、高可靠性系統(如航太或醫療設備)的研究者來說極具價值。
    • 展示了程式語言設計如何與形式化驗證(Formal Verification)深度結合。

    原文連結:Explore the theory

    💼 創業與商業管理

    The Economics of Software Teams: Why Most Engineering Orgs Are Flying Blind 📉

    軟體團隊的經濟學:為何多數工程組織都在盲目飛行

    • 揭示了當前科技公司在衡量工程產出與成本時,普遍缺乏精準數據的困境。
    • 討論了如何從經濟學角度重新審視研發預算與團隊規模的配置。
    • 對於技術主管(CTO/VP of Engineering)來說,這是優化研發 ROI 的必讀指南。

    原文連結:Read the analysis

    🔬 開源專案與底層研究

    All elementary functions from a single binary operator 🔢

    從單一二元運算子實現所有初等函數

    • 一篇具有數學高度的論文,展示了極簡運算子構建複雜函數的可能性。
    • 對編譯器設計、密碼學以及計算理論的研究具有高度啟發性。
    • 展示了底層運算邏輯的極致簡約與強大。

    原文連結:View on arXiv

    Optimization of 32-bit Unsigned Division by Constants on 64-bit Targets ⚙️

    針對 64 位元架構優化 32 位元無符號常數除法

    • 深入研究如何在現代 CPU 架構上,透過演算法優化來提升除法運算的效能。
    • 屬於典型的「硬核」底層優化研究,對系統程式設計師極具參考價值。
    • 探討了硬體特性與軟體演算法之間的協同優化空間。

    原文連結:View on arXiv

    🌈 其他有趣話題

    • DIY Soft Drinks 🥤:如何在家中透過科學原理自製各種口味的汽水。查看全文
    • Most people can’t juggle one ball 🧠:探討認知能力與專注力的心理學研究。了解更多
    • Ask HN: What Are You Working On? (April 2026) 🛠️:社群討論,看看全球開發者現在都在忙什麼專案。參與討論

    💡 今日觀點與行動建議

    觀察今日的熱門話題,我們可以發現一個核心主題:「效率的重新定義」。無論是商業管理層面尋求工程投入的效率,還是底層研究尋求運算指令的效率,亦或是設計層面尋求語言慣用的效率,大家都在試圖從混亂的現狀中找回「秩序」與「精準度」。

    🚀 給讀者的行動建議:

    1. 對於管理者: 停止僅僅依靠直覺來分配工程資源,開始建立以數據為基礎的軟體經濟模型。
    2. 對於開發者: 除了追求新框架,請務必回歸「慣用設計」(Idiomatic Design),寫出符合語言本質的代碼,這才是長期的生產力。
    3. 對於學習者: 不要忽視底層數學與架構,理解了「單一運算子如何構建複雜函數」,你才能真正掌握計算的本質。



    “`

  • Ionic 逆向重建 Flutter 完整踩坑紀錄:三套 APP、20+ 個坑、一套方法論

    重點摘要

    • 從 Ionic 1 / AngularJS + WordPress PHP 逆向重建三套 Flutter APP,歷時一週,踩了 20+ 個分類的坑
    • 最毒的方法論錯誤:審查時讀了原始碼,實作時卻只靠缺失清單——導致多輪審查永不收斂
    • PHP Broker API 的 Content-Type 是 text/html、數值欄位全是 String、token 欄位名稱每個子系統不同
    • WordPress Multisite 的 table prefix 陷阱:直接打 plugin 路徑走 main site(wp_),跟 WP Admin 看到的(wp_2_)不同
    • 逆向工程的核心原則:原始碼是唯一 ground truth,不是用戶的記憶,不是截圖,不是 AI 的推測

    這篇文章記錄了一個完整的逆向重建專案:將一套社區管理系統(三套 APP + WordPress 後端)從 Ionic 1 / AngularJS 遷移到 Flutter。不是新開發,是一比一逆向複製——原版有的每一個按鈕、每一個欄位規則、每一個邊角功能,新版都必須完全復現。

    三套 APP 分別是:住戶 APP(社區公告、包裹、費用等)、管理平板(出勤、包裹管理、公設租借、訪客登記等)、門禁保全 APP(簽到簽退、巡邏、QR 掃描)。它們共用同一個 WordPress PHP 後端。

    以下是按主題分類的完整踩坑紀錄和方法論總結。

    一、逆向重建最毒的方法論錯誤是什麼?

    逆向重建最毒的方法論錯誤是「審查讀原始碼、實作不讀」。這聽起來不可能,但在 AI 輔助開發中極其常見:

    1. 審查 Agent:從原版 HTML / JS 逐行讀 → 產出缺失清單 → 每項有行號出處 ✅
    2. 實作 Agent / 新 Session:拿到缺失描述(如「公告無窮捲軸缺失」)→ 直接寫 Flutter → 沒有讀原版邏輯
    3. 結果:修了 A,同頁的 B/C/D 仍缺(因為根本沒讀那頁的完整原始碼)
    4. 下輪審查:再從原版讀 → 再找到 B/C/D → 循環不斷

    我們經歷了 7 輪審查才讓管理平板的缺失清單收斂。前 3 輪每輪都有 10+ 個新發現,根本原因只有一個——方向反了。

    建立的強制規則

    • 拿到缺失 ID 時,必須先讀對應的原始碼(file:line),不得只靠缺失描述動手
    • 修某頁某功能時,如果手上沒有該頁完整 Source Inventory → 必須先做 Source Inventory
    • Review 方向必須是:原版 HTML → 原版 JS → 原版 PHP → 列清單 → 逐項對照新版。禁止從 Flutter 出發猜原版有什麼

    二、Source Inventory 協議:逆向工程的唯一正確開始方式

    Source Inventory 是在動手寫任何程式之前,先把原版的所有功能逐行列出來的過程。每一項都必須標注原始碼來源(檔案 + 行號),沒有行號的項目等於捏造的。

    - [ ] 登出按鈕
          來源:account.html:142 <button ng-click="logout()">
    
    - [ ] 棟別/樓層兩層下拉選單
          來源:visitor-form.html:55-67, controllers.js:9366-9398
    
    - [ ] 黑名單檢查(借用簽名後、API 呼叫前)
          來源:publicequip.js:2040-2052

    清單涵蓋四大類:所有 UI 元素(按鈕、欄位、badge、空狀態提示)、所有互動行為(點擊、滑動、下拉刷新)、所有 API 呼叫及欄位映射所有頁面入口/出口

    最容易漏的永遠是邊角功能:登出藏在 header component、空狀態提示在條件分支、黑名單檢查在簽名驗證之後。主流程之外的東西,AI 不會主動去找。

    三、PHP Broker API 的七大陷阱

    這套系統的 API 層是 WordPress PHP plugin,所有請求都走同一個 Broker endpoint。以下是我們踩過的每一個 API 相關的坑:

    3.1 Content-Type 是 text/html

    PHP Broker 的回應 Content-Type 是 text/html 而非 application/json。Dio(Flutter HTTP 套件)如果設了 ResponseType.json,會解析失敗。必須設 ResponseType.plain 再手動 jsonDecode

    3.2 數值欄位全是 String,除了偶爾不是

    大多數 JSON 數值欄位回傳的是字串("id":"1")。但 $wpdb->get_results() 對 int DB 欄位有時會回傳 PHP native int(JSON 裡沒有引號)。解法:所有 nullable String 欄位一律用 ?.toString(),避免 type 'int' is not a subtype of type 'String?'

    3.3 Token 欄位名稱每個子系統不同

    子系統 Token 放在 例外
    主 Broker(住戶/平板)data.cookie
    Guard Broker(門禁)data.tokenvalidate_authdata.cookie

    搞混了就會被判定為 anonymous user,API 回 "非有效角色,無法使用!",而且沒有任何認證失敗的明確錯誤碼

    3.4 returnCode 不統一

    "OK""0"(package 模組)、"1""ERROR""dup"——全是字串,不要做整數比較。每個模組的成功碼不同,必須逐一確認。

    3.5 Category / Method 命名混亂

    有的是 snake_case(get_ticket_by_member_id),有的是 camelCase(retrivePublicEquipTXN——對,retrive 是拼錯的),有的是完全不同的規則(createTableAccount)。必須從 PHP 程式碼抄出精確字串,不能猜。

    3.6 Response 結構不一致

    • mutual:回 {rows_1:[], rows_2:[]}(自己的 + 社區其他人的)
    • ticket:回 {data: {rows, total, totalPages}}(分頁結構)
    • package_light:returnCode 用 "0" 而非 "OK"
    • getTwCode:空表時回 data: ""(空字串),不是 []

    3.7 磁卡批次 API 是 flat array

    大部分 API 的 data 格式是 {data: {key: value}},但磁卡批次領取是平坦陣列 [{id, card_no, status}]。PHP 端用 is_array($data) 判斷。這意味著 Flutter HTTP client 的 data 參數不能寫死為 Map,必須改成 dynamic

    四、WordPress Multisite 的 Table Prefix 陷阱

    WordPress Multisite table prefix 陷阱是整個專案最令人崩潰的問題之一。直接打 plugin PHP 路徑(如 /wp-content/plugins/.../API_Broker.php)時,WP 識別為 main site (blog_id=1),使用 wp_ 前綴。但在 WP Admin 介面、phpMyAdmin 看到的是 wp_2_(子站表)。

    後果:seed 測試資料時插到 wp_2_ulifeplus_*,但 API 查的是 wp_ulifeplus_*——資料永遠看不到,而且不報錯,就是空陣列。

    另外,改網域必須改 5 個地方wp_options(siteurl + home)、wp_blogs(domain)、wp_site(domain)、wp-config.php(DOMAIN_CURRENT_SITE + WP_HOME + WP_SITEURL)。DOMAIN_CURRENT_SITE必須含 port,否則會無限 302 redirect。

    五、validate_role() 全面封鎖事件

    所有 28 個 PHP endpoint 都在進入主邏輯前呼叫 validate_role()。原版只接受 ulifeplus_api / ulifeplus_admin / administrator / ulifeplus_operator,不含住戶帳號 ulifeplus_subscriber

    原因:原版平板用的是專屬服務帳號(QR code 開通碼流程),不是住戶帳號直接登入。重建版讓住戶直接登入 → 所有 API 一律 ERROR

    修補方案需要雙層:(1) patch PHP 接受 ulifeplus_subscriber;(2) 給測試帳號加 ulifeplus_api role。而且 wp_capabilities 的序列化格式 a:N:{s:LEN:"role_name";b:1;} 的 LEN 必須精確——寫錯一個字元,整個 unserialize 就失效,用戶變成沒有任何 role。

    六、三套 APP 的進化軌跡:從混亂到方法論

    第一套:住戶 APP——摸清後端規則

    住戶 APP 是最先做的,主要踩了後端相關的坑:Content-Type 是 text/html、數值欄位全是 String、member_id vs WP user_id 混淆、validate_role 全面封鎖、WP Multisite table prefix。這些經驗讓我們對 PHP Broker 的行為有了完整的心智模型。

    第二套:管理平板——方法論崩潰與重建

    管理平板是最複雜的(10+ 模組),也是方法論崩潰的戰場。7 輪審查、每輪都有新發現、多個 Agent 交叉審查卻互相誤報(false positive)。最終逼迫我們建立了 Source Inventory 協議、四大卡點規則、和「審查必須從原版出發」的強制流程。

    核心教訓包括:

    • Grid → Card List 轉換會遺失 checkbox column 和 cellTemplate 等「免費」UI 行為
    • 條件式按鈕(enableShowClosed 等)的每個 ng-if 都是業務意圖,不能合併或省略
    • smart_disp.get_hidden_code() 遮名演算法需要完整移植,不能簡化
    • 日期範圍查詢必須含時間(00:00:00 / 23:59:59),否則 BETWEEN 漏掉當天
    • Feature flag 的預設值不一定是 false,必須讀完整條件判斷

    第三套:門禁 APP——方法論成熟

    到第三套門禁 APP 時,Source Inventory 協議已經內化。整個 APP 從原始碼分析到 21/21 測試通過只花了一天。但還是踩了新坑:

    • Guard Broker 的 token 欄位是 data.token(不是主 Broker 的 data.cookie
    • PHP class constant const data = "date" 從不生效(bareword array key 行為)
    • showDialog 的 BuildContext 跨 async 導致確認 dialog 靜默失敗
    • HTTP 部署下相機 API 被瀏覽器封鎖(Secure Context 限制)
    • Flutter Web 的 SPA 狀態在測試間污染(需 full HTTP reload)

    七、Flutter 遷移的技術坑清單

    症狀 修法
    flutter_secure_storage Web 失敗HTTPS 才能用 Web Crypto APIWeb 改用 SharedPreferences
    Platform.isAndroid Web crashdart:io 在 Web 不可用kIsWeb + defaultTargetPlatform
    CORS 重複 headerAccess-Control-Allow-Origin: *, *PHP 已有 CORS,不要加 .htaccess
    Dialog context stale確認後邏輯靜默跳過dialogCtx 不用外層 context
    Release build debugPrint 消失完全無錯誤訊息寫 SharedPreferences breadcrumb
    ConsumerWidget 無 initState需要 async 初始化改用 ConsumerStatefulWidget
    base-href 部署子路徑資源 404--base-href=/app/
    API_BASE_URL dart-defineWeb 連到 localhost--dart-define=API_BASE_URL=http://IP:PORT

    八、AI 輔助逆向工程的認知陷阱

    AI 輔助逆向工程的認知陷阱是這個專案最深層的教訓。AI 在讀原始碼方面能力極強,但在以下場景會系統性犯錯:

    1. 「我以為我知道這頁有什麼」——AI 會基於已讀的程式碼推測未讀的部分,經常猜錯
    2. 改名後誤報缺失——搜尋 scan_addr_code() 找不到,但 Flutter 用了 _scanAddressCode(),同樣邏輯換了名字就被誤報
    3. 函數名稱 ≠ 實際行為——updateUser() 實際上還做 log 寫入、狀態機轉換;getActiveItems() SQL 是 status != 'deleted' 不是 status = 'active'
    4. 「延後功能」不是藉口——AI 傾向把不確定的功能標記為「deferred」,但只要原版存在的就必須實作

    破解方式:建立 Source Inventory 清單,每項附行號。行號是可審計的(可以回去原始碼驗證),AI 的記憶不可審計。

    九、跨專案知識管理:Brain 檔案系統

    為了讓踩過的坑不只活在一個專案裡,我們建立了 Domain Brain 系統:每個技術領域一個 markdown 文件,記錄所有踩過的坑、修法、和來源。

    例如 legacy-code-rebuild.md 記錄了逆向工程的通用方法論;wordpress-broker.md 記錄了 WP Broker API 的所有特殊行為。每次 fix: commit 時,強制要求更新對應的 brain file。

    核心原則:Brain = 上次做的時候踩了什麼坑(經驗)Skill = 正確的做法是什麼(模式)。兩個都要讀。

    十、建議給想做逆向重建的團隊

    1. Source Inventory 先行——不管系統多簡單,先把所有功能列出來(附行號)。簡單的系統往往有最多隱性規則
    2. 後端先跑通——先用 curl 打通每一個 API endpoint,確認 request/response 格式,再寫 Flutter
    3. 每個模組獨立測試——不要等到所有模組做完才整合。我們的 21/21 unit + integration test 在開發過程中抓到了大量問題
    4. 記錄每一個坑——Brain 系統不是可選的,是必要的。第一套 APP 踩的坑如果沒記下來,第三套 APP 會再踩一次
    5. 不要信任 AI 的記憶——AI 會遺忘、會推測、會混淆。只有程式碼行號是可驗證的

    結語

    這個專案讓我深刻體會到:逆向重建的難度不在於「寫新程式」,而在於「完整理解舊系統」。舊系統的每一行 if/else 都是某個真實場景的反映,每一個看似多餘的欄位都有它存在的理由。

    AI 可以加速這個過程,但無法取代「逐行讀原始碼」這個步驟。捷徑是最遠的路——我們前 3 輪審查走了捷徑(從 Flutter 出發找問題),花了 3 倍時間;改用正確方法論(從原版出發列清單)後,第三套 APP 一天就完成了。

    希望這篇記錄能讓正在考慮做逆向重建的團隊少走一些彎路。

    追記:驗收階段又踩的五個大坑(2026-04-13 深夜)

    上面的文章發佈後幾小時,用戶開始實際驗收。結果又炸出一系列問題,每一個都是在開發階段「看了原始碼但沒看完整」造成的。

    坑 10:Dart 的 Map<dynamic, dynamic> 型別推導陷阱

    Flutter 的 API client 在呼叫 api.call(data: {}, token: token) 時,{} 空 Map literal 在 dynamic 參數中被 Dart 推導為 Map<dynamic, dynamic>。原本程式碼檢查 effectiveData is Map<String, dynamic>false → token 永遠不被加到 request 裡。

    影響範圍:27 個 API call。所有傳 data: {} 的頁面全壞(儀表板、帳號開通等),但傳了明確 key 的頁面(如 data: {'address_code': x})正常。這造成「有些頁面好有些壞」的假象,極難排查。

    教訓:原版 JavaScript param.token = value 不做任何型別檢查,直接賦值。重寫到靜態型別語言時,不要加「聰明」的型別守衛。笨方法之所以沒 bug,就是因為它笨。

    坑 11:config.js 常數映射層被完全忽略

    原版 Ionic 有一層 config.js 常數映射:sel → "select"upd → "update"del → "delete"add → "insert"sel2 → "select2"。Flutter 重寫時,有些地方用了短名(config.js 的 key),有些用了長名(PHP 接受的值)。PHP 只認長名。

    影響範圍:19 個 API call。寄放交辦頁面完全打不開(PHP switch/case 不匹配,回 ERROR 但 message 空字串),debug 時完全看不到線索。

    教訓:逆向重建時必須識別原版的每一層抽象。看了 controllers.js(呼叫端)和 factories.js(發送端)不夠——中間的 config.js 映射層不能跳過。每一個字串值都必須追到 PHP handler 的 switch/case 確認一致。

    坑 12:捏造不存在的 API method

    Flutter 程式碼裡出現了 method: 'select_category_detail'(包裹統計)和 category: 'util'(多帳號驗證碼)等原版完全不存在的 API call。追溯原始碼發現:前者在原版是純客戶端計算(groupBy + filter),後者正確的 category 是 'app_member'、method 是 'register_switchsite_validation_code'

    教訓:如果在原始碼裡找不到某個 API endpoint,那它就不存在。不要「合理推測」一個 API 可能存在。

    坑 13:API URL 不應該寫死在 compile time

    Flutter 用 --dart-define=API_BASE_URL=http://192.168.0.48:8010 在 compile time 寫死 API URL。結果 LAN 和 HTTPS 要分別 build,Cloudflare 快取舊 JS 時 URL 就錯。

    原版做法:API_BROKER_URL 在 config.js 是空字串,QR 碼啟動或登入時從 server 回傳 burl(blog_site_url),組合成完整 URL 存入 localStorage。

    修法:Web 版用 Uri.base.origin 自動偵測當前頁面的 origin 作為 API base URL。不管從哪個網域開,API 自動跟著。compile time 的 --dart-define 只作為 fallback。

    坑 14:外部服務走錯路由

    廣告 Banner API 在原版是打一個完全獨立的外部伺服器(vender01.tw),不是走 WP Broker。Flutter 把它包成了 category: 'ad_banner' 送進 WP Broker,當然找不到。

    教訓:原版有多個 factory(brokerFactorycommonServerFactoryomFactoryapi-vender),每個打不同的 server。重寫時必須識別每個 API call 走的是哪個 factory、打的是哪個 server。

    總結:為什麼驗收時才發現這些問題

    以上 5 個坑有一個共通點:「看了原始碼」不等於「看完了原始碼」。每次都是「看了主要的幾個檔案」但漏了中間層(config.js 映射、factory 路由、型別推導規則)。

    逆向重建的正確審查流程應該是:從 PHP handler 的 switch/case 開始,反向追每一個常數值回到 config.js,再追到 controllers.js 的呼叫端。不是從呼叫端出發猜 PHP 接受什麼。方向反了,就永遠追不完。

  • Flutter Web + Selenium E2E 測試踩坑全紀錄:九大實戰教訓

    重點摘要

    • Flutter Web 將所有 UI 渲染到 Canvas,Selenium 必須透過 Tab 鍵啟動語意樹(flt-semantics)才能操作元素
    • showDialog 的按鈕呼叫 Navigator.pop 時,必須使用 dialog 自己的 BuildContext,否則 async 後 context stale 會導致靜默失敗
    • HTTP 部署無法使用相機(非 Secure Context),需實作手動輸入 fallback
    • Release build 的 debugPrint 完全靜默,debug 必須靠 SharedPreferences breadcrumb + Selenium 輪詢 localStorage

    本文記錄了在 Flutter Web 應用上進行 Selenium E2E 自動化測試時踩過的所有坑。這不是教科書式的教學,而是一次完整的實戰復盤——從「為什麼 Selenium 找不到任何元素」到「為什麼確認 dialog 回傳 null」,每一條都是反覆碰壁後才理解的真相。適合正在做 Flutter Web 自動化測試、或考慮將 Ionic/AngularJS 遷移到 Flutter 的開發者。

    一、Flutter Web 語意樹(Semantics Tree)是什麼?為什麼 Selenium 找不到元素?

    Flutter Web 語意樹是 Flutter 為了無障礙功能(Accessibility)而產生的一組 DOM overlay 元素。與 React 或 Vue 不同,Flutter Web 預設將所有 UI 渲染到 <canvas> 元素中,DOM 裡沒有任何可見的文字節點。這意味著 Selenium 的常規定位方式(XPath 文字搜尋、CSS text selector)完全失效。

    如何啟動語意樹?

    語意樹由 flt-semantics 自定義元素組成,但預設是未啟動的。啟動方式只需要一行:

    driver.find_element(By.TAG_NAME, "body").send_keys(Keys.TAB)

    一次 Tab 鍵足以觸發整個語意樹。之後,所有語意元素都會以 <flt-semantics> 出現在 DOM 中。讀取文字的穩定做法:

    text = driver.execute_script("return arguments[0].innerText", element)

    語意角色速查表

    Flutter Widget CSS Selector 備註
    ElevatedButton / TextButtonflt-semantics[role='button']innerText 包含按鈕文字
    BottomNavigationBar Tabflt-semantics[role='tab']innerText 含 “Tab X of Y”
    SwitchListTileflt-semantics[role='switch']title 文字不在 innerText!
    FloatingActionButtonflt-semantics[flt-tappable]tooltip 文字在 innerText
    DropdownButtonflt-semantics[role='button']同按鈕角色

    最大的坑:SwitchListTile 的 title 文字(如「開始巡邏」)完全不會出現在語意樹的 innerText 中。你必須用 role='switch' 去定位元素,而不是搜尋 title 文字。這個坑讓我浪費了整整一小時。

    ScrollView 內容不在語意樹

    AlertDialog 裡的可滾動 ListView 內容(如 RadioListTile 選項列表)全部以 Canvas 渲染,不在 flt-semantics 中。測試時只能驗證「dialog 是否開啟」(確認「關閉」按鈕出現),無法直接驗證列表內容。

    二、Flutter Web SPA 狀態污染:為什麼 driver.get() 沒有重置頁面?

    Flutter Web SPA 狀態污染是指在同一個 Flutter 應用中,前一個測試留下的 widget 狀態(dialog、表單、loading)會影響下一個測試。這是因為 Flutter Web 使用 hash-based routing,driver.get(url + "/#/settings") 如果 hash 沒有變,瀏覽器不會發出新的 HTTP 請求,Flutter widget tree 完全保持原狀。

    # ❌ 錯誤:如果已在 #/settings,不會重置
    driver.get("http://host:port/app/#/settings")
    
    # ✅ 正確:先到無 hash URL(觸發真正的 HTTP reload)
    driver.get("http://host:port/app")         # Step 1: full reload
    time.sleep(3)
    driver.find_element(By.TAG_NAME, "body").send_keys(Keys.TAB)  # Step 2: 啟動語意樹
    time.sleep(1)
    driver.execute_script("window.location.hash = '/settings'")   # Step 3: SPA 導航
    time.sleep(2)

    這個模式在 session-scoped fixture 中尤其重要。當多個測試共享同一個 driver 時,前一個測試的 dialog 可能還開著,下一個測試找不到預期的元素。

    三、BuildContext 跨 async 使用的致命陷阱

    BuildContext 跨 async 陷阱是整個 debug 過程中最花時間的一個坑——3 小時才定位到根因。症狀是:確認 dialog 明明點了「是」,但後續邏輯完全沒執行,沒有任何錯誤訊息。

    問題程式碼

    Future<bool> _confirm(String message) async {
      final result = await showDialog<bool>(
        context: context,           // 外層 widget 的 context
        builder: (_) => AlertDialog(
          actions: [
            TextButton(
              // ❌ 用外層 context 呼叫 Navigator.pop
              onPressed: () => Navigator.pop(context, true),
              child: const Text('是'),
            ),
          ],
        ),
      );
      return result == true;  // null == true → false → 邏輯被跳過!
    }

    發生了什麼?

    1. showDialog() 開啟一個新的 Route(dialog route)
    2. 使用者點擊「是」
    3. Navigator.pop(context, true) 中的 context 是外層 widget 的 BuildContext
    4. 如果外層 widget 在 async 等待期間被 Framework 重建(例如 Provider 狀態更新),該 context 可能 stale
    5. Navigator.pop() 找到錯誤的 Navigator 或 pop 了錯誤的 route
    6. showDialog() 的 Future 得到 null(而非 true
    7. null == truefalse → 確認邏輯認為使用者「取消」了
    8. 所有後續程式碼被跳過,沒有任何錯誤訊息

    正確做法

    builder: (dialogCtx) => AlertDialog(
      actions: [
        TextButton(
          // ✅ 用 dialog 自己的 context
          onPressed: () => Navigator.of(dialogCtx).pop(true),
          child: const Text('是'),
        ),
      ],
    )

    通用規則:showDialog 的 builder 中,永遠使用 builder 參數提供的 context(命名為 dialogCtx),不要使用外層 widget 的 context。這在 Flutter 官方文檔中有提到「Don’t use BuildContext across async gaps」,但在 dialog builder 內部容易忽略。

    四、Release Build 下的靜默失敗:debugPrint 不見了

    Flutter release build 的靜默失敗是另一個時間殺手。flutter build web 產出的是 release build,以下行為與 debug build 完全不同:

    • debugPrint() 完全靜默——不會輸出到 browser console
    • assert() 完全跳過——不會觸發 assertion error
    • 未被 catch 的 async exception 由 Flutter Framework 處理,通常靜默記錄

    Debug Breadcrumb 技巧:寫入 SharedPreferences,Selenium 讀 localStorage

    // Dart side — 在 try/catch 每個步驟寫 breadcrumb
    try {
      final prefs = await SharedPreferences.getInstance();
      await prefs.setString('flutter.dbg', 'step_A: loading points');
      // ... 業務邏輯 ...
      await prefs.setString('flutter.dbg', 'step_B: saved record');
    } catch (e) {
      final prefs = await SharedPreferences.getInstance();
      await prefs.setString('flutter.dbg', 'ERROR: $e');
    }
    
    # Selenium side — 輪詢 localStorage
    for i in range(20):
        time.sleep(0.5)
        dbg = driver.execute_script("return localStorage.getItem('flutter.dbg')")
        print(f't={i*0.5}s  dbg={dbg}')
        if dbg and 'ERROR' in dbg: break

    這個技巧讓我在 5 分鐘內定位到「_confirm() 回傳 null」這個根因,而之前靠 console.log 完全看不到任何線索。

    五、Secure Context 限制:HTTP 部署的相機和 Service Worker 全掛

    Secure Context 是瀏覽器對某些敏感 Web API 的安全限制。以下 API 只在 HTTPS 或 localhost 下可用:

    • navigator.mediaDevices.getUserMedia()(相機 / 麥克風)
    • navigator.serviceWorker(Service Worker / PWA)
    • window.crypto.subtle(Web Crypto API,flutter_secure_storage 依賴此 API)

    用 HTTP + IP 位址部署 Flutter Web(如 http://192.168.x.x:8010),嘗試使用 mobile_scanner 掃描 QR code 會得到:

    MobileScannerException(unsupported, This browser does not support displaying video from the camera.)

    解決方案:Web fallback 自動降級

    @override
    void initState() {
      super.initState();
      if (kIsWeb) {
        // Web 版直接跳手動輸入,不嘗試開相機
        WidgetsBinding.instance.addPostFrameCallback((_) {
          _showManualInputDialog();
        });
      }
    }
    
    // 相機版的 errorBuilder 也要處理
    errorBuilder: (context, error, child) {
      if (error.toString().contains('unsupported')) {
        _showManualInputDialog();  // 降級為手動輸入
      }
      return child ?? const SizedBox.shrink();
    }

    生產環境建議架設 HTTPS(Let’s Encrypt / Nginx 反向代理),即可解除所有限制。開發環境用 localhost 也自動視為 Secure Context。

    六、SharedPreferences 在 Flutter Web 的雙重 JSON 編碼

    Flutter Web 的 shared_preferences 套件底層使用 localStorage,但有兩個容易踩坑的特性:

    Key 前綴

    prefs.setString('my_key', 'value');
    // 實際存儲為: localStorage['flutter.my_key']

    值的雙重 JSON 編碼

    prefs.setString('foo', '[{"id":1}]');
    // localStorage 實際內容: '"[{\\"id\\":1}]"'
    // 注意外層多了一組引號 + 轉義
    
    # Selenium 讀取時要解兩層
    import json
    raw = driver.execute_script("return localStorage.getItem('flutter.foo')")
    layer1 = json.loads(raw)     # 解 SharedPreferences 包裝
    parsed = json.loads(layer1)  # 解原始 JSON 內容

    七、非同步快取的時機問題:為什麼清單永遠是空的?

    非同步快取時機問題是一個隱蔽的 bug:App 啟動時以 async 方式從 API 抓取主資料(如巡邏點清單)並快取到 localStorage。但如果用戶在快取完成前就操作,快取為空 → 業務邏輯用空資料建立記錄 → 記錄一旦建立就再也不會重新抓取 → 永遠空白

    解法:On-demand fallback + 恢復修補

    Future<List<PatrolPoint>> _loadPatrolPoints() async {
      var rawPoints = await db.getCachedPatrolPoints();
    
      if (rawPoints.isEmpty) {
        // 快取為空 → 直接打 API 補抓(此 API 不需要 token)
        try {
          final live = await api.getNavPoints(token);
          rawPoints = live.map((item) => {
            'point_id': item['id']?.toString() ?? '',
            'point_name': item['navpoint_name']?.toString() ?? '',
          }).toList();
          if (rawPoints.isNotEmpty) await db.cachePatrolPoints(rawPoints);
        } catch (e) {
          debugPrint('Live fetch failed: $e');
        }
      }
    
      return rawPoints.map((p) => PatrolPoint(...)).toList();
    }
    
    // 恢復進行中任務時也要修補
    if (patrol != null && patrol.patrolPoints.isEmpty) {
      final fresh = await _loadPatrolPoints();
      if (fresh.isNotEmpty) {
        patrol = patrol.copyWith(patrolPoints: fresh);
        await db.updatePatrolRecord(patrol);
      }
    }

    八、Selenium + Firefox geckodriver 實戰注意事項

    Console Log 不可直接取

    Firefox geckodriver 不支援 driver.get_log('browser')。要捕捉 console 輸出,需要在頁面注入攔截器:

    driver.execute_script("""
        window.__logs = [];
        var orig = console.log;
        console.log = function() {
            window.__logs.push(Array.from(arguments).join(' '));
            orig.apply(console, arguments);
        };
    """)
    # 操作後讀取
    logs = driver.execute_script("return window.__logs")

    但在 release build 中,Flutter 的 debugPrint 不呼叫 console.log,所以這招也沒用。前面提到的 SharedPreferences breadcrumb 才是唯一可靠的方式。

    click_text() helper 的完整實作

    封裝一個通用的「找語意元素並點擊」函式,需要同時搜尋 button、tab、tappable 三種角色:

    def click_text(driver, text, timeout=15):
        """Click flt-semantics element whose innerText contains text."""
        end = time.time() + timeout
        while time.time() < end:
            selectors = [
                "flt-semantics[role='button']",
                "flt-semantics[role='tab']",
                "flt-semantics[flt-tappable]",
            ]
            for sel in selectors:
                for el in driver.find_elements(By.CSS_SELECTOR, sel):
                    t = driver.execute_script("return arguments[0].innerText", el) or ""
                    if text in t:
                        driver.execute_script("arguments[0].click()", el)
                        return
            time.sleep(0.5)
        raise AssertionError(f"Clickable element '{text}' not found")

    九、方法論:Flutter Web Debug 的正確排查流程

    當 Selenium 測試與 Flutter Web 出現異常時,依照以下順序排查,可以最快定位問題:

    1. 語意樹是否啟動?按 Tab 後檢查 flt-semantics 元素數量。為 0 → 重按 Tab
    2. localStorage 狀態正確嗎?檢查 flutter.* key,確認 token、快取資料是否存在
    3. HTTP 還是 HTTPS?相機、Service Worker、Web Crypto 都需要 Secure Context
    4. Dialog 回傳值正確嗎?用 dialog 內部的 context 而非外層 context
    5. Release 還是 Debug build?debugPrint 在 release 完全靜默
    6. 寫 debug breadcrumb:try/catch 裡寫 SharedPreferences,Selenium 輪詢 localStorage
    7. SPA 狀態殘留?用 full HTTP reload(無 hash URL)強制重置 Flutter widget tree

    總結:Flutter Web + Selenium 是可行的,但需要正確的心智模型

    Flutter Web 的測試難度不在於「找不到元素」,而在於「找到了但行為不符預期」。Canvas 渲染 + 語意覆層的架構與傳統 HTML DOM 測試完全不同,需要建立新的心智模型。最大的兩個坑——BuildContext 跨 async 靜默失敗release build debugPrint 消失——可以讓你花上數小時 debug 而沒有任何錯誤訊息。

    掌握了這九個教訓後,我們的 Selenium 測試從 0 做到 40/40 全過,涵蓋了登入、導航、簽到/簽退、巡邏流程、設定頁面等完整 E2E 場景。希望這篇記錄能幫到正在走同一條路的開發者。

  • Hacker News 每日精選 – 2026-04-12

    今日科技圈的焦點集中在 AI 技術的效能邊界、開發工具生態的更迭,以及儲存技術的革命性進展。從 AI 模型如何以更小的體積達成資安檢測,到老牌靜態網站生成器 Eleventy 的存續討論,這些話題揭示了軟硬體發展正朝著更高效率與更真實的效能評估邁進。

    🤖 AI / 機器學習

    小模型也能發現 Mythos 曾找出的資安漏洞

    這篇文章深入探討了 AI 在網路安全領域的實力,指出即使是體積較小的語言模型,也能成功識別出過往被認為只有頂尖模型(如 Mythos 專案)才能發現的系統漏洞。

    • 效能邊界模糊:研究顯示模型的大小並非資安偵測能力的絕對唯一決定因素。
    • 成本效益:使用較小的模型進行初步掃描,能大幅降低自動化資安審計的成本。
    • 「凹凸不平的前線」:作者描述了 AI 能力分佈的不均勻性,提醒開發者不應盲目依賴單一模型。

    原文連結:Small models also found the vulnerabilities that Mythos found 🚀

    我們如何打破頂尖 AI Agent 基準測試:以及下一步是什麼

    來自柏克萊大學的研究團隊揭示了目前 AI Agent 評測標準(Benchmarks)存在的漏洞,並討論了為什麼現有的高分可能並不代表真正的智能。

    • 基準測試失效:許多模型透過過度擬合或利用測試集的缺陷來獲取高分。
    • 更可靠的評估:團隊提出了新的評估框架,旨在衡量 AI Agent 在真實、不可預測環境中的應對能力。
    • 信任危機:這篇文章提醒業界,我們需要更透明且具備對抗性的測試方法來定義「強大」的模型。

    原文連結:How We Broke Top AI Agent Benchmarks: And What Comes Next 📊

    🛠️ 開發工具與開源專案

    Eleventy 的終局(The End of Eleventy)

    這是一篇引發開發社群廣泛討論的爭議性文章,探討了知名靜態網站生成器 Eleventy (11ty) 目前面臨的挑戰與作者決定離開的原因。

    • 生態系變遷:分析了現代網頁開發環境如何從簡單的靜態生成轉向更複雜的伺服器端框架。
    • 維護壓力:討論了開源專案在缺乏商業支持下,長期維持核心開發的艱辛。
    • 開發者反思:這篇文章不僅是技術選型的分享,更是對「簡單性」在現代開發中逐漸消亡的感嘆。

    原文連結:The End of Eleventy 🛠️

    我的程式碼有多複雜?(How Complex is my Code?)

    本文探討了衡量程式碼複雜度的各種指標,以及為什麼開發者應該重視「認知負荷」而非僅僅是程式碼行數。

    • 指標誤區:雖然循環複雜度(Cyclomatic Complexity)很有用,但它不能完全代表程式碼的可讀性。
    • 簡化策略:作者分享了幾種重構技巧,幫助開發者將複雜的邏輯拆解為易於理解的模組。
    • 維護成本:複雜度是技術債的主要來源,早期監控複雜度能顯著提升專案壽命。

    原文連結:How Complex is my Code? 🧠

    最簡單的雜湊函數(Simplest Hash Functions)

    這是一篇技術性極強的短文,介紹了幾種在特定場景下效能極佳且實作極其簡單的雜湊演算法。

    • 算法純粹性:展示了如何用寥寥幾行程式碼達成高效的資料分佈。
    • 適用場景:並非所有情況都需要加密等級的雜湊,有時極速的非加密雜湊才是首選。
    • 教育價值:適合想深入理解資料結構底層運作原理的開發者。

    原文連結:Simplest Hash Functions 🔢

    🔬 其他(硬體、科學與法律)

    原子級儲存技術:每平方公分 447 TB 且零耗能

    這是一項重大的硬體突破,研究人員在氟化石墨烯上實現了極高密度的原子級記憶體。

    • 極限密度:447 TB/cm² 的儲存密度遠超目前的商用硬碟與 SSD。
    • 零保持能量:該技術在維持資料儲存狀態時不需消耗電能,對節能減碳有巨大潛力。
    • 未來展望:雖然尚處於實驗階段,但這為超大規模資料中心提供了新的想像空間。

    原文連結:447 TB/cm² at zero retention energy 💾

    美國上訴法院宣佈具有 158 年歷史的家庭蒸餾禁令違憲

    一項法律史上的重要裁決,撤銷了自南北戰爭時代以來對個人在家釀造烈酒的聯邦禁令。

    • 個人自由:法院裁定聯邦政府無權禁止純屬私人用途的家庭蒸餾行為。
    • 歷史背景:這條法律最初是為了確保政府的酒稅收入,現在被視為過度擴權。
    • 社會影響:此舉可能帶動精釀烈酒社群與相關硬體市場的興起。

    原文連結:US appeals court declares home distilling ban unconstitutional ⚖️

    為什麼有意義的日子在度過時看起來平凡無奇

    這是一篇關於人生哲學的短文,探討了「意義感」往往是事後回溯產生的,而非當下的體感。

    • 當下的平凡:最深刻的進步往往發生在看似枯燥的日常練習中。
    • 宏大航線:借用《海賊王》的概念,描述人生的成長是一段漫長的積累,而非瞬間的煙火。
    • 心態建議:讀者應學會接納無聊,因為那是通往不凡的必經之路。

    原文連結:Why meaningful days look like nothing while you are living them 🌱

    🎯 今日觀點

    綜觀今日的熱門話題,我們可以發現一個共同的主題:「去偽存真」。無論是研究者挑戰 AI 基準測試的真實性,還是開發者對複雜程式碼與重量級框架的反思,甚至是法律對過時禁令的修正,都顯示出科技圈正在經歷一場從「追求規模」轉向「追求實效」的質變。

    💡 給讀者的行動建議:

    • 不要迷信大型模型:在實作 AI 應用時,嘗試評估小模型是否能以更低成本達成目標。
    • 檢視你的技術棧:如果像 Eleventy 這樣的工具讓你感到受限,或許是時候簡化你的工作流,回歸到最基礎的開發邏輯。
    • 關注硬體底層:原子級記憶體的突破預示著未來十年儲存成本可能再次大幅下降,思考這對你的資料架構有何影響。
  • Hacker News 每日精選 – 2026-04-11

    🚀 科技趨勢週報:從雲端二十年到月球重返,探索創意的極限

    今日的科技圈展現了極其豐富的多樣性:我們既看到了像 AWS 這樣運作二十年的雲端長青樹,也見證了人類重返月球的關鍵進展。與此同時,開發者們持續在單一 HTML 檔案或一維空間中挑戰遊戲開發的邊界,這些主題提醒著我們,技術的價值不僅在於前沿的突破,更在於長期的維護與對使用者體驗的極致追求。

    🛠️ 開發工具與技術探索

    1. 安裝「所有」Firefox 擴充功能:一場瀏覽器的壓力測試實驗

    這是一項瘋狂的技術實驗,作者嘗試在 Firefox 瀏覽器中安裝數千個擴充功能,以測試軟體的極限。文章詳細記錄了瀏覽器在面對海量套件時的性能崩潰過程、記憶體占用情形以及啟動時間的變化。這不僅是對瀏覽器穩定性的測試,也反映了現代網頁生態中「擴充功能膨脹」的潛在威脅。對於關注瀏覽器引擎與優化的開發者來說,這是一份極具參考價值的壓力測試報告。

    👉 閱讀全文

    2. Bevy 遊戲開發指南:深入探索 Rust 遊戲引擎生態系

    這是一個專為 Bevy 引擎設計的深度學習資源庫,Bevy 是目前 Rust 社群中最受矚目的資料驅動(ECS)遊戲引擎。該網站提供了從入門到進階的完整教學,涵蓋了渲染管線、音訊處理以及複雜的邏輯架構。對於想要跳脫 C++ 傳統框架,轉向更安全、高效的 Rust 語言進行遊戲開發的工程師來說,這是一份不容錯過的資源。

    👉 閱讀全文

    3. WireGuard 獲微軟數位簽署:Windows 版本正式釋出

    知名開源 VPN 協議 WireGuard 宣布了解決微軟驅動程式簽署問題後的最新 Windows 版本。這標誌著 WireGuard 在 Windows 平台上的穩定性與安全性邁進了一大步,使用者不再需要繞過系統限制來安裝高性能的加密網路隧道。這對於推廣開源安全協議至企業級桌面環境具有指標性意義。

    👉 閱讀全文

    💼 創業、商業與職涯發展

    1. 在 AWS 的二十年:永不謝幕的雲端使命

    作者回顧了他在 AWS 平台上運作服務長達 20 年的經驗,從 S3 的測試版一直到現在的複雜生態。文章深刻探討了「基礎設施即責任」的概念,強調維護一項服務二十年不僅僅是技術挑戰,更是一種對使用者的長期承諾。這對於處於快速更迭技術環境中的開發者來說,提供了一種關於「長青軟體」的珍貴視角。

    👉 閱讀全文

    2. 擅長電玩?美國航空管制局正邀請你加入

    美國聯邦航空局(FAA)發現,頂尖電玩玩家具備的高專注力、多工處理能力與快速決策力,與航空管制員所需的職能高度重合。這篇報導探討了現代職業技能的轉移,說明了休閒娛樂中的技能如何被轉化為高壓、高專業性職業的優勢。這也反映出數位原生代在傳統高難度產業中展現出的獨特人才價值。

    👉 閱讀全文

    🎨 開源專案與創意實驗

    1. 一維西洋棋:當複雜的博弈簡化到極致

    這是一個極具啟發性的遊戲實驗,將傳統二維空間的西洋棋簡化成一條直線上的對弈。開發者透過這個專案探討了遊戲設計的本質:當拿掉一個維度後,策略、步法與勝負邏輯會發生什麼變化?雖然畫面極簡,但其背後的邏輯與網頁實作技巧非常值得前端開發者觀摩。

    👉 閱讀全文

    2. Starfling:僅用單個 HTML 檔案打造的軌道彈射遊戲

    Starfling 是一款一鍵操作的無限軌道飛行遊戲,最令人驚嘆的是整個遊戲(含圖形與邏輯)完全包含在一個 HTML 檔案中。它展示了 Web 技術在極端限制下的表現力,開發者利用精簡的代碼實現了流暢的物理模擬與優美的視覺效果,是「極簡開發」愛好者的絕佳案例。

    👉 閱讀全文

    🌍 其他值得關注的話題

    1. 磨平我的 MacBook 稜角:一場對硬體設計的個人抗爭

    這篇引起廣泛討論的文章記錄了作者如何因為不滿 MacBook 鋒利的手托邊緣,而親自動手用銼刀將其磨平的過程。雖然這行為看似極端,但卻引發了大量使用者對於 Apple 產品「美學優先於人體工學」的深度反思。這是一篇關於使用者體驗(UX)在現實硬體中碰撞的精彩故事。

    👉 閱讀全文

    2. 阿提米絲 2 號安全濺落:人類重返月球的關鍵里程碑

    NASA 的阿提米絲 2 號任務(Artemis II)成功返回地球並安全濺落,這標誌著載人環繞月球任務的圓滿成功。這是自阿波羅時代以來,人類與月球最近距離接觸的一次飛躍。文章詳細記錄了重返大氣層的技術細節與未來的月球登陸計畫,象徵著太空探索新紀元的開啟。

    👉 閱讀全文

    3. 烏干達黑猩猩長達八年的「內戰」:靈長類行為的震撼觀察

    BBC 報導了科學家在烏干達觀察到的一場黑猩猩族群分裂與長年爭鬥。這場長達八年的「內戰」展現了非人類靈長類動物在政治結構、領土爭奪與暴力行為上的複雜性。這項研究不僅對生物學有重大意義,也讓讀者反思人類社會行為的演化根源。

    👉 閱讀全文

    💡 今日觀點:在變動中尋找恆常

    綜觀今日的熱門話題,我們可以看到兩個有趣的對照:

    • 長期主義與快速創新: AWS 的 20 年維護經驗告訴我們「守成」的價值;而 1D Chess 和單檔案遊戲則展示了「創新」的樂趣。
    • 硬體與人性: 無論是 NASA 的尖端太空船,還是使用者用銼刀磨平電腦邊角,都在說明技術最終必須服務於人類的體感與生存。

    行動建議: 作為開發者,本週不妨挑戰一下「限制下的開發」(例如嘗試單個檔案或簡化維度),並同時檢視自己手頭專案的長期維護性。另外,如果你是個遊戲高手,或許現在是考慮轉職航空管制員的好時機!✈️

  • AI 逆向工程協作:Source Inventory 協議讓一比一重建不再落東掉西

    重點摘要

    • AI 協作做逆向工程,最大的坑不是技術,而是 AI 「選擇性閱讀」原始碼
    • 根治方法:強制 Source Inventory 協議 — 動手前先列清單、附行號、用戶確認邏輯後才寫程式
    • 用戶不需要知道原版長什麼樣,清單上的每一項都要有原始碼行號出處
    • 這套協議已寫入 CLAUDE.md,適用於任何 legacy code 逆向重建專案

    最近在做一個系統重建專案:把一套已上線多年的 Ionic + PHP 系統,逐頁逐模組一比一重建為 Flutter 應用。聽起來直白,實際做起來卻踩了一個讓人很沮喪的坑——不是技術難度的問題,而是 AI 協作流程設計的問題

    這篇文章記錄我如何診斷這個問題,並建立一套可複用的 AI 逆向工程協作協議。

    問題:一比一複製,卻一直落東掉西

    我明確告訴 AI:「這是舊版原始碼,你幫我一比一複製到 Flutter。」但每次交付,都會發現缺少功能。最典型的例子:登入後的登出功能不見了

    登出不是什麼複雜功能,但它不在主流程頁面——它藏在 header component 的某個角落。AI 在讀原始碼的時候,只讀了主頁面,沒讀共用元件,自然就沒看到登出按鈕的存在。

    更麻煩的是:我自己也不知道原版長什麼樣(這是別人開發的系統),所以我也沒辦法直接指出「你少了這個」。每次都要在用起來的時候才發現缺少什麼,然後重來。

    根本原因:AI 的「選擇性閱讀」

    深入想了一下,問題的根源在於 AI 讀原始碼的方式:

    錯誤的讀法 正確的讀法
    找「我預期會有的東西」 逐行掃描,列出「所有看到的東西」
    只讀主要頁面檔案 讀所有相關檔案(service、routing、共用元件)
    憑印象補齊功能 每一項都有原始碼出處(檔案 + 行號)
    「讀完」就開始寫 產出清單、確認後才開始寫

    用一句話說:AI 是在「確認自己的預期」,不是在「完整盤點原始碼」。這種差距在簡單頁面感覺不出來,在有大量共用邏輯的真實系統裡,每個模組都會漏東西。

    解法:Source Inventory 協議

    我和 AI 一起設計了一套強制執行的協議,並寫入專案的 CLAUDE.md,讓每個接手這個專案的 AI session 都必須遵守。

    Step 0 — 列出所有要讀的檔案(動手前)

    在讀任何內容之前,先列出這個模組涉及的所有檔案,包含:

    • 頁面本體(template + logic)
    • 所有被引用的 service
    • 共用元件(header、footer、tab-bar、nav)
    • 路由設定
    • 後端 handler

    每個檔案讀完打勾,沒打勾的不算讀過。這一步強迫 AI 在開始之前就意識到「這個模組牽涉哪些檔案」,避免只讀主檔就動手。

    Step 1 — 產出 Source Inventory(每項附行號)

    讀完所有檔案後,產出功能清單。格式固定:

    - [ ] 登出按鈕
          來源:home.page.html:142 <ion-button (click)="logout()">
    
    - [ ] 下拉刷新
          來源:ticket.page.html:38 <ion-refresher (ionRefresh)="doRefresh($event)">
    
    - [ ] 空狀態提示
          來源:ticket.page.html:91 <div *ngIf="tickets.length === 0">目前沒有票券</div>

    關鍵點:每一項都必須有原始碼的檔案和行號

    這解決了「用戶不知道原版長什麼樣,所以沒辦法驗證清單正確性」的問題。用戶不需要看過原版,只需要問:「這一項你是從哪行讀到的?」答不出來,就代表是 AI 憑印象補的,不是真的在原始碼裡。

    Step 2 — 逐項實作,逐項打勾

    對照清單一項一項做,每完成一項就打勾。不允許「大概實作了」或「之後再補」。

    Step 3 — Self-Verify 後才交付

    交付前,AI 自己對照清單跑一遍,確認每項都存在於程式碼中。如果 Step 3 發現漏項,必須補完再說「完成」。漏項不能留給用戶發現。

    一條關鍵規則:禁止在原始碼存在的情況下向用戶索取截圖

    另一個常見的壞模式是:AI 讀不懂某個行為,就跟用戶要截圖或圖片。

    這在逆向工程的情境下尤其荒謬。用戶拿到舊系統原始碼,通常自己也沒有截圖,甚至從來沒用過那個系統。要求用戶提供截圖,是把 AI 本來應該做的工作(讀原始碼)轉嫁給用戶。

    正確的做法是:原始碼讀不懂的地方,標記 [?],說明不確定的點,讓用戶確認業務邏輯——而不是要求視覺確認。

    這套協議適用於任何逆向升級工程

    雖然這次的情境是 Ionic → Flutter,但這套協議的核心邏輯對任何 legacy 重建都適用:

    • jQuery / AngularJS → React / Vue
    • PHP monolith → 現代後端(Go、Node、Python)
    • 原生 Android/iOS → Flutter / React Native
    • 桌面應用 → Web 應用

    只要是「拿到舊系統 code,重寫到新技術棧」,用戶通常都不知道原版的全貌,也無法驗證 AI 是否讀完了所有東西。Source Inventory + 行號出處 是讓這個過程可稽核的最低門檻。

    如何在你的專案裡執行這套協議

    把以下內容加入你的專案 CLAUDE.md(或任何 AI 工作指引文件):

    ## 一比一複製協議(MANDATORY)
    
    > 原始碼是唯一的 ground truth。用戶不應該被要求提供截圖。
    
    ### Step 0 — 列出所有要讀的檔案
    (所有頁面、service、共用元件、routing、後端 handler)
    每個檔案讀完打勾,沒打勾不算讀過。
    
    ### Step 1 — Source Inventory(每項附檔案:行號)
    格式:
    - [ ] 功能描述
          來源:filename.html:LINE_NUMBER <原始碼片段>
    
    ### Step 2 — 逐項實作,逐項打勾
    
    ### Step 3 — Self-Verify 後才交付
    
    ### 禁止行為
    - 在原始碼存在的情況下向用戶索取截圖
    - 沒有產出 Source Inventory 就開始寫程式
    - 只讀主頁面,不讀 service / routing / 共用元件

    結語

    逆向升級工程的難點不在技術轉換,而在於如何保證「沒有遺漏」。AI 的天性是預測下一個 token,這讓它容易「補全期望的樣子」,而不是「如實記錄看到的東西」。

    Source Inventory 協議的作用,就是把 AI 的工作模式從「自由發揮」強制切換成「逐行比對」。這不是在限制 AI,而是在讓 AI 的輸出變得可以被信任。

    如果你也在做類似的逆向重建專案,歡迎把這套協議複製進你的 CLAUDE.md,看看效果如何。

  • Hacker News 每日精選 – 2026-04-10

    🚀 今日科技頭條:從硬體極限到 AI 透明度的深度進化

    今天的科技圈焦點集中在「穩定性」與「透明度」的雙重追求:從 NASA 如何打造能在外太空生存的容錯電腦,到確保 AI 不再胡說八道的引用工具。這些發展提醒我們,無論技術如何推陳出新,底層硬體的物理限制與高層數據的真實性,依然是工程師必須攻克的兩大核心堡壘。 🛠️

    🤖 AI / 機器學習

    Grainulator:讓 AI 沒證據就不敢開口

    這是一個針對大型語言模型(LLM)幻覺問題的解決方案。Grainulator 是一項開源工具,它強制要求 AI 在回答時必須提供明確的引用來源。如果 AI 無法在其參考的文件中找到對應的證據,該工具將阻止其生成內容。這對於需要高度準確性的商業應用與學術研究場景來說,是一個極具潛力的輔助組件。

    🔗 閱讀原文

    為什麼我依然偏好 MCP 而非內建 Skills

    作者 David 深入探討了 Anthropic 推出的 Model Context Protocol (MCP) 與傳統 AI 代理內建「技能(Skills)」的差異。他認為 MCP 提供了一種更具標準化且可擴展的方式來連結外部數據與工具,避免了被特定廠商鎖定的風險。這種解耦的架構,讓開發者能更靈活地讓不同模型共享相同的上下文獲取能力。

    🔗 閱讀原文

    🛠️ 開發工具與工程實踐

    NASA 如何打造 Artemis II 的容錯電腦

    在極端輻射與真空環境下,電腦錯誤可能導致致命後果。這篇文章詳解了 NASA 獵戶座太空船(Orion)的電腦架構,包含使用多重冗餘的 PowerPC 處理器與精密投票機制(Voting Mechanism)來確保指令正確。這是關鍵任務系統設計的教科書級案例,展示了硬體與軟體如何協作以達成近乎完美的可靠性。

    🔗 閱讀原文

    GitButler 募資 1700 萬美金:重新定義 Git 之後的未來

    由 Git 共同創辦人 Scott Chacon 領軍的新創公司 GitButler 成功完成 A 輪融資。他們旨在解決現有 Git 工作流中「分支切換」與「多任務並行」的痛點。GitButler 提供了一個更直觀的管理層,讓開發者能同時處理多個功能分支而不混淆,挑戰傳統 Git CLI 的笨重操作體驗。

    🔗 閱讀原文

    機械同理心(Mechanical Sympathy)的原則

    Martin Fowler 網站分享了關於「機械同理心」的文章,強調開發者應該理解底層硬體的運作方式(如快取、內存排序)來撰寫軟體。文章指出,當軟體與硬體的工作方式一致時,效能才能達到最大化。這是一篇探討軟體抽象與物理硬體界限的經典技術論述。

    🔗 閱讀原文

    macOS 原生瞬間空間切換

    這篇部落格文章揭示了一個讓 macOS 用戶興奮的技巧:如何繞過系統動畫,達成在不同桌面空間(Spaces)之間「瞬間」切換。對於極度追求效率的開發者來說,macOS 內建的切換動畫往往顯得過於緩慢,這項原生優化能大幅提升多視窗切換的流暢感。

    🔗 閱讀原文

    🌐 開源專案與硬體研究

    Charcuterie:Unicode 視覺相似度探索器

    這是一個極具價值的安全研究工具,專門用來尋找視覺上相似但編碼不同的 Unicode 字元。這類字元常被用於網路釣魚攻擊(例如用相似的希臘字母替換網址中的拉丁字母)。該專案能幫助資安專家與開發者識別潛在的混淆攻擊路徑。

    🔗 閱讀原文

    RAM 的 1966 年設計缺陷及其繞過方法

    在一則精彩的技術影片中,作者深入探討了動態隨機存取記憶體(DRAM)自 1966 年以來的設計架構限制,特別是關於刷新週期(Refresh Cycle)導致的延遲。作者展示了如何透過創新的硬體控制手段來「繞過」這些限制,雖然這更像是一種極限實驗,但也為記憶體架構的進化提供了思考。

    🔗 閱讀原文

    🎨 其他:藝術與文化

    生成藝術的歲月流變

    這篇文章回顧了從 1960 年代至今的生成藝術(Generative Art)發展史。它不僅討論了技術上的演進(如繪圖機到現在的 AI 生成),更探討了「算法作為藝術家」這一概念在美學上的爭議與突破,是了解數位藝術歷史的絕佳指南。

    🔗 閱讀原文

    Hip-hop 先驅 Afrika Bambaataa 逝世

    嘻哈音樂的重量級人物 Afrika Bambaataa 辭世,享年 67 歲。他被譽為「嘻哈教父」之一,透過《Planet Rock》等作品將電音與饒舌融合,並創立了 Universal Zulu Nation。他的離去象徵著流行音樂史上一個時代的終結。

    🔗 閱讀原文

    💡 今日觀點:掌握「底層知識」是未來唯一的競爭力

    觀看今日的熱門文章,我們可以發現一個清晰的主旋律:去黑盒化(De-blackboxing)

    • 在 AI 領域,我們不再滿足於黑盒子的回答,開始要求精確引用
    • 在開發領域,我們重新審視 Git 的本質RAM 的硬體限制
    • 在系統設計上,NASA 的案例告訴我們容錯源於對每一個物理組件的深度掌控。

    行動建議:本週不妨花一點時間深入研究你最常使用的工具底層架構。無論是 Git 的內部實現,還是作業系統的記憶體管理,當 AI 能幫我們寫程式時,理解「底層是如何運作的」將成為區分優秀工程師與平庸者的關鍵。