Mindwendel v0.4.9 REST API 与 WebSocket 协议详解
Mindwendel 通信体系完整梳理:Phoenix LiveView 架构 vs 传统 REST+Channels 对比、全部 REST 路由清单(创建/导出/链接预览/附件上传下载)及 curl 示例、phx-js 协议帧结构(HELLO/phx_join/phx_event/phx_reply)、连接生命周期、Phoenix.PubSub 广播机制、多节点部署 PubSub 问题、二次开发集成指南(Python 示例)。
Mindwendel v0.4.9 REST API 与 WebSocket 协议详解
目标:系统梳理 Mindwendel 对外暴露的 HTTP/REST 端点和实时通信协议(Phoenix LiveView 专用 WebSocket),覆盖路由清单、请求/响应格式、LiveView 协议帧结构、连接生命周期,以及二次开发集成指引。
一、协议体系概述
Mindwendel 采用 Phoenix LiveView 架构,其通信模型与传统"REST API + WebSocket Channels"有本质区别:
传统架构(如 Wekan / tldraw Yjs):
浏览器 ←─── REST API(CRUD 资源)───▶ 后端
浏览器 ←─── WebSocket Channel(实时事件推送)───▶ 后端
Mindwendel 架构:
浏览器 ←─── LiveView 专用 WebSocket(phx-js 协议)───▶ Phoenix LiveView 进程
│
├─ 首次连接: HTTP 200 返回完整 HTML(含 LiveView 挂载点)
└─ 后续交互: 全部走 WebSocket 帧(事件 → DOM 差分推送)
仅有的少量 REST 端点:
浏览器 ←─── 静态资源下载 / 简单 JSON 接口 ──────▶ Phoenix Controller
核心差异
| 维度 | 传统 REST + Channels | Mindwendel LiveView |
|---|---|---|
| CRUD 操作 | REST POST/PUT/DELETE | LiveView handle_event 内部调用 Ecto 操作 |
| 实时推送 | WebSocket 自定义 JSON | LiveView push_event / Phoenix.PubSub 广播 |
| 状态管理 | 前端维护 state | 服务端 LiveView 进程维护 state,前端仅持有最小快照 |
| 二次开发 | 直接 HTTP 请求 | 需模拟 LiveView WebSocket 协议 |
| 资源 URL | /api/v1/brainstormings/:id | 无,URL 仅作为 LiveView 挂载路由 |
结论:Mindwendel 没有设计对外 REST API——其 REST 端点仅服务于前端页面的特定功能(文件上传、链接预览),核心业务逻辑完全封装在 LiveView 进程中。如需程序化集成,应走 Phoenix LiveView WebSocket 协议。
二、HTTP 路由清单
2.1 路由来源
Mindwendel 的路由定义在 lib/mindwendel_web/router.ex,由 Phoenix Router 框架注册。所有 HTTP 请求先经过 Plug 管道(安全头、session、CSRF、Locale),再分发到 Controller 或 LiveView。
2.2 完整路由表
以下路由按 HTTP 方法分组,路径参数用 :param 标注。
页面路由(LiveView 挂载端点)
| 方法 | 路径 | 类型 | Controller/LiveView | 说明 |
|---|---|---|---|---|
| GET | / | LiveView | StartLive.Index | 首页,创建/参与头脑风暴入口 |
| GET | /brainstorming/new | LiveView | StartLive.New | 创建新头脑风暴 |
| GET | /brainstorming/:id | LiveView | BrainstormingLive.Index | 头脑风暴主界面(核心协作页) |
| GET | /admin | LiveView | Admin.Index | 管理员后台(需登录) |
这四个 GET 路由返回完整 HTML 页面,页面内嵌的 <div data-phx-main="true"> 就是 LiveView 的挂载点,随后客户端 JS phx-js 建立 WebSocket 连接接管交互。
REST 端点(JSON 响应)
| 方法 | 路径 | Controller | 请求格式 | 响应格式 | 说明 |
|---|---|---|---|---|---|
| POST | /brainstorming/new-ajax | BrainstormingController | application/json | 201 {id, url_token} | AJAX 方式创建头脑风暴,不需要 LiveView 上下文 |
| GET | /brainstorming/:id/export | BrainstormingController | query 参数: format=html|csv | 文件下载 | 导出头脑风暴为 HTML 或 CSV |
| POST | /link-preview | LinkPreviewController | {url: "https://..."} | 200 {title, description, image, ...} | URL Open Graph 元数据提取 |
| POST | /brainstorming/:brainstorming_id/idea/:idea_id/file-upload | FileController | multipart/form-data (file) | 201 {id, url} | 便签附件上传 |
| GET | /file/:id | FileController | — | 文件下载 | 下载加密存储的附件(解密后返回) |
| DELETE | /file/:id | FileController | — | 204 No Content | 删除附件 |
静态页面
| 方法 | 路径 | Controller | 说明 |
|---|---|---|---|
| GET | /about | StaticPageController | 关于页面 |
| GET | /privacy | StaticPageController | 隐私声明(需 MW_FEATURE_LEGAL_PRIVACY_LINKS=true) |
| GET | /imprint | StaticPageController | 法律声明 |
| GET | /health | — | 内置健康检查端点(Phoenix 默认) |
三、REST 端点详解
3.1 创建头脑风暴(AJAX 方式)
POST /brainstorming/new-ajax
请求:
curl -X POST http://localhost:4000/brainstorming/new-ajax \
-H "Content-Type: application/json" \
-d '{
"brainstorming": {
"challenge": "今天晚上吃什么?",
"username": "测试用户",
"language": "en"
}
}'
响应(成功):
{
"id": 42,
"url_token": "brainstorming_abc123",
"path": "/brainstorming/brainstorming_abc123"
}
响应(参数校验失败):
{
"error": "challenge cannot be blank"
}
💡 注意:这个端点不做认证——任何人都可以创建头脑风暴。Mindwendel 是匿名优先设计,不强制注册/登录。
3.2 导出头脑风暴
GET /brainstorming/:id/export?format=html
# HTML 导出
curl -s http://localhost:4000/brainstorming/brainstorming_abc123/export?format=html \
-H "Accept: text/html" \
-o brainstorming.html
# CSV 导出
curl -s http://localhost:4000/brainstorming/brainstorming_abc123/export?format=csv \
-H "Accept: text/csv" \
-o brainstorming.csv
CSV 导出示例内容:
"id","challenge","idea_text","username","labels","likes"
"42","今天晚上吃什么?","火锅","测试用户","美食,辣味","3"
"42","今天晚上吃什么?","烧烤","用户A","户外","5"
"42","今天晚上吃什么?","沙拉","用户B","健康","1"
3.3 链接预览(Open Graph 提取)
POST /link-preview
请求:
curl -X POST http://localhost:4000/link-preview \
-H "Content-Type: application/json" \
-d '{"url": "https://www.quzuo.net"}'
响应:
{
"url": "https://www.quzuo.net",
"type": "website",
"title": "Quzuo.net - 自托管技术文档库",
"description": "精选自托管技术组件实战指南",
"image": "https://www.quzuo.net/og-image.png",
"site_name": "Quzuo"
}
💡 注意:此端点服务端爬取目标 URL 的
<meta property="og:*">标签——可能受网络超时、目标站点爬虫策略影响。对于恶意网站,也可能触发 SSRF 风险。
3.4 文件附件上传(需要 LiveView session)
POST /brainstorming/:brainstorming_id/idea/:idea_id/file-upload
curl -X POST \
-F "file=@/path/to/attachment.pdf" \
http://localhost:4000/brainstorming/brainstorming_abc123/idea/7/file-upload
响应:
{
"id": 15,
"idea_id": 7,
"content_type": "application/pdf",
"filename": "attachment.pdf",
"size": 1048576,
"url": "/file/15"
}
⚠️ 重要:上传端点需要合法的 LiveView session cookie(
_mindwendel_key)。如果直接 curl 不带 cookie,会返回 403。这是因为文件上传在创建时关联到具体头脑风暴和想法,服务端需要确认会话合法性。
3.5 文件下载(加密解密后返回)
GET /file/:id
curl -O http://localhost:4000/file/15
下载流程:
- 浏览器请求
/file/15 FileController从数据库查询 file 记录- 通过 S3 客户端从 rustfs/MinIO 获取密文
Mindwendel.Services.Vault(Cloak AES-GCM)解密- 返回明文文件内容 + 正确的 Content-Type / Content-Disposition
四、Phoenix LiveView WebSocket 协议
这是 Mindwendel 实时协作的核心通道。所有头脑风暴页面的交互(创建想法、投票、评论、标签、泳道)都通过 WebSocket 帧传输。
4.1 连接建立流程
客户端 JS (phx-js) Phoenix Server
│ │
│ 1. HTTP GET /brainstorming/:id │
│ ─────────────────────────────────▶│
│ │ 生成 LiveView 状态快照
│ 2. HTTP 200 OK + HTML 页面 │
│ ◀─────────────────────────────────│
│ (含 data-phx-session token) │
│ │
│ 3. WS handshake │
│ Upgrade: websocket │
│ ─────────────────────────────────▶│
│ │
│ 4. WS 101 Switching Protocols │
│ ◀─────────────────────────────────│
│ │
│ 5. HELLO frame (phx-js 协议) │
│ ─────────────────────────────────▶│
│ │ 创建 LiveView 进程树
│ 6. PHX:CONNECT frame │
│ ◀─────────────────────────────────│
│ │
│ 7. LIVEVIEW mount 事件 │
│ ─────────────────────────────────▶│
│ │ 查询数据库 + pub/sub 订阅
│ 8. DIFF (DOM 差分推送) │
│ ◀─────────────────────────────────│
│ │
│ 9. 进入正常交互阶段 │
4.2 WebSocket 端点
| 端点 | 用途 | 鉴权 |
|---|---|---|
ws://host:port/live | LiveView 主通道 | 通过 data-phx-session token 自动携带 |
ws://host:port/phoenix/live_reload | 开发时热重载 | 生产环境禁用 |
4.3 协议帧结构
Phoenix LiveView 使用 phx-js 二进制协议(v0.2.17+ 默认),帧格式为带长度前缀的二进制帧。以下是帧类型定义:
| 帧类型 | 方向 | 格式前缀 | 说明 |
|---|---|---|---|
phx_join | 客户端→服务端 | "0,omitempty" | 加入 LiveView topic |
phx_leave | 客户端→服务端 | "1,omitempty" | 离开 topic |
phx_event | 客户端→服务端 | "2,omitempty" | 发送事件(如按钮点击、表单提交) |
phx_reply | 服务端→客户端 | "3,omitempty" | 事件回复 |
phx_ping | 客户端→服务端 | "4,omitempty" | 心跳 |
phx_close | 双向 | "5,omitempty" | 关闭连接 |
phx_error | 服务端→客户端 | "6,omitempty" | 连接错误 |
event (业务) | 双向 | "7,omitempty" | 业务事件(如 DIFF、心跳) |
4.4 典型帧序列示例
创建想法(Idea)事件流
1. 客户端 → 服务端 (phx_event)
{
"id": "idea_create_123",
"event": "idea_created",
"topic": "brainstorming:brainstorming_abc123",
"payload": {
"idea": {
"body": "火锅",
"color": "red",
"username": "测试用户"
}
},
"ref": "E-1234567"
}
2. 服务端处理 handle_event("idea_created", payload, socket)
→ Mindwendel.Ideas.create_idea(payload.idea)
→ 写入 PostgreSQL
→ Phoenix.PubSub.broadcast("brainstorming:abc123", {:idea_created, idea})
3. 服务端 → 客户端 (phx_reply + 广播给所有连接)
{
"id": "idea_create_123",
"topic": "brainstorming:brainstorming_abc123",
"event": "phx_reply",
"payload": {"response": {}},
"ref": "E-1234567"
}
4. 服务端 → 所有连接的客户端 (diff 推送)
{
"event": "live_diff",
"diff": [
{"op": "append", "selector": "#ideas-container", "html": "<div class='idea-card'>...</div>"}
]
}
投票(Upvote)事件流
1. 客户端 → 服务端
{
"event": "upvote",
"topic": "brainstorming:abc123",
"payload": {"idea_id": 7},
"ref": "E-7890123"
}
2. 服务端 Mindwendel.Ideas.upvote_idea(7)
→ INSERT user_id + idea_id 到 upvotes 表
→ update ideas.likes_count = likes_count + 1
→ PubSub 广播
3. 服务端 → 客户端 A (diff)
{
"diff": [
{"op": "replace", "selector": "#idea-7-likes", "html": "<span class='likes'>4</span>"}
]
}
4. 服务端 → 客户端 B、C(其他用户也实时看到投票变化)
{
"diff": [
{"op": "replace", "selector": "#idea-7-likes", "html": "<span class='likes'>4</span>"}
]
}
4.5 心跳与保活
客户端每隔 ~30 秒发送:
{"event": "phx_ping", "ref": "heartbeat-1"}
服务端立即回复:
{"event": "phx_reply", "ref": "heartbeat-1", "payload": {"response": {}}}
超时检测:如果服务端在 ~60 秒内未收到任何帧,判定连接断开,触发 LiveView 的 terminate/2 回调。
五、Phoenix.PubSub 广播机制
Mindwendel 的实时同步完全依赖 Phoenix.PubSub——它是一个进程本地 pub/sub 系统,同一 OTP 节点内的所有 LiveView 进程可以通过 Phoenix.PubSub.broadcast 发送事件。
PubSub 订阅主题
| 主题格式 | 事件类型 | 订阅者 |
|---|---|---|
brainstorming:<url_token> | idea_created, idea_updated, idea_deleted, idea_upvoted, label_assigned, lane_moved, comment_added | 头脑风暴 LiveView 进程 |
brainstorming:<url_token>:presence | user_joined, user_left, user_cursor_moved | Presence Tracker(用户在线状态) |
典型的事件广播代码逻辑(概念化)
# Mindwendel.Ideas.create_idea/1 的实现逻辑
def create_idea(attrs) do
%Idea{}
|> Idea.changeset(attrs)
|> Repo.insert()
|> case do
{:ok, idea} ->
Phoenix.PubSub.broadcast(
Mindwendel.PubSub,
"brainstorming:#{idea.brainstorming_url_token}",
{:idea_created, idea}
)
{:ok, idea}
error -> error
end
end
多节点部署的 PubSub 问题
Mindwendel 默认使用 Phoenix.PubSub 的本地实现——只在单个 BEAM 节点内广播事件。如果部署多个 Mindwendel 实例(不同的 OTP 节点),会出现:
- 用户 A 连接到节点 1,创建了想法 → 节点 1 的 PubSub 广播
- 用户 B 连接到节点 2 → 节点 2 收不到节点 1 的 PubSub 消息
- 结果:用户 B 看不到用户 A 创建的想法,反之亦然
解决方案(任选其一):
- Phoenix.PubSub Redis Adapter:引入 Redis 做跨节点 PubSub
- libcluster + Phoenix Tracker:启用 BEAM 集群,PubSub 自动跨节点
- 单节点部署:简化架构,容忍单点故障(绝大多数场景足够)
六、二次开发集成指南
6.1 场景 1:程序化创建头脑风暴
最简单的集成方式——用 REST AJAX 端点:
import requests
response = requests.post(
"http://mindwendel.example.com/brainstorming/new-ajax",
json={
"brainstorming": {
"challenge": "2026 Q1 产品方向头脑风暴",
"username": "自动化创建器",
"language": "en"
}
},
timeout=10
)
result = response.json()
print(f"创建成功: id={result['id']}, url_token={result['url_token']}")
# 用户访问: http://mindwendel.example.com{result['path']}
6.2 场景 2:程序化监控头脑风暴变化
Mindwendel 没有事件流 API。可行方案:
| 方案 | 复杂度 | 说明 |
|---|---|---|
| 轮询导出接口 | 低 | 定期 GET /brainstorming/:id/export?format=csv,解析 CSV 提取变化 |
| 监控数据库 | 中 | Postgres LISTEN/NOTIFY 或 CDC(Debezium)监听 ideas/upvotes 表变更 |
| 模拟 LiveView 协议 | 高 | 完整实现 phx-js 协议 + LiveView mount + PubSub 订阅,工作量大 |
| 源码改造 | 高 | 在 Mindwendel 代码中加 Webhook / Webhook Controller |
轮询示例:
import requests, time, hashlib
last_hash = None
brainstorming_id = "brainstorming_abc123"
while True:
response = requests.get(
f"http://mindwendel.example.com/brainstorming/{brainstorming_id}/export?format=csv"
)
current_hash = hashlib.md5(response.content).hexdigest()
if current_hash != last_hash:
print(f"[变化检测] CSV 内容 hash 变更: {last_hash} → {current_hash}")
# 解析 CSV,对比变化
last_hash = current_hash
time.sleep(10) # 10 秒轮询间隔
6.3 场景 3:模拟 LiveView WebSocket 协议
Phoenix LiveView 的协议实现参考开源库 phoenix-channel、phoenix-liveview-protocol 或现成的 JavaScript phx-js 客户端。协议握手步骤:
import websockets, json, time
async def connect_mindwendel(url_token):
# 1. 先访问 HTTP 页面获取 session token
async with websockets.connect(
f"ws://localhost:4000/live",
subprotocols=["phx-js"]
) as ws:
# 2. 发送 HELLO
await ws.send(json.dumps({
"v": "2.17.3",
"cvs": {},
"sig": None
}))
# 3. 收到 CONNECT 后发送 phx_join
await ws.send(json.dumps({
"join": "brainstorming:" + url_token,
"topic": "brainstorming:" + url_token,
"ref": "1",
"presence": true
}))
# 4. 接收事件
while True:
frame = json.loads(await ws.recv())
print(f"收到帧: {frame}")
if frame.get("event") == "phx_ping":
await ws.send(json.dumps({
"topic": frame["topic"],
"event": "phx_reply",
"ref": frame["ref"],
"payload": {"response": {}}
}))
⚠️ 重要说明:上述代码仅为原理演示,实际 phx-js 协议包含更多细节(会话加密、差分算法、心跳超时、重连逻辑等),完整实现建议参考官方
phx.jsnpm 包或社区现成客户端库。
七、故障排查
WebSocket 连接问题
| 症状 | 排查方向 |
|---|---|
| HTTP 200 但 LiveView 不挂载 | 检查浏览器控制台 phx-js 初始化错误;确认 URL_HOST 配置正确 |
| WS 握手失败(101 非预期) | 检查反代 Upgrade / Connection header |
| 连接频繁断开 | 检查反代 proxy_read_timeout ≥ 86400s;检查防火墙会话超时 |
| 只有自己能看到变化 | PubSub 跨节点问题(§5);确认是单节点部署 |
REST 端点返回 403
可能原因:
/brainstorming/new-ajax需要合法 CSRF token(如果在同一浏览器页面发起)- 文件上传端点需要 LiveView session cookie
- 反向代理配置了 IP 白名单
总结
Mindwendel 的通信模型以 Phoenix LiveView WebSocket 为核心,REST 端点为辅助。这意味着:
- 前端:完全依赖 LiveView,无法被普通 HTTP 客户端替代
- 后端集成:REST 端点只覆盖创建、导出、预览、上传等少量场景
- 实时同步:通过 Phoenix.PubSub 本地广播实现,单节点足够,多节点需额外配置
- 程序化接入:创建/导出用 REST,监控/自动化用数据库轮询或源码改造
对于典型自托管场景(单节点、小型团队),这种架构简单高效;对于需要大规模集成或多节点高可用的场景,建议在 Mindwendel 源码基础上扩展专用 API 层。