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