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 示例)。

MindwendelMindwendelElixirPhoenix实时协作2026/10/27

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 + ChannelsMindwendel LiveView
CRUD 操作REST POST/PUT/DELETELiveView handle_event 内部调用 Ecto 操作
实时推送WebSocket 自定义 JSONLiveView 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/LiveViewStartLive.Index首页,创建/参与头脑风暴入口
GET/brainstorming/newLiveViewStartLive.New创建新头脑风暴
GET/brainstorming/:idLiveViewBrainstormingLive.Index头脑风暴主界面(核心协作页)
GET/adminLiveViewAdmin.Index管理员后台(需登录)

这四个 GET 路由返回完整 HTML 页面,页面内嵌的 <div data-phx-main="true"> 就是 LiveView 的挂载点,随后客户端 JS phx-js 建立 WebSocket 连接接管交互。

REST 端点(JSON 响应)

方法路径Controller请求格式响应格式说明
POST/brainstorming/new-ajaxBrainstormingControllerapplication/json201 {id, url_token}AJAX 方式创建头脑风暴,不需要 LiveView 上下文
GET/brainstorming/:id/exportBrainstormingControllerquery 参数: format=html|csv文件下载导出头脑风暴为 HTML 或 CSV
POST/link-previewLinkPreviewController{url: "https://..."}200 {title, description, image, ...}URL Open Graph 元数据提取
POST/brainstorming/:brainstorming_id/idea/:idea_id/file-uploadFileControllermultipart/form-data (file)201 {id, url}便签附件上传
GET/file/:idFileController—文件下载下载加密存储的附件(解密后返回)
DELETE/file/:idFileController—204 No Content删除附件

静态页面

方法路径Controller说明
GET/aboutStaticPageController关于页面
GET/privacyStaticPageController隐私声明(需 MW_FEATURE_LEGAL_PRIVACY_LINKS=true)
GET/imprintStaticPageController法律声明
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

下载流程:

  1. 浏览器请求 /file/15
  2. FileController 从数据库查询 file 记录
  3. 通过 S3 客户端从 rustfs/MinIO 获取密文
  4. Mindwendel.Services.Vault(Cloak AES-GCM)解密
  5. 返回明文文件内容 + 正确的 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/liveLiveView 主通道通过 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>:presenceuser_joined, user_left, user_cursor_movedPresence 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 创建的想法,反之亦然

解决方案(任选其一):

  1. Phoenix.PubSub Redis Adapter:引入 Redis 做跨节点 PubSub
  2. libcluster + Phoenix Tracker:启用 BEAM 集群,PubSub 自动跨节点
  3. 单节点部署:简化架构,容忍单点故障(绝大多数场景足够)

六、二次开发集成指南

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.js npm 包或社区现成客户端库。

七、故障排查

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 层。