<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Routing on Tarragon</title><link>https://tarrragon.github.io/blog/tags/routing/</link><description>Recent content in Routing on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 19 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/routing/index.xml" rel="self" type="application/rss+xml"/><item><title>Flutter GoRouter 導航設計</title><link>https://tarrragon.github.io/blog/ux-design/05-navigation-patterns/flutter-gorouter/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/05-navigation-patterns/flutter-gorouter/</guid><description>&lt;p>GoRouter 是 Flutter 官方推薦的 declarative router。路由定義集中在一個 &lt;code>GoRouter&lt;/code> 物件中，導航操作用 URL path 表達（&lt;code>context.go('/terminal')&lt;/code>），支援 deep link、redirect、和巢狀路由。&lt;/p>
&lt;h2 id="路由定義">路由定義&lt;/h2>
&lt;p>GoRouter 的路由定義是一棵樹，每個節點是一個 &lt;code>GoRoute&lt;/code>，指定 path 和 builder。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">GoRouter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nl">routes:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="n">GoRoute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nl">path:&lt;/span> &lt;span class="s1">&amp;#39;/&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nl">builder:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">HomeScreen&lt;/span>&lt;span class="p">()),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="n">GoRoute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nl">path:&lt;/span> &lt;span class="s1">&amp;#39;/enrollment&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nl">builder:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">EnrollmentScreen&lt;/span>&lt;span class="p">()),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="n">GoRoute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nl">path:&lt;/span> &lt;span class="s1">&amp;#39;/terminal&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nl">builder:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">TerminalScreen&lt;/span>&lt;span class="p">()),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">]);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>路由定義是 app 所有可到達畫面的完整清單。新增畫面時先在路由定義中加入 path，再實作 builder。路由定義同時也是路由可達性檢查的 source of truth（&lt;a href="https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/route-reachability/" data-link-title="路由可達性檢查" data-link-desc="Router 定義的路由 vs UI 實際可達的路由 — 路由存在但 UI 不可達等於死程式碼的 UX 版本">路由可達性&lt;/a>）。&lt;/p>
&lt;h2 id="導航-api">導航 API&lt;/h2>
&lt;p>GoRouter 提供三個主要的導航方法，語意不同，適用場景不同。&lt;/p>
&lt;h3 id="contextgopath">context.go(path)&lt;/h3>
&lt;p>替換整個導航堆疊。&lt;code>go('/terminal')&lt;/code> 讓使用者直接到 terminal 畫面，按 back 不會回到前一個畫面（堆疊已被替換）。&lt;/p>
&lt;p>適合場景：切換主要工作區。從登入畫面到首頁（登入成功後使用者不應該按 back 回到登入畫面）。&lt;/p>
&lt;h3 id="contextpushpath">context.push(path)&lt;/h3>
&lt;p>把新畫面推入導航堆疊。&lt;code>push('/enrollment')&lt;/code> 讓使用者到 enrollment 畫面，按 back 回到前一個畫面。&lt;/p>
&lt;p>適合場景：暫時離開做一件事，做完回來。從首頁到配對畫面，配對完成後按 back 回首頁。&lt;/p>
&lt;h3 id="contextpushreplacementpath">context.pushReplacement(path)&lt;/h3>
&lt;p>替換堆疊頂端的畫面。不改變堆疊深度 — 前一個畫面被新畫面取代，按 back 回到更早的畫面。&lt;/p>
&lt;p>適合場景：步驟式流程中的前進。步驟 1 → pushReplacement 步驟 2 → pushReplacement 步驟 3。使用者在步驟 3 按 back 不會回到步驟 2（已被替換），而是回到流程開始前的畫面。&lt;/p>
&lt;h2 id="redirect-機制">Redirect 機制&lt;/h2>
&lt;p>GoRouter 的 redirect 在每次導航前執行，可以根據 app 狀態（登入狀態、權限）把使用者導向不同畫面。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">GoRouter&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nl">redirect:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="kd">final&lt;/span> &lt;span class="n">isLoggedIn&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">authState&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">isLoggedIn&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="o">!&lt;/span>&lt;span class="n">isLoggedIn&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">matchedLocation&lt;/span> &lt;span class="o">!=&lt;/span> &lt;span class="s1">&amp;#39;/login&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="s1">&amp;#39;/login&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">isLoggedIn&lt;/span> &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">matchedLocation&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s1">&amp;#39;/login&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="s1">&amp;#39;/&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="kc">null&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 不 redirect
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> &lt;span class="nl">routes:&lt;/span> &lt;span class="p">[...],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Redirect 集中管理「什麼條件下使用者不能到某個畫面」的邏輯。比在每個畫面的 &lt;code>initState&lt;/code> 中各自檢查更容易維護和測試。&lt;/p>
&lt;h2 id="shellroute巢狀導航">ShellRoute（巢狀導航）&lt;/h2>
&lt;p>ShellRoute 讓多個畫面共享同一個外殼（tab bar、bottom navigation、drawer）。子路由的導航在 shell 內發生，shell 本身不變。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="n">ShellRoute&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nl">builder:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">state&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">child&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">=&amp;gt;&lt;/span> &lt;span class="n">ScaffoldWithNavBar&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nl">child:&lt;/span> &lt;span class="n">child&lt;/span>&lt;span class="p">),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nl">routes:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="n">GoRoute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nl">path:&lt;/span> &lt;span class="s1">&amp;#39;/home&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nl">builder:&lt;/span> &lt;span class="p">...),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="n">GoRoute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nl">path:&lt;/span> &lt;span class="s1">&amp;#39;/search&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nl">builder:&lt;/span> &lt;span class="p">...),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="n">GoRoute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nl">path:&lt;/span> &lt;span class="s1">&amp;#39;/profile&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nl">builder:&lt;/span> &lt;span class="p">...),&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="p">)&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ShellRoute 適合 tab bar 導航模式 — 底部的 tab bar 是 shell，每個 tab 的內容是子路由。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>go / push / pushReplacement 的 UX 語意 → &lt;a href="https://tarrragon.github.io/blog/ux-design/05-navigation-patterns/go-push-semantics/" data-link-title="go vs push vs pushReplacement 的 UX 語意表" data-link-desc="三種導航方法對堆疊、back 行為、使用者心理模型的影響 — 選擇依據是使用者的意圖而非技術方便">go vs push vs pushReplacement 語意表&lt;/a>&lt;/li>
&lt;li>iOS 和 Android 的導航差異 → &lt;a href="https://tarrragon.github.io/blog/ux-design/05-navigation-patterns/ios-vs-material-navigation/" data-link-title="iOS HIG vs Material Design 導航差異" data-link-desc="兩個平台在 back 行為、手勢、tab bar 位置、modal 呈現上的差異 — 跨平台 app 需要決定遵循哪套慣例">iOS HIG vs Material Design 導航差異&lt;/a>&lt;/li>
&lt;li>Deep link 設計 → &lt;a href="https://tarrragon.github.io/blog/ux-design/05-navigation-patterns/deep-link-design/" data-link-title="Deep link 設計" data-link-desc="URL scheme / Universal Link / App Link — deep link 讓外部來源直接導航到 app 的特定畫面">Deep link 設計&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>GoRouter 是 Flutter 官方推薦的 declarative router。路由定義集中在一個 <code>GoRouter</code> 物件中，導航操作用 URL path 表達（<code>context.go('/terminal')</code>），支援 deep link、redirect、和巢狀路由。</p>
<h2 id="路由定義">路由定義</h2>
<p>GoRouter 的路由定義是一棵樹，每個節點是一個 <code>GoRoute</code>，指定 path 和 builder。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">GoRouter</span><span class="p">(</span><span class="nl">routes:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="n">GoRoute</span><span class="p">(</span><span class="nl">path:</span> <span class="s1">&#39;/&#39;</span><span class="p">,</span> <span class="nl">builder:</span> <span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">state</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">HomeScreen</span><span class="p">()),</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="n">GoRoute</span><span class="p">(</span><span class="nl">path:</span> <span class="s1">&#39;/enrollment&#39;</span><span class="p">,</span> <span class="nl">builder:</span> <span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">state</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">EnrollmentScreen</span><span class="p">()),</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="n">GoRoute</span><span class="p">(</span><span class="nl">path:</span> <span class="s1">&#39;/terminal&#39;</span><span class="p">,</span> <span class="nl">builder:</span> <span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">state</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">TerminalScreen</span><span class="p">()),</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">]);</span></span></span></code></pre></div><p>路由定義是 app 所有可到達畫面的完整清單。新增畫面時先在路由定義中加入 path，再實作 builder。路由定義同時也是路由可達性檢查的 source of truth（<a href="/blog/ux-design/01-screen-state-machine/route-reachability/" data-link-title="路由可達性檢查" data-link-desc="Router 定義的路由 vs UI 實際可達的路由 — 路由存在但 UI 不可達等於死程式碼的 UX 版本">路由可達性</a>）。</p>
<h2 id="導航-api">導航 API</h2>
<p>GoRouter 提供三個主要的導航方法，語意不同，適用場景不同。</p>
<h3 id="contextgopath">context.go(path)</h3>
<p>替換整個導航堆疊。<code>go('/terminal')</code> 讓使用者直接到 terminal 畫面，按 back 不會回到前一個畫面（堆疊已被替換）。</p>
<p>適合場景：切換主要工作區。從登入畫面到首頁（登入成功後使用者不應該按 back 回到登入畫面）。</p>
<h3 id="contextpushpath">context.push(path)</h3>
<p>把新畫面推入導航堆疊。<code>push('/enrollment')</code> 讓使用者到 enrollment 畫面，按 back 回到前一個畫面。</p>
<p>適合場景：暫時離開做一件事，做完回來。從首頁到配對畫面，配對完成後按 back 回首頁。</p>
<h3 id="contextpushreplacementpath">context.pushReplacement(path)</h3>
<p>替換堆疊頂端的畫面。不改變堆疊深度 — 前一個畫面被新畫面取代，按 back 回到更早的畫面。</p>
<p>適合場景：步驟式流程中的前進。步驟 1 → pushReplacement 步驟 2 → pushReplacement 步驟 3。使用者在步驟 3 按 back 不會回到步驟 2（已被替換），而是回到流程開始前的畫面。</p>
<h2 id="redirect-機制">Redirect 機制</h2>
<p>GoRouter 的 redirect 在每次導航前執行，可以根據 app 狀態（登入狀態、權限）把使用者導向不同畫面。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">GoRouter</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nl">redirect:</span> <span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">state</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="kd">final</span> <span class="n">isLoggedIn</span> <span class="o">=</span> <span class="n">authState</span><span class="p">.</span><span class="n">isLoggedIn</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">isLoggedIn</span> <span class="o">&amp;&amp;</span> <span class="n">state</span><span class="p">.</span><span class="n">matchedLocation</span> <span class="o">!=</span> <span class="s1">&#39;/login&#39;</span><span class="p">)</span> <span class="k">return</span> <span class="s1">&#39;/login&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">isLoggedIn</span> <span class="o">&amp;&amp;</span> <span class="n">state</span><span class="p">.</span><span class="n">matchedLocation</span> <span class="o">==</span> <span class="s1">&#39;/login&#39;</span><span class="p">)</span> <span class="k">return</span> <span class="s1">&#39;/&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">return</span> <span class="kc">null</span><span class="p">;</span> <span class="c1">// 不 redirect
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"></span>  <span class="p">},</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">  <span class="nl">routes:</span> <span class="p">[...],</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">);</span></span></span></code></pre></div><p>Redirect 集中管理「什麼條件下使用者不能到某個畫面」的邏輯。比在每個畫面的 <code>initState</code> 中各自檢查更容易維護和測試。</p>
<h2 id="shellroute巢狀導航">ShellRoute（巢狀導航）</h2>
<p>ShellRoute 讓多個畫面共享同一個外殼（tab bar、bottom navigation、drawer）。子路由的導航在 shell 內發生，shell 本身不變。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">ShellRoute</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nl">builder:</span> <span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">state</span><span class="p">,</span> <span class="n">child</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="n">ScaffoldWithNavBar</span><span class="p">(</span><span class="nl">child:</span> <span class="n">child</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nl">routes:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">GoRoute</span><span class="p">(</span><span class="nl">path:</span> <span class="s1">&#39;/home&#39;</span><span class="p">,</span> <span class="nl">builder:</span> <span class="p">...),</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="n">GoRoute</span><span class="p">(</span><span class="nl">path:</span> <span class="s1">&#39;/search&#39;</span><span class="p">,</span> <span class="nl">builder:</span> <span class="p">...),</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="n">GoRoute</span><span class="p">(</span><span class="nl">path:</span> <span class="s1">&#39;/profile&#39;</span><span class="p">,</span> <span class="nl">builder:</span> <span class="p">...),</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">  <span class="p">],</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">)</span></span></span></code></pre></div><p>ShellRoute 適合 tab bar 導航模式 — 底部的 tab bar 是 shell，每個 tab 的內容是子路由。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>go / push / pushReplacement 的 UX 語意 → <a href="/blog/ux-design/05-navigation-patterns/go-push-semantics/" data-link-title="go vs push vs pushReplacement 的 UX 語意表" data-link-desc="三種導航方法對堆疊、back 行為、使用者心理模型的影響 — 選擇依據是使用者的意圖而非技術方便">go vs push vs pushReplacement 語意表</a></li>
<li>iOS 和 Android 的導航差異 → <a href="/blog/ux-design/05-navigation-patterns/ios-vs-material-navigation/" data-link-title="iOS HIG vs Material Design 導航差異" data-link-desc="兩個平台在 back 行為、手勢、tab bar 位置、modal 呈現上的差異 — 跨平台 app 需要決定遵循哪套慣例">iOS HIG vs Material Design 導航差異</a></li>
<li>Deep link 設計 → <a href="/blog/ux-design/05-navigation-patterns/deep-link-design/" data-link-title="Deep link 設計" data-link-desc="URL scheme / Universal Link / App Link — deep link 讓外部來源直接導航到 app 的特定畫面">Deep link 設計</a></li>
</ul>
]]></content:encoded></item><item><title>導航路徑 test</title><link>https://tarrragon.github.io/blog/testing/04-ui-automation/navigation-path-test/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/04-ui-automation/navigation-path-test/</guid><description>&lt;p>導航路徑 test 驗證的是使用者在畫面之間的移動是否符合設計 — 每個畫面的 back 按鈕是否導向正確的上層畫面、每個 router 定義的路由是否從 UI 可達、&lt;code>go&lt;/code> 和 &lt;code>push&lt;/code> 的語意是否產生正確的返回堆疊。&lt;/p>
&lt;h2 id="back-按鈕-test">Back 按鈕 test&lt;/h2>
&lt;p>每個有 back 按鈕的畫面需要一個 test 驗證「按下 back 後導航到哪裡」。Back 按鈕的目標畫面依導航方式而定：&lt;/p>
&lt;ul>
&lt;li>&lt;code>context.push('/terminal')&lt;/code> 進入 → back 回到推入前的畫面（首頁）&lt;/li>
&lt;li>&lt;code>context.go('/terminal')&lt;/code> 進入 → back 行為依 router 設定，可能沒有上一頁&lt;/li>
&lt;/ul>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="n">testWidgets&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;back from terminal returns to home (pushed)&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">tester&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kd">async&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="kd">await&lt;/span> &lt;span class="n">tester&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">pumpWidget&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">app&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 從首頁 push 到 terminal
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">await&lt;/span> &lt;span class="n">tester&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">tap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">find&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Connect Terminal&amp;#39;&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="kd">await&lt;/span> &lt;span class="n">tester&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">pumpAndSettle&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="n">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">find&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">byType&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">TerminalScreen&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">findsOneWidget&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 按 back
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">await&lt;/span> &lt;span class="n">tester&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">tap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">find&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">byKey&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Key&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;back_button&amp;#39;&lt;/span>&lt;span class="p">)));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="kd">await&lt;/span> &lt;span class="n">tester&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">pumpAndSettle&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="n">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">find&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">byType&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">HomeScreen&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">findsOneWidget&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="route-可達性-test">Route 可達性 test&lt;/h2>
&lt;p>Router 定義的每個路由都應該有從 UI 可達的路徑（&lt;a href="https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/route-reachability/" data-link-title="路由可達性檢查" data-link-desc="Router 定義的路由 vs UI 實際可達的路由 — 路由存在但 UI 不可達等於死程式碼的 UX 版本">ux-design 模組一 路由可達性&lt;/a>）。Route 可達性 test 驗證「從首頁出發，透過 UI 操作能到達每個路由」。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="n">testWidgets&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;enrollment route is reachable from home&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">tester&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kd">async&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="kd">await&lt;/span> &lt;span class="n">tester&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">pumpWidget&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">app&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 找到配對入口按鈕
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">final&lt;/span> &lt;span class="n">enrollButton&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">find&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Enroll Device&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="n">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">enrollButton&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">findsOneWidget&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 點擊後到達 enrollment 畫面
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">await&lt;/span> &lt;span class="n">tester&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">tap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">enrollButton&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="kd">await&lt;/span> &lt;span class="n">tester&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">pumpAndSettle&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="n">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">find&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">byType&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">EnrollmentScreen&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">findsOneWidget&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不可達的路由在 test 中表現為「找不到導航到該路由的 UI 元素」。如果 router 定義了 &lt;code>/enrollment&lt;/code> 但首頁沒有對應按鈕，&lt;code>find.text('Enroll Device')&lt;/code> 會找不到元素 — test 失敗暴露入口缺失。&lt;/p>
&lt;h2 id="go-vs-push-語意的-test">&lt;code>go&lt;/code> vs &lt;code>push&lt;/code> 語意的 test&lt;/h2>
&lt;p>&lt;code>go&lt;/code> 和 &lt;code>push&lt;/code> 對返回堆疊的影響不同（&lt;a href="https://tarrragon.github.io/blog/ux-design/05-navigation-patterns/" data-link-title="模組五：導航模式" data-link-desc="Push/pop stack、GoRouter 命名路由、tab bar、drawer — 導航方法選擇是設計決策">ux-design 模組五 導航模式&lt;/a>）。Test 需要驗證正確的導航方式被使用：&lt;/p>
&lt;h3 id="push-語意保留返回堆疊">Push 語意：保留返回堆疊&lt;/h3>
&lt;p>Push 後按系統 back 鍵應該回到推入前的畫面。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="n">testWidgets&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;push preserves back stack&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="n">tester&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="kd">async&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> &lt;span class="kd">await&lt;/span> &lt;span class="n">tester&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">pumpWidget&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">app&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="c1">// push to enrollment
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">await&lt;/span> &lt;span class="n">tester&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">tap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">find&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Enroll Device&amp;#39;&lt;/span>&lt;span class="p">));&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="kd">await&lt;/span> &lt;span class="n">tester&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">pumpAndSettle&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 系統 back 鍵
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">final&lt;/span> &lt;span class="n">backButton&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">find&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">byTooltip&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Back&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="kd">await&lt;/span> &lt;span class="n">tester&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">tap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">backButton&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="kd">await&lt;/span> &lt;span class="n">tester&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">pumpAndSettle&lt;/span>&lt;span class="p">();&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="c1">// 應該回到首頁
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="n">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">find&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">byType&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">HomeScreen&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="n">findsOneWidget&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="go-語意替換路由堆疊">Go 語意：替換路由堆疊&lt;/h3>
&lt;p>Go 後按系統 back 鍵的行為依 router 設定。如果 go 到的路由是根層級，系統 back 鍵可能退出 app 而非回到前一個畫面。&lt;/p></description><content:encoded><![CDATA[<p>導航路徑 test 驗證的是使用者在畫面之間的移動是否符合設計 — 每個畫面的 back 按鈕是否導向正確的上層畫面、每個 router 定義的路由是否從 UI 可達、<code>go</code> 和 <code>push</code> 的語意是否產生正確的返回堆疊。</p>
<h2 id="back-按鈕-test">Back 按鈕 test</h2>
<p>每個有 back 按鈕的畫面需要一個 test 驗證「按下 back 後導航到哪裡」。Back 按鈕的目標畫面依導航方式而定：</p>
<ul>
<li><code>context.push('/terminal')</code> 進入 → back 回到推入前的畫面（首頁）</li>
<li><code>context.go('/terminal')</code> 進入 → back 行為依 router 設定，可能沒有上一頁</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">testWidgets</span><span class="p">(</span><span class="s1">&#39;back from terminal returns to home (pushed)&#39;</span><span class="p">,</span> <span class="p">(</span><span class="n">tester</span><span class="p">)</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">await</span> <span class="n">tester</span><span class="p">.</span><span class="n">pumpWidget</span><span class="p">(</span><span class="n">app</span><span class="p">());</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="c1">// 從首頁 push 到 terminal
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span>  <span class="kd">await</span> <span class="n">tester</span><span class="p">.</span><span class="n">tap</span><span class="p">(</span><span class="n">find</span><span class="p">.</span><span class="n">text</span><span class="p">(</span><span class="s1">&#39;Connect Terminal&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="kd">await</span> <span class="n">tester</span><span class="p">.</span><span class="n">pumpAndSettle</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="n">expect</span><span class="p">(</span><span class="n">find</span><span class="p">.</span><span class="n">byType</span><span class="p">(</span><span class="n">TerminalScreen</span><span class="p">),</span> <span class="n">findsOneWidget</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="c1">// 按 back
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>  <span class="kd">await</span> <span class="n">tester</span><span class="p">.</span><span class="n">tap</span><span class="p">(</span><span class="n">find</span><span class="p">.</span><span class="n">byKey</span><span class="p">(</span><span class="n">Key</span><span class="p">(</span><span class="s1">&#39;back_button&#39;</span><span class="p">)));</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="kd">await</span> <span class="n">tester</span><span class="p">.</span><span class="n">pumpAndSettle</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="n">expect</span><span class="p">(</span><span class="n">find</span><span class="p">.</span><span class="n">byType</span><span class="p">(</span><span class="n">HomeScreen</span><span class="p">),</span> <span class="n">findsOneWidget</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><h2 id="route-可達性-test">Route 可達性 test</h2>
<p>Router 定義的每個路由都應該有從 UI 可達的路徑（<a href="/blog/ux-design/01-screen-state-machine/route-reachability/" data-link-title="路由可達性檢查" data-link-desc="Router 定義的路由 vs UI 實際可達的路由 — 路由存在但 UI 不可達等於死程式碼的 UX 版本">ux-design 模組一 路由可達性</a>）。Route 可達性 test 驗證「從首頁出發，透過 UI 操作能到達每個路由」。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">testWidgets</span><span class="p">(</span><span class="s1">&#39;enrollment route is reachable from home&#39;</span><span class="p">,</span> <span class="p">(</span><span class="n">tester</span><span class="p">)</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">await</span> <span class="n">tester</span><span class="p">.</span><span class="n">pumpWidget</span><span class="p">(</span><span class="n">app</span><span class="p">());</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="c1">// 找到配對入口按鈕
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span>  <span class="kd">final</span> <span class="n">enrollButton</span> <span class="o">=</span> <span class="n">find</span><span class="p">.</span><span class="n">text</span><span class="p">(</span><span class="s1">&#39;Enroll Device&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="n">expect</span><span class="p">(</span><span class="n">enrollButton</span><span class="p">,</span> <span class="n">findsOneWidget</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="c1">// 點擊後到達 enrollment 畫面
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>  <span class="kd">await</span> <span class="n">tester</span><span class="p">.</span><span class="n">tap</span><span class="p">(</span><span class="n">enrollButton</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="kd">await</span> <span class="n">tester</span><span class="p">.</span><span class="n">pumpAndSettle</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="n">expect</span><span class="p">(</span><span class="n">find</span><span class="p">.</span><span class="n">byType</span><span class="p">(</span><span class="n">EnrollmentScreen</span><span class="p">),</span> <span class="n">findsOneWidget</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>不可達的路由在 test 中表現為「找不到導航到該路由的 UI 元素」。如果 router 定義了 <code>/enrollment</code> 但首頁沒有對應按鈕，<code>find.text('Enroll Device')</code> 會找不到元素 — test 失敗暴露入口缺失。</p>
<h2 id="go-vs-push-語意的-test"><code>go</code> vs <code>push</code> 語意的 test</h2>
<p><code>go</code> 和 <code>push</code> 對返回堆疊的影響不同（<a href="/blog/ux-design/05-navigation-patterns/" data-link-title="模組五：導航模式" data-link-desc="Push/pop stack、GoRouter 命名路由、tab bar、drawer — 導航方法選擇是設計決策">ux-design 模組五 導航模式</a>）。Test 需要驗證正確的導航方式被使用：</p>
<h3 id="push-語意保留返回堆疊">Push 語意：保留返回堆疊</h3>
<p>Push 後按系統 back 鍵應該回到推入前的畫面。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">testWidgets</span><span class="p">(</span><span class="s1">&#39;push preserves back stack&#39;</span><span class="p">,</span> <span class="p">(</span><span class="n">tester</span><span class="p">)</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="kd">await</span> <span class="n">tester</span><span class="p">.</span><span class="n">pumpWidget</span><span class="p">(</span><span class="n">app</span><span class="p">());</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="c1">// push to enrollment
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span>  <span class="kd">await</span> <span class="n">tester</span><span class="p">.</span><span class="n">tap</span><span class="p">(</span><span class="n">find</span><span class="p">.</span><span class="n">text</span><span class="p">(</span><span class="s1">&#39;Enroll Device&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="kd">await</span> <span class="n">tester</span><span class="p">.</span><span class="n">pumpAndSettle</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="c1">// 系統 back 鍵
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>  <span class="kd">final</span> <span class="n">backButton</span> <span class="o">=</span> <span class="n">find</span><span class="p">.</span><span class="n">byTooltip</span><span class="p">(</span><span class="s1">&#39;Back&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="kd">await</span> <span class="n">tester</span><span class="p">.</span><span class="n">tap</span><span class="p">(</span><span class="n">backButton</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="kd">await</span> <span class="n">tester</span><span class="p">.</span><span class="n">pumpAndSettle</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="c1">// 應該回到首頁
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span>  <span class="n">expect</span><span class="p">(</span><span class="n">find</span><span class="p">.</span><span class="n">byType</span><span class="p">(</span><span class="n">HomeScreen</span><span class="p">),</span> <span class="n">findsOneWidget</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><h3 id="go-語意替換路由堆疊">Go 語意：替換路由堆疊</h3>
<p>Go 後按系統 back 鍵的行為依 router 設定。如果 go 到的路由是根層級，系統 back 鍵可能退出 app 而非回到前一個畫面。</p>
<p>Test 策略：驗證 go 後的路由堆疊狀態。如果設計意圖是「切換工作區，不保留前一個畫面」，斷言系統 back 鍵不回到前一個畫面。</p>
<h2 id="深層連結-test">深層連結 test</h2>
<p>深層連結（deep link）讓使用者從 app 外部直接進入特定畫面。Deep link test 驗證「直接導航到內部路由時，畫面和導航堆疊是否正確」。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">testWidgets</span><span class="p">(</span><span class="s1">&#39;deep link to /terminal shows terminal&#39;</span><span class="p">,</span> <span class="p">(</span><span class="n">tester</span><span class="p">)</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">await</span> <span class="n">tester</span><span class="p">.</span><span class="n">pumpWidget</span><span class="p">(</span><span class="n">app</span><span class="p">(</span><span class="nl">initialRoute:</span> <span class="s1">&#39;/terminal&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="n">expect</span><span class="p">(</span><span class="n">find</span><span class="p">.</span><span class="n">byType</span><span class="p">(</span><span class="n">TerminalScreen</span><span class="p">),</span> <span class="n">findsOneWidget</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>深層連結的特殊性在於使用者跳過了正常的導航流程。從首頁到 terminal 的正常流程可能經過認證 gate，但深層連結直接到 terminal — 認證 gate 是否仍然生效需要額外的 test。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>狀態覆蓋策略 → <a href="/blog/testing/04-ui-automation/state-coverage-strategy/" data-link-title="Widget test 的狀態覆蓋策略" data-link-desc="從畫面狀態矩陣推導 widget test case — 每個狀態的顯示、操作、退出路徑都是獨立的斷言目標">Widget test 的狀態覆蓋策略</a></li>
<li>Playwright 驗證流程 → <a href="/blog/testing/04-ui-automation/playwright-verification/" data-link-title="Playwright 瀏覽器驗證流程" data-link-desc="用 Playwright 驗證 web 版本的 UI 行為 — test 結構、selector 策略、和 widget test 的互補關係">Playwright 瀏覽器驗證流程</a></li>
<li>路由可達性的設計原則 → <a href="/blog/ux-design/01-screen-state-machine/route-reachability/" data-link-title="路由可達性檢查" data-link-desc="Router 定義的路由 vs UI 實際可達的路由 — 路由存在但 UI 不可達等於死程式碼的 UX 版本">ux-design 模組一 路由可達性</a></li>
</ul>
]]></content:encoded></item><item><title>路由可達性檢查</title><link>https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/route-reachability/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/route-reachability/</guid><description>&lt;p>路由可達性檢查比較兩個集合：router 定義的所有路由，和使用者從 UI 操作能到達的所有路由。兩個集合的差集就是問題所在 — 定義了但不可達的路由是入口缺失，可達但未定義的路由是 404 風險。&lt;/p>
&lt;h2 id="定義-vs-可達">定義 vs 可達&lt;/h2>
&lt;h3 id="router-定義的路由">Router 定義的路由&lt;/h3>
&lt;p>現代前端框架（Flutter GoRouter、React Router、Vue Router）通常有一個集中的路由定義檔，列出所有可存取的路徑和對應的畫面元件。這個列表是 router 認知的「所有畫面」。&lt;/p>
&lt;h3 id="ui-可達的路由">UI 可達的路由&lt;/h3>
&lt;p>從首頁（或 app 的入口畫面）開始，透過 UI 上的按鈕、連結、手勢能到達的所有路由。這個集合代表使用者實際能存取的畫面。&lt;/p>
&lt;h3 id="差集分析">差集分析&lt;/h3>
&lt;p>&lt;strong>router 有但 UI 不可達&lt;/strong>：路由定義了、畫面元件也實作了，但沒有任何 UI 元素導航到這個路由。功能存在但使用者找不到入口。&lt;/p>
&lt;p>&lt;strong>UI 指向但 router 沒有&lt;/strong>：UI 上有一個按鈕 &lt;code>navigateTo('/settings')&lt;/code>，但 router 沒有定義 &lt;code>/settings&lt;/code> 路由。使用者點擊後會看到 404 或空白畫面。&lt;/p>
&lt;h2 id="路由存在但不可達的案例">路由存在但不可達的案例&lt;/h2>
&lt;p>app_tunnel 的 router 定義了三條路由：&lt;code>/&lt;/code>（首頁）、&lt;code>/enrollment&lt;/code>（配對）、&lt;code>/terminal&lt;/code>（終端機）。首頁只有一個 Connect Terminal 按鈕導航到 &lt;code>/terminal&lt;/code>。&lt;code>/enrollment&lt;/code> 路由存在，&lt;code>EnrollmentScreen&lt;/code> 完整實作，但首頁沒有任何 UI 元素導航到這個路由（&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/missing-enrollment-entry-point/" data-link-title="U.C4 首頁缺配對入口按鈕、導航流未完整列出" data-link-desc="Flutter app 首頁只有 Connect Terminal 按鈕、沒有 Enroll Device 入口 — 使用者首次使用時找不到配對功能。根因是導航流設計只考慮了日常操作（UC-02 連線）、遺漏了首次操作（UC-01 配對）的入口">U.C4&lt;/a>）。&lt;/p>
&lt;p>從使用者視角看，配對功能不存在。從開發者視角看，配對功能完整 — 路由定義了、畫面寫好了、業務邏輯都通了。問題出在「入口」這個連接層。&lt;/p>
&lt;p>這和程式碼裡寫了一個 function 但沒有任何地方呼叫它的情況結構相同。Function 本身可能正確無誤，但從系統角度看是死程式碼。路由可達性檢查是這個問題在 UX 層的對應。&lt;/p>
&lt;h2 id="檢查方法">檢查方法&lt;/h2>
&lt;h3 id="手動檢查">手動檢查&lt;/h3>
&lt;p>列出 router 定義的所有路由，然後逐一在 UI 上找到通往該路由的操作路徑。找不到路徑的就是不可達路由。&lt;/p>
&lt;p>手動檢查的成本隨畫面數量線性增長。5 個路由的 app 很快能查完；50 個路由的 app 需要系統化方法。&lt;/p>
&lt;h3 id="從操作盤點交叉比對">從操作盤點交叉比對&lt;/h3>
&lt;p>BDD 操作盤點列出了所有使用者操作（UC）。每個 UC 對應至少一個畫面。把 UC 清單和 router 定義對照：&lt;/p>
&lt;ul>
&lt;li>每個 UC 的主要入口畫面是否有從首頁可達的路徑？&lt;/li>
&lt;li>每個 UC 涉及的中間畫面是否都有進入和退出路徑？&lt;/li>
&lt;/ul>
&lt;p>app_tunnel 的操作盤點列了四個操作（配對、連線、輪替、啟停），首頁只提供了「連線」的入口。「配對」是 app 操作，應該有入口但沒有。「輪替」和「啟停」是主機端操作，不需要 app 入口。這個交叉比對能在 5 分鐘內揭露入口缺失。&lt;/p>
&lt;h3 id="自動化檢查">自動化檢查&lt;/h3>
&lt;p>從 router 定義檔解析所有路由路徑，再從 UI 元件的程式碼中搜尋所有 &lt;code>navigateTo&lt;/code>、&lt;code>context.go&lt;/code>、&lt;code>context.push&lt;/code>、&lt;code>router.push&lt;/code> 等導航呼叫的目標路徑。兩個集合取差集。&lt;/p>
&lt;p>自動化檢查能發現靜態定義的入口缺失，但無法發現動態導航（根據執行期條件決定目標路由）的可達性問題。&lt;/p>
&lt;h2 id="go-vs-push-的語意影響">&lt;code>go&lt;/code> vs &lt;code>push&lt;/code> 的語意影響&lt;/h2>
&lt;p>路由可達性確認之後，導航方式的選擇影響使用者的返回路徑。&lt;/p>
&lt;p>&lt;code>push&lt;/code> 把新畫面推入導航堆疊，使用者按 back 能回到前一個畫面。&lt;code>go&lt;/code> 替換整個導航堆疊，使用者按 back 不會回到原來的畫面。&lt;/p>
&lt;p>選擇 &lt;code>go&lt;/code> 還是 &lt;code>push&lt;/code> 取決於使用者的心理模型：這個導航是「暫時離開主畫面去做一件事，做完回來」（push），還是「切換到另一個主要工作區」（go）。&lt;/p>
&lt;p>app_tunnel 修復時選擇 &lt;code>context.push('/enrollment')&lt;/code> 讓使用者配對完成後按 back 回首頁 — 配對是「暫時去做一件事」，不是切換工作區（&lt;a href="https://tarrragon.github.io/blog/ux-design/cases/missing-enrollment-entry-point/" data-link-title="U.C4 首頁缺配對入口按鈕、導航流未完整列出" data-link-desc="Flutter app 首頁只有 Connect Terminal 按鈕、沒有 Enroll Device 入口 — 使用者首次使用時找不到配對功能。根因是導航流設計只考慮了日常操作（UC-02 連線）、遺漏了首次操作（UC-01 配對）的入口">U.C4&lt;/a>）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>畫面狀態矩陣完整定義 → &lt;a href="https://tarrragon.github.io/blog/ux-design/01-screen-state-machine/state-matrix-definition/" data-link-title="畫面狀態矩陣的定義與填寫方法" data-link-desc="四欄矩陣（顯示 / 可用操作 / 進入條件 / 退出路徑）的定義、填寫步驟和檢查規則 — 退出路徑為空 = UX 死胡同">畫面狀態矩陣的定義與填寫方法&lt;/a>&lt;/li>
&lt;li>想測試導航路徑的正確性 → &lt;a href="https://tarrragon.github.io/blog/testing/04-ui-automation/" data-link-title="模組四：自動化 UI 驗證" data-link-desc="Widget test 的狀態覆蓋策略、Playwright 驗證流程、螢幕狀態 coverage">testing 模組四 UI 自動化&lt;/a>&lt;/li>
&lt;li>想設計完整導航模式 → &lt;a href="https://tarrragon.github.io/blog/ux-design/05-navigation-patterns/" data-link-title="模組五：導航模式" data-link-desc="Push/pop stack、GoRouter 命名路由、tab bar、drawer — 導航方法選擇是設計決策">ux-design 模組五 導航模式&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>路由可達性檢查比較兩個集合：router 定義的所有路由，和使用者從 UI 操作能到達的所有路由。兩個集合的差集就是問題所在 — 定義了但不可達的路由是入口缺失，可達但未定義的路由是 404 風險。</p>
<h2 id="定義-vs-可達">定義 vs 可達</h2>
<h3 id="router-定義的路由">Router 定義的路由</h3>
<p>現代前端框架（Flutter GoRouter、React Router、Vue Router）通常有一個集中的路由定義檔，列出所有可存取的路徑和對應的畫面元件。這個列表是 router 認知的「所有畫面」。</p>
<h3 id="ui-可達的路由">UI 可達的路由</h3>
<p>從首頁（或 app 的入口畫面）開始，透過 UI 上的按鈕、連結、手勢能到達的所有路由。這個集合代表使用者實際能存取的畫面。</p>
<h3 id="差集分析">差集分析</h3>
<p><strong>router 有但 UI 不可達</strong>：路由定義了、畫面元件也實作了，但沒有任何 UI 元素導航到這個路由。功能存在但使用者找不到入口。</p>
<p><strong>UI 指向但 router 沒有</strong>：UI 上有一個按鈕 <code>navigateTo('/settings')</code>，但 router 沒有定義 <code>/settings</code> 路由。使用者點擊後會看到 404 或空白畫面。</p>
<h2 id="路由存在但不可達的案例">路由存在但不可達的案例</h2>
<p>app_tunnel 的 router 定義了三條路由：<code>/</code>（首頁）、<code>/enrollment</code>（配對）、<code>/terminal</code>（終端機）。首頁只有一個 Connect Terminal 按鈕導航到 <code>/terminal</code>。<code>/enrollment</code> 路由存在，<code>EnrollmentScreen</code> 完整實作，但首頁沒有任何 UI 元素導航到這個路由（<a href="/blog/ux-design/cases/missing-enrollment-entry-point/" data-link-title="U.C4 首頁缺配對入口按鈕、導航流未完整列出" data-link-desc="Flutter app 首頁只有 Connect Terminal 按鈕、沒有 Enroll Device 入口 — 使用者首次使用時找不到配對功能。根因是導航流設計只考慮了日常操作（UC-02 連線）、遺漏了首次操作（UC-01 配對）的入口">U.C4</a>）。</p>
<p>從使用者視角看，配對功能不存在。從開發者視角看，配對功能完整 — 路由定義了、畫面寫好了、業務邏輯都通了。問題出在「入口」這個連接層。</p>
<p>這和程式碼裡寫了一個 function 但沒有任何地方呼叫它的情況結構相同。Function 本身可能正確無誤，但從系統角度看是死程式碼。路由可達性檢查是這個問題在 UX 層的對應。</p>
<h2 id="檢查方法">檢查方法</h2>
<h3 id="手動檢查">手動檢查</h3>
<p>列出 router 定義的所有路由，然後逐一在 UI 上找到通往該路由的操作路徑。找不到路徑的就是不可達路由。</p>
<p>手動檢查的成本隨畫面數量線性增長。5 個路由的 app 很快能查完；50 個路由的 app 需要系統化方法。</p>
<h3 id="從操作盤點交叉比對">從操作盤點交叉比對</h3>
<p>BDD 操作盤點列出了所有使用者操作（UC）。每個 UC 對應至少一個畫面。把 UC 清單和 router 定義對照：</p>
<ul>
<li>每個 UC 的主要入口畫面是否有從首頁可達的路徑？</li>
<li>每個 UC 涉及的中間畫面是否都有進入和退出路徑？</li>
</ul>
<p>app_tunnel 的操作盤點列了四個操作（配對、連線、輪替、啟停），首頁只提供了「連線」的入口。「配對」是 app 操作，應該有入口但沒有。「輪替」和「啟停」是主機端操作，不需要 app 入口。這個交叉比對能在 5 分鐘內揭露入口缺失。</p>
<h3 id="自動化檢查">自動化檢查</h3>
<p>從 router 定義檔解析所有路由路徑，再從 UI 元件的程式碼中搜尋所有 <code>navigateTo</code>、<code>context.go</code>、<code>context.push</code>、<code>router.push</code> 等導航呼叫的目標路徑。兩個集合取差集。</p>
<p>自動化檢查能發現靜態定義的入口缺失，但無法發現動態導航（根據執行期條件決定目標路由）的可達性問題。</p>
<h2 id="go-vs-push-的語意影響"><code>go</code> vs <code>push</code> 的語意影響</h2>
<p>路由可達性確認之後，導航方式的選擇影響使用者的返回路徑。</p>
<p><code>push</code> 把新畫面推入導航堆疊，使用者按 back 能回到前一個畫面。<code>go</code> 替換整個導航堆疊，使用者按 back 不會回到原來的畫面。</p>
<p>選擇 <code>go</code> 還是 <code>push</code> 取決於使用者的心理模型：這個導航是「暫時離開主畫面去做一件事，做完回來」（push），還是「切換到另一個主要工作區」（go）。</p>
<p>app_tunnel 修復時選擇 <code>context.push('/enrollment')</code> 讓使用者配對完成後按 back 回首頁 — 配對是「暫時去做一件事」，不是切換工作區（<a href="/blog/ux-design/cases/missing-enrollment-entry-point/" data-link-title="U.C4 首頁缺配對入口按鈕、導航流未完整列出" data-link-desc="Flutter app 首頁只有 Connect Terminal 按鈕、沒有 Enroll Device 入口 — 使用者首次使用時找不到配對功能。根因是導航流設計只考慮了日常操作（UC-02 連線）、遺漏了首次操作（UC-01 配對）的入口">U.C4</a>）。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>畫面狀態矩陣完整定義 → <a href="/blog/ux-design/01-screen-state-machine/state-matrix-definition/" data-link-title="畫面狀態矩陣的定義與填寫方法" data-link-desc="四欄矩陣（顯示 / 可用操作 / 進入條件 / 退出路徑）的定義、填寫步驟和檢查規則 — 退出路徑為空 = UX 死胡同">畫面狀態矩陣的定義與填寫方法</a></li>
<li>想測試導航路徑的正確性 → <a href="/blog/testing/04-ui-automation/" data-link-title="模組四：自動化 UI 驗證" data-link-desc="Widget test 的狀態覆蓋策略、Playwright 驗證流程、螢幕狀態 coverage">testing 模組四 UI 自動化</a></li>
<li>想設計完整導航模式 → <a href="/blog/ux-design/05-navigation-patterns/" data-link-title="模組五：導航模式" data-link-desc="Push/pop stack、GoRouter 命名路由、tab bar、drawer — 導航方法選擇是設計決策">ux-design 模組五 導航模式</a></li>
</ul>
]]></content:encoded></item><item><title>6.5 跨進 production 的 routing 中樞</title><link>https://tarrragon.github.io/blog/llm/06-security/routing-to-production-security/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/06-security/routing-to-production-security/</guid><description>&lt;p>模組六前五章建立了個人 dev 視角的 LLM 安全判讀（&lt;a href="https://tarrragon.github.io/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">6.0 供應鏈&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/06-security/inference-server-binding/" data-link-title="6.1 推論伺服器的綁定與暴露範圍" data-link-desc="個人 dev 場景下 llama-server / Ollama / LM Studio 的 bind address 判讀：127.0.0.1 vs LAN vs 反代、預設安全、誤開放給內網的後果">6.1 伺服器綁定&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/06-security/tool-use-permission-model/" data-link-title="6.2 tool use 與 MCP server 的權限模型" data-link-desc="個人 dev 場景下 tool use / MCP server 的副作用權限：檔案系統 / shell / 網路存取邊界、第三方 MCP 信任、副作用的可逆性">6.2 tool use 權限&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/06-security/prompt-injection-in-ide/" data-link-title="6.3 IDE 場景的 prompt injection" data-link-desc="個人 dev 場景下 IDE 寫 code 工作流的 prompt injection：codebase 內容、外部文件、剪貼簿作為攻擊面、跟雲端 LLM 場景的差異">6.3 prompt injection&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/llm/06-security/cross-cloud-local-data-boundary/" data-link-title="6.4 跨雲端 / 本地的資料邊界" data-link-desc="個人 dev 場景下混用雲端 LLM 跟本地 LLM 時的 prompt 洩漏點：Continue.dev 多 provider 設定、隱私資料流、按敏感度分流的判讀">6.4 跨雲端資料邊界&lt;/a>）、framing 的根基是 &lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">0.7 隱私資料流原理&lt;/a>。當工作流從個人 dev 跨進團隊共用、再跨進 production 服務時、安全議題的 framing 跟控制機制都會升級。升級的軸對應 backend 既有卡片：&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/attack-surface/" data-link-title="Attack Surface" data-link-desc="說明系統哪些對外暴露面會被先行探測與枚舉">attack-surface&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast-radius&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/trust-boundary/" data-link-title="Trust Boundary" data-link-desc="說明系統哪些位置開始不能沿用原本的信任假設">trust-boundary&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant-boundary&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/iam/" data-link-title="IAM" data-link-desc="說明 identity and access management 如何集中管理身分、角色與權限">iam&lt;/a> 等。本章是這兩個跨越的 routing 中樞、把每個議題在 production 場景下的對應位置（backend/07 對應卡片）整理出來、避免讀者在升級階段「不知道下一步該讀什麼」。&lt;/p>
&lt;p>讀完本章後、你應該能判讀自己當前處在三層哪一階、要跨到下一階時需要補哪些議題、對應到 backend/07 哪些卡片。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;ol>
&lt;li>區分個人 dev、團隊共用、production 三層 LLM 部署的安全議題差異。&lt;/li>
&lt;li>知道從個人 dev 跨到團隊共用時、需要補哪些控制。&lt;/li>
&lt;li>知道從團隊共用跨到 production 時、需要補哪些控制。&lt;/li>
&lt;li>認識每層演化對應的 backend/07 卡片清單。&lt;/li>
&lt;li>知道何時該停留在當前層、何時該主動升級。&lt;/li>
&lt;/ol>
&lt;h2 id="三層演化的判讀軸">三層演化的判讀軸&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">個人 dev（本模組前五章）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">團隊共用（家裡 / 小團隊 / 內部部署）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> ↓
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">production 服務（對外服務 / SaaS / B2B）&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>三層的核心差異：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>個人 dev&lt;/th>
 &lt;th>團隊共用&lt;/th>
 &lt;th>production 服務&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>使用者數&lt;/td>
 &lt;td>1&lt;/td>
 &lt;td>5 ~ 50&lt;/td>
 &lt;td>50+ / 對外不限&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>信任假設&lt;/td>
 &lt;td>自己信自己&lt;/td>
 &lt;td>同事互信、訪客不信&lt;/td>
 &lt;td>全部不信、用 IAM 控制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>資料邊界&lt;/td>
 &lt;td>本機 user account&lt;/td>
 &lt;td>內網&lt;/td>
 &lt;td>多租戶、明確隔離&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失誤後果&lt;/td>
 &lt;td>自己承擔&lt;/td>
 &lt;td>影響少數同事&lt;/td>
 &lt;td>影響大量用戶 / 法律責任&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>控制機制需求&lt;/td>
 &lt;td>基本配置 + git track&lt;/td>
 &lt;td>+ auth + log + 政策&lt;/td>
 &lt;td>+ IAM + audit + IR + 合規&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>對應的時間 / 預算&lt;/td>
 &lt;td>小時級&lt;/td>
 &lt;td>天級&lt;/td>
 &lt;td>週 / 月級、需要專人或團隊&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵原則：&lt;strong>控制機制應該跟需求對齊、不該過度設計也不該不足&lt;/strong>。個人 dev 不需要 SOC 2 audit、production 不能只靠 git track。&lt;/p></description><content:encoded><![CDATA[<p>模組六前五章建立了個人 dev 視角的 LLM 安全判讀（<a href="/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">6.0 供應鏈</a>、<a href="/blog/llm/06-security/inference-server-binding/" data-link-title="6.1 推論伺服器的綁定與暴露範圍" data-link-desc="個人 dev 場景下 llama-server / Ollama / LM Studio 的 bind address 判讀：127.0.0.1 vs LAN vs 反代、預設安全、誤開放給內網的後果">6.1 伺服器綁定</a>、<a href="/blog/llm/06-security/tool-use-permission-model/" data-link-title="6.2 tool use 與 MCP server 的權限模型" data-link-desc="個人 dev 場景下 tool use / MCP server 的副作用權限：檔案系統 / shell / 網路存取邊界、第三方 MCP 信任、副作用的可逆性">6.2 tool use 權限</a>、<a href="/blog/llm/06-security/prompt-injection-in-ide/" data-link-title="6.3 IDE 場景的 prompt injection" data-link-desc="個人 dev 場景下 IDE 寫 code 工作流的 prompt injection：codebase 內容、外部文件、剪貼簿作為攻擊面、跟雲端 LLM 場景的差異">6.3 prompt injection</a>、<a href="/blog/llm/06-security/cross-cloud-local-data-boundary/" data-link-title="6.4 跨雲端 / 本地的資料邊界" data-link-desc="個人 dev 場景下混用雲端 LLM 跟本地 LLM 時的 prompt 洩漏點：Continue.dev 多 provider 設定、隱私資料流、按敏感度分流的判讀">6.4 跨雲端資料邊界</a>）、framing 的根基是 <a href="/blog/llm/00-foundations/privacy-data-flow/" data-link-title="0.7 隱私 / 資安的資料流原理" data-link-desc="從「位置」到「資料流」的思考升級：信任邊界、合約模型、零信任原則套用到 LLM 工作流">0.7 隱私資料流原理</a>。當工作流從個人 dev 跨進團隊共用、再跨進 production 服務時、安全議題的 framing 跟控制機制都會升級。升級的軸對應 backend 既有卡片：<a href="/blog/backend/knowledge-cards/attack-surface/" data-link-title="Attack Surface" data-link-desc="說明系統哪些對外暴露面會被先行探測與枚舉">attack-surface</a>、<a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast-radius</a>、<a href="/blog/backend/knowledge-cards/trust-boundary/" data-link-title="Trust Boundary" data-link-desc="說明系統哪些位置開始不能沿用原本的信任假設">trust-boundary</a>、<a href="/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">tenant-boundary</a>、<a href="/blog/backend/knowledge-cards/iam/" data-link-title="IAM" data-link-desc="說明 identity and access management 如何集中管理身分、角色與權限">iam</a> 等。本章是這兩個跨越的 routing 中樞、把每個議題在 production 場景下的對應位置（backend/07 對應卡片）整理出來、避免讀者在升級階段「不知道下一步該讀什麼」。</p>
<p>讀完本章後、你應該能判讀自己當前處在三層哪一階、要跨到下一階時需要補哪些議題、對應到 backend/07 哪些卡片。</p>
<h2 id="本章目標">本章目標</h2>
<ol>
<li>區分個人 dev、團隊共用、production 三層 LLM 部署的安全議題差異。</li>
<li>知道從個人 dev 跨到團隊共用時、需要補哪些控制。</li>
<li>知道從團隊共用跨到 production 時、需要補哪些控制。</li>
<li>認識每層演化對應的 backend/07 卡片清單。</li>
<li>知道何時該停留在當前層、何時該主動升級。</li>
</ol>
<h2 id="三層演化的判讀軸">三層演化的判讀軸</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">個人 dev（本模組前五章）
</span></span><span class="line"><span class="ln">2</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln">3</span><span class="cl">團隊共用（家裡 / 小團隊 / 內部部署）
</span></span><span class="line"><span class="ln">4</span><span class="cl">   ↓
</span></span><span class="line"><span class="ln">5</span><span class="cl">production 服務（對外服務 / SaaS / B2B）</span></span></code></pre></div><p>三層的核心差異：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>個人 dev</th>
          <th>團隊共用</th>
          <th>production 服務</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>使用者數</td>
          <td>1</td>
          <td>5 ~ 50</td>
          <td>50+ / 對外不限</td>
      </tr>
      <tr>
          <td>信任假設</td>
          <td>自己信自己</td>
          <td>同事互信、訪客不信</td>
          <td>全部不信、用 IAM 控制</td>
      </tr>
      <tr>
          <td>資料邊界</td>
          <td>本機 user account</td>
          <td>內網</td>
          <td>多租戶、明確隔離</td>
      </tr>
      <tr>
          <td>失誤後果</td>
          <td>自己承擔</td>
          <td>影響少數同事</td>
          <td>影響大量用戶 / 法律責任</td>
      </tr>
      <tr>
          <td>控制機制需求</td>
          <td>基本配置 + git track</td>
          <td>+ auth + log + 政策</td>
          <td>+ IAM + audit + IR + 合規</td>
      </tr>
      <tr>
          <td>對應的時間 / 預算</td>
          <td>小時級</td>
          <td>天級</td>
          <td>週 / 月級、需要專人或團隊</td>
      </tr>
  </tbody>
</table>
<p>關鍵原則：<strong>控制機制應該跟需求對齊、不該過度設計也不該不足</strong>。個人 dev 不需要 SOC 2 audit、production 不能只靠 git track。</p>
<h2 id="個人-dev--團隊共用要補什麼">個人 dev → 團隊共用：要補什麼</h2>
<p>從個人 dev 跨到團隊共用、典型的觸發場景：</p>
<ol>
<li>家裡跑模型給家人 / 室友用</li>
<li>小團隊共用一台 LLM server</li>
<li>公司內部部署、有 5 ~ 50 個工程師用</li>
</ol>
<p>需要補的控制（在前五章的基礎上）：</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>從個人 dev 的什麼演化而來</th>
          <th>對應的補強</th>
          <th>backend/07 對應卡片</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>身份識別</td>
          <td>自己一人 → 多人共用</td>
          <td>加 auth、知道誰送了什麼 prompt</td>
          <td><a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">identity-access-boundary</a></td>
      </tr>
      <tr>
          <td>入口治理</td>
          <td>bind 到 LAN 加 API key</td>
          <td>反代 + TLS + rate limit</td>
          <td><a href="/blog/backend/07-security-data-protection/entrypoint-and-server-protection/" data-link-title="7.3 入口治理與伺服器防護" data-link-desc="以問題驅動方式整理對外入口、管理平面與伺服器邊界">entrypoint-and-server-protection</a></td>
      </tr>
      <tr>
          <td>傳輸信任</td>
          <td>內網 HTTP 偶爾 OK</td>
          <td>內網全程 HTTPS、TLS 憑證管理</td>
          <td><a href="/blog/backend/07-security-data-protection/transport-trust-and-certificate-lifecycle/" data-link-title="7.5 傳輸信任與憑證生命週期" data-link-desc="以問題驅動方式整理傳輸信任鏈、會話完整性與憑證節奏">transport-trust-and-certificate-lifecycle</a></td>
      </tr>
      <tr>
          <td>秘密管理</td>
          <td>dotfile 環境變數</td>
          <td>集中 secret store（Vault / SSM / Doppler）</td>
          <td><a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">secrets-and-machine-credential-governance</a></td>
      </tr>
      <tr>
          <td>供應鏈</td>
          <td>自己抓 GGUF / npm package（見 <a href="/blog/llm/06-security/model-supply-chain-trust/" data-link-title="6.0 模型供應鏈與信任邊界" data-link-desc="個人 dev 用本地 LLM 時的模型權重來源信任：GGUF 完整性、Hugging Face / Ollama registry 信任、量化版本污染、檔案完整性檢查">6.0</a>）</td>
          <td>內部 mirror、固定 version、定期 audit</td>
          <td><a href="/blog/backend/07-security-data-protection/supply-chain-integrity-and-artifact-trust/" data-link-title="7.12 供應鏈完整性與 Artifact 信任" data-link-desc="定義 build provenance、artifact 信任與交付鏈風險問題">supply-chain-integrity-and-artifact-trust</a></td>
      </tr>
      <tr>
          <td>政策</td>
          <td>自己腦中的判讀</td>
          <td>寫明 acceptable use、敏感內容指引</td>
          <td>（結合各章的政策性章節）</td>
      </tr>
  </tbody>
</table>
<p>團隊共用階段的常見 anti-pattern：</p>
<ol>
<li><strong>把個人 dev 的 dotfile config 直接複製到團隊 server</strong>：API key、log 路徑、reset 機制都不對。</li>
<li><strong>依賴單一管理員口頭傳遞政策</strong>：沒寫下來、新成員不知道、人離職就失傳。</li>
<li><strong>跳過 auth 直接用「公司內網本來就安全」當理由</strong>：內網設備有訪客、有實習生、有 BYOD、有合作廠商；零信任的最低版本仍要做。</li>
</ol>
<h2 id="團隊共用--production要補什麼">團隊共用 → production：要補什麼</h2>
<p>從團隊共用跨到 production 服務、典型的觸發場景：</p>
<ol>
<li>把內部 LLM 服務開放給外部客戶（B2B）</li>
<li>做 SaaS-like LLM API 對外賣</li>
<li>把 LLM 嵌入產品給終端用戶用</li>
</ol>
<p>需要補的控制（在前面兩層的基礎上）：</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>從團隊共用的什麼演化而來</th>
          <th>對應的補強</th>
          <th>backend/07 對應卡片</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>多租戶隔離</td>
          <td>共用 server 跨同事 → 跨用戶</td>
          <td>KV cache / log / model 訪問權的多租戶隔離</td>
          <td><a href="/blog/backend/07-security-data-protection/llm-multi-tenant-isolation/" data-link-title="LLM 多租戶推論隔離" data-link-desc="production LLM 服務的多租戶隔離：KV cache 不共享、log / model artifact 隔離、跨用戶 prompt 洩漏面">llm-multi-tenant-isolation</a></td>
      </tr>
      <tr>
          <td>deployment 供應鏈</td>
          <td>內部 mirror → 對外責任</td>
          <td>模型 release 流程、簽章、回退機制</td>
          <td><a href="/blog/backend/07-security-data-protection/llm-deployment-supply-chain/" data-link-title="LLM Deployment 供應鏈完整性" data-link-desc="把 LLM 模型權重、推論伺服器、第三方 plugin 三條 production 供應鏈納入既有 artifact trust 框架的判讀">llm-deployment-supply-chain</a></td>
      </tr>
      <tr>
          <td>agent prompt injection 後果</td>
          <td>IDE injection（<a href="/blog/llm/06-security/prompt-injection-in-ide/" data-link-title="6.3 IDE 場景的 prompt injection" data-link-desc="個人 dev 場景下 IDE 寫 code 工作流的 prompt injection：codebase 內容、外部文件、剪貼簿作為攻擊面、跟雲端 LLM 場景的差異">6.3</a>）→ agent 場景（<a href="/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">4.4</a>）</td>
          <td>tool spec 設計、限制 agent loop、人為 review checkpoint</td>
          <td><a href="/blog/backend/07-security-data-protection/llm-prompt-injection-in-agent/" data-link-title="LLM Agent Prompt Injection 後果治理" data-link-desc="production LLM agent 場景的 prompt injection 後果：tool spec 設計、agent loop 限制、review checkpoint、跟 incident workflow 的接合">llm-prompt-injection-in-agent</a></td>
      </tr>
      <tr>
          <td>log / PII 治理</td>
          <td>簡單 access log → 完整 prompt log</td>
          <td>log 累積的 prompt 內容、PII 偵測與過濾、保留期限</td>
          <td><a href="/blog/backend/07-security-data-protection/llm-log-and-pii-governance/" data-link-title="LLM Log 與 PII 治理" data-link-desc="production LLM 服務的 prompt log 累積、PII 偵測與過濾、保留期限與合規對齊">llm-log-and-pii-governance</a></td>
      </tr>
      <tr>
          <td>偵測訊號</td>
          <td>看 log → 主動偵測</td>
          <td>LLM agent 異常行為的訊號設計、tool use 異常模式</td>
          <td><a href="/blog/backend/07-security-data-protection/llm-as-service-detection-coverage/" data-link-title="LLM Service 偵測訊號覆蓋" data-link-desc="production LLM 服務的 detection 訊號設計：tool call 異常模式、prompt injection 觸發徵兆、abuse 跟濫用模式、跟既有 detection-coverage 框架的接合">llm-as-service-detection-coverage</a></td>
      </tr>
      <tr>
          <td>Workload Identity</td>
          <td>server 自己持 API key → workload IAM</td>
          <td>每個 workload 一個身份、可 audit</td>
          <td><a href="/blog/backend/07-security-data-protection/workload-identity-and-federated-trust/" data-link-title="7.10 Workload Identity 與聯邦信任邊界" data-link-desc="定義非人類身份、跨平台信任與短時憑證治理問題">workload-identity-and-federated-trust</a></td>
      </tr>
      <tr>
          <td>偵測平台</td>
          <td>手動觀察 → SIEM</td>
          <td>集中偵測、alert 系統</td>
          <td><a href="/blog/backend/07-security-data-protection/detection-coverage-and-signal-governance/" data-link-title="7.13 偵測覆蓋率與訊號治理" data-link-desc="定義偵測覆蓋、訊號品質與誤報成本的治理問題">detection-coverage-and-signal-governance</a></td>
      </tr>
      <tr>
          <td>Incident response</td>
          <td>重啟解決 → IR 流程</td>
          <td>IR 演練、escalation、post-mortem</td>
          <td><a href="/blog/backend/07-security-data-protection/incident-case-to-control-workflow/" data-link-title="7.16 從公開事故到工程 Workflow：案例如何回寫控制面" data-link-desc="建立公開事故如何轉成控制面失效樣式與 workflow 回寫的大綱">incident-case-to-control-workflow</a></td>
      </tr>
      <tr>
          <td>合規</td>
          <td>不需要 → 對外服務需要</td>
          <td>GDPR / HIPAA / SOC 2 等</td>
          <td><a href="/blog/backend/07-security-data-protection/data-protection-and-masking-governance/" data-link-title="7.4 資料保護與遮罩治理" data-link-desc="以問題驅動方式整理資料分級、遮罩、匯出與備份治理">data-protection-and-masking-governance</a></td>
      </tr>
  </tbody>
</table>
<p>production 階段不是「把團隊共用放大」、是「另一個複雜度等級」。多數議題從 backend/07 既有卡片開始讀、LLM-specific 議題在 backend/07 的 LLM 相關章節（<code>llm-*.md</code>）補充。</p>
<h2 id="何時該停留在當前層">何時該停留在當前層</h2>
<p>不是所有工作流都需要升級。停留在當前層的合理判讀：</p>
<table>
  <thead>
      <tr>
          <th>當前層</th>
          <th>該停留的徵兆</th>
          <th>升級的徵兆</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>個人 dev</td>
          <td>只有自己用、不分享、沒對外暴露需求</td>
          <td>開始有人想連你的 server / 想做 demo 給朋友 / 想分享給家人</td>
      </tr>
      <tr>
          <td>團隊共用</td>
          <td>5 ~ 50 人的內部使用、不對外賣、不涉及客戶 PII</td>
          <td>客戶要連 / 對外 SLA / 要收費 / 開始涉及客戶 PII</td>
      </tr>
      <tr>
          <td>production</td>
          <td>已對外服務、有 SLA、有客戶</td>
          <td>（目標狀態）</td>
      </tr>
  </tbody>
</table>
<p>升級的兩個常見錯誤：</p>
<ol>
<li><strong>過早升級</strong>：個人 dev 階段就上 enterprise stack（IAM、Vault、SIEM）、複雜度過高、自己用不到、維護成本反而傷工作流。</li>
<li><strong>過晚升級</strong>：團隊共用階段該補的控制沒補、出事才補、可能已經有資料外洩 / 法律責任。</li>
</ol>
<p>判讀依據：<strong>控制機制對齊實際 threat model 跟 user 規模</strong>、不是「越多越好」。</p>
<h2 id="跨層升級的常見-anti-pattern">跨層升級的常見 anti-pattern</h2>
<p>從各層往上跨時、常見的意外：</p>
<ol>
<li><strong>把個人 dev 的 LLM client config 直接放上 production</strong>：autocomplete model、default model、API key 都不對；production 場景需要重新設計 model 路由。</li>
<li><strong>把個人習慣的 prompt injection 防護當 production 防護</strong>：「我 git track 工作流」對個人 dev 夠、production agent 場景下、git 不在迴路裡、要改用 tool spec + review checkpoint。</li>
<li><strong>production 場景仍然依賴使用者「看 prompt 內容」</strong>：使用者數量大、不可能每個 prompt 都人工看；production 需要自動化偵測訊號。</li>
<li><strong>production 場景沒 tenant 隔離</strong>：所有用戶的 KV cache / log / context 混在一起、A 用戶能看到 B 用戶的 cache hit。</li>
<li><strong>沒有 vendor 政策的書面化承諾</strong>：team 階段口頭講「我們不訓練客戶資料」、production 階段要寫進條款 / SLA。</li>
</ol>
<h2 id="給讀者的層級判讀清單">給讀者的層級判讀清單</h2>
<p>判斷自己當前在哪一層：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[ ] 只有自己用                                              → 個人 dev
</span></span><span class="line"><span class="ln">2</span><span class="cl">[ ] 1 ~ 5 個人共用一台 server                                → 個人 dev 或團隊共用初期
</span></span><span class="line"><span class="ln">3</span><span class="cl">[ ] 5 ~ 50 個人共用、內部部署                                → 團隊共用
</span></span><span class="line"><span class="ln">4</span><span class="cl">[ ] 對外提供 API 服務 / SaaS                                 → production
</span></span><span class="line"><span class="ln">5</span><span class="cl">[ ] 服務多個客戶 / 涉及客戶 PII                              → production
</span></span><span class="line"><span class="ln">6</span><span class="cl">[ ] 有 SLA / 合約承諾                                        → production</span></span></code></pre></div><p>對應的「要補的議題」：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">個人 dev → 團隊共用：
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  [ ] auth                  ← backend/07 identity-access-boundary
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  [ ] 入口治理               ← backend/07 entrypoint-and-server-protection
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  [ ] TLS                    ← backend/07 transport-trust-and-certificate-lifecycle
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  [ ] secret 集中管理        ← backend/07 secrets-and-machine-credential-governance
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  [ ] 內部 supply chain      ← backend/07 supply-chain-integrity-and-artifact-trust
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  [ ] 寫下 acceptable use 政策
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">團隊共用 → production：
</span></span><span class="line"><span class="ln">10</span><span class="cl">  [ ] 多租戶 isolation       ← backend/07 llm-multi-tenant-isolation
</span></span><span class="line"><span class="ln">11</span><span class="cl">  [ ] deployment 供應鏈      ← backend/07 llm-deployment-supply-chain
</span></span><span class="line"><span class="ln">12</span><span class="cl">  [ ] agent prompt injection ← backend/07 llm-prompt-injection-in-agent
</span></span><span class="line"><span class="ln">13</span><span class="cl">  [ ] log / PII 治理         ← backend/07 llm-log-and-pii-governance
</span></span><span class="line"><span class="ln">14</span><span class="cl">  [ ] 偵測訊號               ← backend/07 llm-as-service-detection-coverage
</span></span><span class="line"><span class="ln">15</span><span class="cl">  [ ] workload identity      ← backend/07 workload-identity-and-federated-trust
</span></span><span class="line"><span class="ln">16</span><span class="cl">  [ ] 偵測平台               ← backend/07 detection-coverage-and-signal-governance
</span></span><span class="line"><span class="ln">17</span><span class="cl">  [ ] IR 流程                ← backend/07 incident-case-to-control-workflow
</span></span><span class="line"><span class="ln">18</span><span class="cl">  [ ] 合規                   ← backend/07 data-protection-and-masking-governance</span></span></code></pre></div><h2 id="下一步">下一步</h2>
<p>本章是模組六的最後一章。下一步可以回到 <a href="/blog/llm/06-security/" data-link-title="模組六：本地 LLM 的安全與權限" data-link-desc="個人 dev 在自己機器上跑本地 LLM 的安全議題：模型供應鏈、推論伺服器綁定、tool use 副作用、prompt injection 在 IDE、跨雲端 / 本地資料邊界">模組六 _index</a> 看其他章節、或進入 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">Backend 模組七 資安與資料保護</a> 接 production 場景。</p>
]]></content:encoded></item></channel></rss>