URL 分享:無後端的計畫協作
學完你能做什麼
- ✅ 透過 URL 分享計畫和註解,無需登入帳號或部署伺服器
- ✅ 理解 deflate 壓縮和 Base64 編碼如何將資料嵌入 URL hash
- ✅ 區分分享模式(唯讀)和本地模式(可編輯)
- ✅ 配置
PLANNOTATOR_SHARE環境變數控制分享功能 - ✅ 處理 URL 長度限制和分享失敗的情況
你現在的困境
問題 1:想請團隊成員協助審查 AI 生成的計畫,但沒有協作平台。
問題 2:使用截圖或複製文字的方式分享審查內容,對方無法直接看到你的註解。
問題 3:部署線上協作伺服器成本高,或公司安全政策不允許。
問題 4:需要一個簡單快速的分享方式,但不知道如何保證資料隱私。
Plannotator 能幫你:
- 無需後端伺服器,所有資料壓縮在 URL 中
- 分享連結包含完整計畫和註解,接收方可查看
- 資料不離開本機裝置,隱私安全
- 生成的 URL 可複製到任何通訊工具
什麼時候用這一招
使用場景:
- 需要團隊成員審查 AI 生成的實施計畫
- 想分享程式碼審查結果給同事
- 需要保存審查內容到筆記(結合 Obsidian/Bear 整合)
- 快速獲取他人對計畫的反饋
不適用場景:
- 需要即時協作編輯(Plannotator 分享是唯讀的)
- 計畫內容超過 URL 長度限制(通常數千行)
- 分享內容包含敏感資訊(URL 本身不加密)
安全提示
分享 URL 包含完整計畫和註解,請勿分享包含敏感資訊的內容(如 API 金鑰、密碼等)。分享 URL 本身可被任何人存取,不會自動過期。
核心思路
URL 分享是什麼
URL 分享是 Plannotator 提供的一種無後端協作方式,透過將計畫和註解壓縮到 URL hash 中,實現無需伺服器的分享功能。
為什麼叫「無後端」?
傳統協作方案需要後端伺服器儲存計畫和註解,使用者透過 ID 或 token 存取。Plannotator 的 URL 分享不依賴任何後端——所有資料都在 URL 中,接收方打開連結即可解析內容。這保證了隱私(資料不上傳)和簡潔性(無需部署服務)。
工作原理
┌─────────────────────────────────────────────────────────┐
│ 使用者 A(分享者) │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. 審查計畫,新增註解 │
│ ┌──────────────────────┐ │
│ │ Plan: 實施計畫 │ │
│ │ Annotations: [ │ │
│ │ {type: 'REPLACE'}, │ │
│ │ {type: 'COMMENT'} │ │
│ │ ] │ │
│ └──────────────────────┘ │
│ │ │
│ ▼ │
│ 2. 點擊 Export → Share │
│ │ │
│ ▼ │
│ 3. 壓縮資料 │
│ JSON → deflate → Base64 → URL 安全字元 │
│ ↓ │
│ https://share.plannotator.ai/#eJyrVkrLz1... │
│ │
└─────────────────────────────────────────────────────────┘
│
│ 複製 URL
▼
┌─────────────────────────────────────────────────────────┐
│ 使用者 B(接收者) │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. 打開分享 URL │
│ https://share.plannotator.ai/#eJyrVkrLz1... │
│ │ │
│ ▼ │
│ 2. 瀏覽器解析 hash │
│ URL 安全字元 → Base64 解碼 → deflate 解壓 → JSON │
│ │ │
│ ▼ │
│ 3. 恢復計畫和註解 │
│ ┌──────────────────────┐ │
│ │ Plan: 實施計畫 │ ✅ 唯讀模式 │
│ │ Annotations: [ │ (無法提交決策) │
│ │ {type: 'REPLACE'}, │ │
│ │ {type: 'COMMENT'} │ │
│ │ ] │ │
│ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘壓縮演算法詳解
步驟 1:JSON 序列化
{
"p": "# Plan\n\nStep 1...",
"a": [
["R", "old text", "new text", null, null],
["C", "context", "comment text", null, null]
],
"g": ["image1.png", "image2.png"]
}步驟 2:Deflate-raw 壓縮
- 使用原生
CompressionStream('deflate-raw')API - 壓縮率典型值為 60-80%(取決於文字重複度,非原始碼定義)
- 原始碼位置:
packages/ui/utils/sharing.ts:34
步驟 3:Base64 編碼
const base64 = btoa(String.fromCharCode(...compressed));步驟 4:URL 安全字元替換
base64
.replace(/\+/g, '-') // + → -
.replace(/\//g, '_') // / → _
.replace(/=/g, ''); // = → ''(移除填充)為什麼替換特殊字元?
URL 中某些字元有特殊含義(如 + 表示空格,/ 是路徑分隔符)。Base64 編碼後可能包含這些字元,會導致 URL 解析錯誤。替換為 - 和 _ 後,URL 變得安全且可複製。
註解格式最佳化
為壓縮效率,Plannotator 使用精簡的註解格式(ShareableAnnotation):
| 原始 Annotation | 精簡格式 | 說明 |
|---|---|---|
{type: 'DELETION', originalText: '...', text: undefined, ...} | ['D', 'old text', null, images?] | D = Deletion,null 表示無 text |
{type: 'REPLACEMENT', originalText: '...', text: 'new...', ...} | ['R', 'old text', 'new text', null, images?] | R = Replacement |
{type: 'COMMENT', originalText: '...', text: 'comment...', ...} | ['C', 'old text', 'comment text', null, images?] | C = Comment |
{type: 'INSERTION', originalText: '...', text: 'new...', ...} | ['I', 'context', 'new text', null, images?] | I = Insertion |
{type: 'GLOBAL_COMMENT', text: '...', ...} | ['G', 'comment text', null, images?] | G = Global comment |
欄位順序固定,省略鍵名,顯著減少資料量。原始碼位置:packages/ui/utils/sharing.ts:76
分享 URL 結構
https://share.plannotator.ai/#<compressed_data>
↑
hash 部分- 基礎網域:
share.plannotator.ai(獨立分享頁面) - Hash 分隔符:
#(不會傳送到伺服器,完全由前端解析) - 壓縮資料:Base64url 編碼的壓縮 JSON
🎒 開始前的準備
前置條件:
檢查分享功能是否啟用:
# 預設啟用
echo $PLANNOTATOR_SHARE
# 如需停用分享(例如企業安全策略)
export PLANNOTATOR_SHARE=disabled環境變數說明
PLANNOTATOR_SHARE 控制分享功能的啟用狀態:
- 未設定或非 "disabled":啟用分享功能
- 設定為 "disabled":停用分享(Export Modal 只顯示 Raw Diff 標籤)
原始碼位置:apps/hook/server/index.ts:44、apps/opencode-plugin/index.ts:50
檢查瀏覽器相容性:
# 在瀏覽器控制台執行
const stream = new CompressionStream('deflate-raw');
console.log('CompressionStream supported');如果輸出 CompressionStream supported,說明瀏覽器支援。現代瀏覽器(Chrome 80+、Firefox 113+、Safari 16.4+)均支援。
跟我做
第 1 步:完成計畫審查
為什麼 分享前需要先完成審查,包括新增註解。
操作:
- 在 Claude Code 或 OpenCode 中觸發計畫審查
- 查看計畫內容,選取需要修改的文字
- 新增註解(刪除、替換、評論等)
- (可選)上傳圖片附件
你應該看到:
┌─────────────────────────────────────────────────────────────┐
│ Plan Review │
├─────────────────────────────────────────────────────────────┤
│ │
│ # Implementation Plan │
│ │
│ ## Phase 1: Setup │
│ Set up WebSocket server on port 8080 │
│ │
│ ## Phase 2: Authentication │
│ Implement JWT authentication middleware │
│ ┌─────────────────────┐ │
│ ━━━━━━━━━━━━━━━━│ Replace: "implement" │ │
│ └─────────────────────┘ │
│ │
│ Annotation Panel │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ REPLACE: "implement" → "add" │ │
│ │ JWT is overkill, use simple session tokens │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ [Approve] [Request Changes] [Export] │
└─────────────────────────────────────────────────────────────┘第 2 步:打開 Export Modal
為什麼 Export Modal 提供了分享 URL 的產生入口。
操作:
- 點擊右上角的 Export 按鈕
- 等待 Export Modal 打開
你應該看到:
┌─────────────────────────────────────────────────────────────┐
│ Export × │
│ 1 annotation Share | Raw Diff │
├─────────────────────────────────────────────────────────────┤
│ │
│ Shareable URL │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ https://share.plannotator.ai/#eJyrVkrLz1... │ │
│ │ [Copy] │ │
│ │ 3.2 KB │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ This URL contains full plan and all annotations. │
│ Anyone with this link can view and add to your annotations.│
│ │
└─────────────────────────────────────────────────────────────┘URL 大小提示
右下角顯示 URL 的位元組數(如 3.2 KB)。如果 URL 過長(超過 8 KB),考慮減少註解數量或圖片附件。
第 3 步:複製分享 URL
為什麼 複製 URL 後可以貼上到任何通訊工具(Slack、Email、微信等)。
操作:
- 點擊 Copy 按鈕
- 等待按鈕變為 Copied!
- URL 已複製到剪貼簿
你應該看到:
┌─────────────────────────────────────────────────────────────┐
│ Shareable URL │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ https://share.plannotator.ai/#eJyrVkrLz1... │ │
│ │ ✓ Copied │ │
│ │ 3.2 KB │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘自動選取
點擊 URL 輸入框會自動選取全部內容,方便手動複製(如果不使用 Copy 按鈕)。
第 4 步:分享 URL 給協作者
為什麼 協作者透過打開 URL 可以查看計畫和註解。
操作:
- 將 URL 貼上到通訊工具(Slack、Email 等)
- 傳送給團隊成員
範例訊息:
Hi @團隊,
請幫忙審查這個實施計畫:
https://share.plannotator.ai/#eJyrVkrLz1...
我在第 2 階段新增了一個替換註解,認為 JWT 過於複雜。
請給出你的反饋,謝謝!第 5 步:協作者打開分享 URL(接收方)
為什麼 協作者需要在瀏覽器中打開 URL 查看內容。
操作(協作者執行):
- 點擊分享 URL
- 等待頁面載入
你應該看到(協作者視角):
┌─────────────────────────────────────────────────────────────┐
│ Plan Review Read-only │
├─────────────────────────────────────────────────────────────┤
│ │
│ # Implementation Plan │
│ │
│ ## Phase 1: Setup │
│ Set up WebSocket server on port 8080 │
│ │
│ ## Phase 2: Authentication │
│ Implement JWT authentication middleware │
│ ┌─────────────────────┐ │
│ ━━━━━━━━━━━━━━━━│ Replace: "implement" │ │
│ │ └─────────────────────┘ │
│ │ This annotation was shared by [Your Name] │
│ │
│ Annotation Panel │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ REPLACE: "implement" → "add" │ │
│ │ JWT is overkill, use simple session tokens │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ [View Only Mode - Approve and Deny disabled] │
└─────────────────────────────────────────────────────────────┘唯讀模式
分享 URL 打開後,介面右上角顯示 "Read-only" 標籤,Approve 和 Deny 按鈕被停用。協作者可以查看計畫和註解,但無法提交決策。
解壓過程
協作者打開 URL 時,瀏覽器會自動執行以下步驟(由 useSharing Hook 觸發):
- 從
window.location.hash提取壓縮資料 - 反向執行 Base64 解碼 → deflate 解壓 → JSON 解析
- 恢復計畫和註解
- 清除 URL hash(避免重新整理時重新載入)
原始碼位置:packages/ui/hooks/useSharing.ts:67
檢查點 ✅
驗證分享 URL 是否有效:
- 複製分享 URL
- 在新分頁或無痕模式打開
- 確認顯示相同的計畫和註解
驗證唯讀模式:
- 協作者打開分享 URL
- 檢查右上角是否有 "Read-only" 標籤
- 確認 Approve 和 Deny 按鈕被停用
驗證 URL 長度:
- 查看 Export Modal 中的 URL 大小
- 確認不超過 8 KB(如超過,考慮減少註解)
踩坑提醒
問題 1:URL 分享按鈕不顯示
現象:Export Modal 中沒有 Share 標籤,只有 Raw Diff。
原因:PLANNOTATOR_SHARE 環境變數設定為 "disabled"。
解決方法:
# 檢查目前值
echo $PLANNOTATOR_SHARE
# 移除或設定為其他值
unset PLANNOTATOR_SHARE
# 或
export PLANNOTATOR_SHARE=enabled原始碼位置:apps/hook/server/index.ts:44
問題 2:分享 URL 打開後顯示空白頁面
現象:協作者打開 URL,頁面無內容。
原因:URL hash 在複製過程中遺失或被截斷。
解決方法:
- 確保複製完整的 URL(包括
#及後面的所有字元) - 不要使用短連結服務(可能會截斷 hash)
- 使用 Export Modal 中的 Copy 按鈕,而不是手動複製
URL hash 長度
分享 URL 的 hash 部分通常有數千個字元,手動複製容易遺漏。建議使用 Copy 按鈕或複製 → 貼上兩次驗證完整性。
問題 3:URL 太長,無法傳送
現象:URL 超過通訊工具的字元限制(如微信、Slack)。
原因:計畫內容過長或註解數量過多。
解決方法:
- 刪除不必要的註解
- 減少圖片附件
- 考慮使用 Raw Diff 匯出並儲存為檔案
- 使用程式碼審查功能(diff 模式的壓縮率更高)
問題 4:協作者看不到我的圖片
現象:分享 URL 包含圖片路徑,但協作者打開後顯示 "Image not found"。
原因:圖片儲存在本地 /tmp/plannotator/ 目錄,協作者無法存取。
解決方法:
- Plannotator 的 URL 分享不支援跨裝置圖片存取
- 建議使用 Obsidian 整合,圖片儲存到 vault 後可以分享
- 或者截圖並嵌入到註解中(文字描述)
原始碼位置:packages/server/index.ts:163(圖片儲存路徑)
問題 5:分享後修改了註解,URL 未更新
現象:新增新註解後,Export Modal 中的 URL 沒有變化。
原因:shareUrl 狀態未自動重新整理(罕見情況,通常是 React 狀態更新問題)。
解決方法:
- 關閉 Export Modal
- 重新打開 Export Modal
- URL 應該自動更新為最新內容
原始碼位置:packages/ui/hooks/useSharing.ts:128(refreshShareUrl 函數)
本課小結
URL 分享功能讓你無需後端伺服器即可分享計畫和註解:
- ✅ 無後端:資料壓縮在 URL hash 中,不依賴伺服器
- ✅ 隱私安全:資料不上傳,只在本地和協作者之間傳遞
- ✅ 簡潔高效:一鍵產生 URL,複製貼上即可分享
- ✅ 唯讀模式:協作者可以查看和新增註解,但無法提交決策
技術原理:
- Deflate-raw 壓縮:將 JSON 資料壓縮約 60-80%
- Base64 編碼:將二進位資料轉換為文字
- URL 安全字元替換:
+→-、/→_、=→'' - Hash 解析:前端自動解壓並恢復內容
配置選項:
PLANNOTATOR_SHARE=disabled:停用分享功能- 預設啟用:分享功能可用
下一課預告
下一課我們學習 Obsidian 整合。
你會學到:
- 自動偵測 Obsidian vaults
- 將批准的計畫儲存到 Obsidian
- 自動產生 frontmatter 和標籤
- 結合 URL 分享和 Obsidian 知識管理
下一課預告
下一課我們學習 Obsidian 整合。
你會學到:
- 如何配置 Obsidian 整合,自動儲存計畫到 vault
- 理解 frontmatter 和標籤產生機制
- 利用 backlink 建構知識圖譜
附錄:原始碼參考
點擊展開查看原始碼位置
更新時間:2026-01-24
| 功能 | 檔案路徑 | 行號 |
|---|---|---|
| 壓縮資料(deflate + Base64) | packages/ui/utils/sharing.ts | 30-48 |
| 解壓資料 | packages/ui/utils/sharing.ts | 53-71 |
| 轉換註解格式(精簡) | packages/ui/utils/sharing.ts | 76-95 |
| 恢復註解格式 | packages/ui/utils/sharing.ts | 102-155 |
| 產生分享 URL | packages/ui/utils/sharing.ts | 162-175 |
| 解析 URL hash | packages/ui/utils/sharing.ts | 181-194 |
| URL 大小格式化 | packages/ui/utils/sharing.ts | 199-205 |
| URL 分享 Hook | packages/ui/hooks/useSharing.ts | 45-155 |
| Export Modal UI | packages/ui/components/ExportModal.tsx | 1-196 |
| 分享開關配置(Hook) | apps/hook/server/index.ts | 44 |
| 分享開關配置(OpenCode) | apps/opencode-plugin/index.ts | 50 |
關鍵常數:
SHARE_BASE_URL = 'https://share.plannotator.ai':分享頁面基礎網域
關鍵函數:
compress(payload: SharePayload): Promise<string>:壓縮 payload 為 base64url 字串decompress(b64: string): Promise<SharePayload>:解壓 base64url 字串為 payloadtoShareable(annotations: Annotation[]): ShareableAnnotation[]:將完整註解轉換為精簡格式fromShareable(data: ShareableAnnotation[]): Annotation[]:將精簡格式恢復為完整註解generateShareUrl(markdown, annotations, attachments): Promise<string>:產生完整的分享 URLparseShareHash(): Promise<SharePayload | null>:解析目前 URL 的 hash
資料型別:
interface SharePayload {
p: string; // plan markdown
a: ShareableAnnotation[];
g?: string[]; // global attachments
}
type ShareableAnnotation =
| ['D', string, string | null, string[]?] // Deletion
| ['R', string, string, string | null, string[]?] // Replacement
| ['C', string, string, string | null, string[]?] // Comment
| ['I', string, string, string | null, string[]?] // Insertion
| ['G', string, string | null, string[]?]; // Global Comment