README.md
August 11, 2026 · View on GitHub
🌐 其他语言 / Other Languages: English · 日本語 · 한국어 · Français · Deutsch · Русский · Español
一款微信风格的端对端加密即时通讯应用,采用无状态 ECDH + XSalsa20-Poly1305 逐消息加密,支持 iOS PWA 永久免签与 Cloudflare R2 文件存储。
📸 截图预览(点击展开)
特性
| 功能 | 说明 |
|---|---|
| 🔐 端对端加密 | 无状态 ECDH + XSalsa20-Poly1305,逐消息临时密钥,前向保密,Signal 风格安全号码验证 |
| 🗝️ 零知识服务器 | 服务器只存储密文,私钥仅在设备本地(四层持久化) |
| 📹 视频/语音通话 | 1:1 私聊和群会议统一使用 LiveKit SFU(最多100人),主席全员静音、讲课模式 |
| 🎙️ 变声功能 | 语音消息 / 1v1 通话 / 群组通话均支持实时变声,3 档可选(0.8x 低沉 / 1.0x 正常 / 1.2x 尖锐),基于 Web Audio API 音频处理链 |
| 📱 会话保持 | 30 分钟 Access Token + 90 天设备 Refresh Token 自动续期;网络/IP/VPN/代理变化时自动重建连接,仅在设备会话被撤销或长期凭证失效时要求重新登录 |
| 📨 可靠消息同步 | WebSocket 双向心跳与半死连接检测、持久化发件箱、客户端消息 ID 幂等、服务端序号增量同步;即使通知到达但实时连接丢包,重连后也会自动补齐 |
| 📴 离线访问 | 按账户隔离缓存联系人、群组、每会话最多 2000 条聊天记录、朋友圈、时间线及媒体;离线发送进入本地队列,联网后自动重试;可在个人资料中手动清理 |
| 🔎 Unicode 好友搜索 | 中文输入法组合状态保护、NFC 归一化与 UTF-8 参数编码,支持可靠搜索中文用户名和昵称 |
| 👥 群聊 | 最多 2000 人群组,支持「加密」与「未加密」两种模式(群主可切换,切换清空历史消息)。加密模式采用 Signal 风格 Sender Key 协议(XSalsa20-Poly1305 对称加密 + ECDH 密钥分发),仅群成员可解密消息;加密模式下无法使用群机器人。免打扰模式,成员管理 |
| 👫 好友系统 | 添加好友需对方审核,支持 512 字验证消息;备注名称;好友标签分组 |
| ⏱️ 消息自动删除 | 5 档可选(永不/1天/3天/1周/1月),私聊双方均可设置,群聊群主专属 |
| 🔔 消息推送 | Web Push (VAPID) + FCM + OneSignal + ntfy + APNS 五通道,离线也能收到通知(iOS 原生 + 国产安卓免 Google 服务) |
| 🌐 多语言 | 中文、英文、日语、韩语、法语、德语、俄语、西班牙语(自动检测 + 手动切换) |
| 📱 iOS 永久免签 | PWA H5 → Safari「添加到主屏幕」,无需企业证书 |
| 📱 Android 原生 App | 已上架 Google Play,支持 FCM 推送通知 |
| 📱 iOS 原生 App | 已上架 App Store,支持 APNS 推送通知 |
| 🖥️ Windows 桌面客户端 | 原生 Windows 桌面应用,点击下载 |
| 🍎 Mac 桌面客户端 | 原生 Mac 桌面应用,点击下载 |
| 💬 消息功能 | 文字、图片、视频、文档文件(PDF/DOCX/XLSX 等带类型图标)、语音消息、Emoji 面板(200+,8 分类)、Telegram 贴纸包、已读状态 |
| 📤 文件上传 | 单文件最大 500MB,支持 Cloudflare R2 或本地存储,带进度条动画 |
| 🌐 朋友圈 | 发动态(文字+最多9张图或1个视频≤10分钟)、点赞(显示好友头像)、评论、标签可见性控制 |
| 👤 个人资料 | 联系人资料页(头像/昵称/朋友圈动态),支持「不看此人朋友圈」与「不让他看我的朋友圈」双向隐私控制 |
| 📰 时间线 | 小红书风格公开发帖区——双列瀑布流布局,图片/视频+文字(最多50个媒体、2000字),支持匿名发帖、点赞、评论 |
| 🏷️ 好友标签 | 为好友设置多个标签(12色预设调色板),按标签分类筛选通讯录 |
| 🗂️ R2 对象存储 | Cloudflare R2 存储图片/语音,可选公开 CDN 直链 |
| 🔑 两步验证 (2FA) | Google Authenticator 兼容 TOTP 验证,8 个一次性恢复码,登录时强制验证 |
| 📷 扫码加好友/入群 | 扫一扫二维码添加好友、加入群聊,群二维码可设置有效期(1 周/1 月/3 月) |
| 🏗️ 可自托管 | Docker Compose 一键部署,Zeabur 一键云部署,前端可部署至 Vercel |
| 🌐 代理设置 | 支持 SOCKS5 / HTTP / HTTPS 代理协议,可在登录页和设置页配置代理服务器地址、端口、用户名和密码,方便受限网络环境下使用 |
| 🛡️ 内容审核 | 用户举报(6 类原因)+ 拉黑用户(即时屏蔽动态/消息)+ 使用条款 EULA |
| 🔧 管理后台 | 内嵌 Web 管理面板(/admin,路径可自定义),密码保护,审核举报、删除违规内容、封禁用户,支持 8 种语言 |
v2.3.9 更新
- 修复历史单向好友记录导致“已是好友”但联系人列表不可见、无法聊天的问题;再次添加时会自动补齐双向关系并立即刷新好友列表。
v2.3.8 更新
- 修复扫码启动摄像头后左上角返回按钮无法退出的问题,退出时会立即停止并释放摄像头。
- 修复向已有好友重复发送添加请求会破坏好友关系的问题;搜索结果现在会明确标记“已是好友”。
- 私聊消息完成端到端加密后立即把密文写入发送中的乐观消息对象,避免等待服务器确认期间将明文短暂持久化到离线缓存。
- 语音消息最长 120 秒,到达上限后自动停止;变声处理后的音频同样限制为 120 秒。
- 录制语音和通话期间保持屏幕唤醒,离开页面时可靠释放录音设备与计时器。
- Android 原生版另使用 Android Keystore 与 AES-256-GCM 保护密钥和聊天缓存;Web 版继续使用浏览器存储模型。
会话恢复与消息可靠性
PaperPhonePlus 将“本地账号状态”“实时连接状态”和“消息同步状态”分开处理。WebSocket 打开并不代表可用;客户端只有收到服务端 auth_ok 后才进入已连接状态,并通过双向 ping/pong 检测 VPN、IP、Wi-Fi/蜂窝切换或系统休眠产生的半死连接。
- Access Token 有效期为 30 分钟,设备 Refresh Token 有效期为 90 天,并在活跃使用时续期。普通 Token 过期会静默刷新,不要求用户重新输入密码。
- 老版本已经登录的设备会在 Token 仍有效时自动升级为新会话。若旧 Token 在升级前已经过期,需要重新登录一次,之后即可使用自动续期。
- 每条消息由客户端生成稳定的
client_msg_id。未收到服务端 ACK 的消息保存在本地发件箱,网络恢复后用同一 ID 自动重试,服务端通过唯一约束防止重复入库。 - 每条服务端消息具有单调递增的
server_seq。客户端在登录、重连和回到前台后按游标增量同步,因此 APNS/FCM 通知与 WebSocket 实时投递不一致时仍能补齐正文。 - 用户主动退出、管理员撤销设备或账号失效时,服务端设备会话会被撤销;普通断网和 IP 变化不会清除登录状态。
Important
升级时必须先部署 server,再发布新版客户端。server 启动会自动新增可靠同步所需的数据库字段并验证迁移结果;迁移不完整时会拒绝启动,避免出现“能连接但消息无法写入”的带病状态。生产升级前请备份 MySQL。
技术栈
后端 (server/)
Rust (Axum 0.8) — 高性能异步 Web 框架
sqlx + MySQL 8.0 — 用户/消息持久化
deadpool-redis + Redis 7 — 在线状态 + 跨节点路由
aws-sdk-s3 — Cloudflare R2 文件存储(S3 兼容 API)
argon2 + jsonwebtoken 认证
前端 (client/)
React 19 + TypeScript + Vite 6
Zustand 状态管理
libsodium-wrappers-sumo (WebAssembly, Curve25519 / XSalsa20-Poly1305)
WebRTC API — 视频/语音通话
Web Audio API — 实时变声处理(ScriptProcessorNode 音频链)
PWA: manifest.json + Service Worker
加密层
无状态 ECDH + XSalsa20-Poly1305 — 逐消息临时 ECDH 密钥对,前向保密
私钥四层持久化: 内存 → localStorage → sessionStorage → IndexedDB
私钥全程存储在设备本地,从不上传服务器
📖 详细部署文档 / Deployment Guide — 包含 Zeabur + Vercel 混合部署、Docker Compose + Nginx 本地部署的完整步骤,以及各客户端服务器地址配置说明。
方式零:Zeabur 一键云部署
Zeabur 通话网络限制: 模板会部署 LiveKit,并通过 WebSocket/API 7880 与 ICE/TCP 7881 提供私聊及群会议连接。Zeabur 当前不支持 UDP 服务端口,因此可以使用 TCP 回退,但弱网延迟和画质可能下降。模板已预留 UDP 7882 配置;平台未来支持 UDP 后只需开放该端口。当前生产级通话建议使用 LiveKit Cloud,或将 LiveKit 部署到支持 UDP 的云主机。
服务端 Nginx 配置
仓库提供双域名生产配置:deploy/nginx/paperphone-plus.conf。将 api.example.com 和 meeting.example.com 替换为实际域名,申请对应 TLS 证书,然后复制到 /etc/nginx/sites-available/paperphone-plus,创建启用链接并执行 sudo nginx -t && sudo systemctl reload nginx。后端设置 LIVEKIT_URL=wss://meeting.example.com。Nginx 仅代理 API 与 WebSocket;LiveKit 的 TCP 7881、UDP 7882 必须在主机和云防火墙直接开放。
Tip
进阶方案:Zeabur + Vercel 混合部署 可以在 Zeabur 部署完成后,手动删除 Zeabur 上的 client 服务,改用 Vercel 部署前端(参见下方“方式二”)。 这样 server/MySQL/Redis 由 Zeabur 托管,前端由 Vercel CDN 加速,全球访问更快。 Vercel 上无需设置任何环境变量,用户在前端登录界面填写后端服务器地址即可连接。
已知注意事项:
- 首次启动 server 会自动创建数据库表(
CREATE TABLE IF NOT EXISTS),无需手动导入 schema - Redis 在集群内无需密码,已默认关闭认证
- 若需配置 MySQL root 密码,可在 server 服务的
DB_PASS里手动填写 MySQL 服务的MYSQL_ROOT_PASSWORD
方式一:Docker Compose(推荐)
# 克隆仓库
git clone <repo-url> && cd paperphone-plus
# 复制并编辑环境变量
cp server/.env.example server/.env
# 按需编辑:DB_PASS / JWT_SECRET / LIVEKIT_URL 等
# 一键启动(含前端、后端、MySQL、Redis)
docker compose up -d
# 查看服务状态
docker compose ps
# 访问
open http://localhost
注意:server 首次启动会自动初始化数据库 schema,无需手动导入 SQL 文件。
方式二:前端部署至 Vercel
# 1. Fork 本仓库
# 2. 在 Vercel 中导入前端项目
# - Root Directory: client/
# - Build Command: npm run build
# - Output Directory: dist/
# - 无需设置任何环境变量
# 3. 后端使用 Docker 部署或 Zeabur 部署
# 4. 打开 Vercel 部署的前端页面,在登录界面填写后端服务器地址即可
# 例如:https://your-server.zeabur.app
方式三:本地手动启动
1. 启动后端(Rust)
cd server
cp .env.example .env # 编辑环境变量
cargo run --release # → http://localhost:3000
2. 启动前端
cd client
npm install
npm run dev # → http://localhost:5173
所有实时音视频通话统一连接自建 LiveKit SFU。必须配置 LIVEKIT_URL、
LIVEKIT_API_KEY 和 LIVEKIT_API_SECRET;客户端不再使用 P2P、Cloudflare TURN
或 Metered TURN。生产环境应为 LiveKit 配置公网地址和可信 TLS,并开放
7881/tcp 与 7882/udp;复杂受限网络可额外启用 LiveKit 内置 TURN/TLS。
通话功能说明
| 类型 | 技术方案 | 适用场景 |
|---|---|---|
| 私聊 1:1 视频 | LiveKit SFU | 所有场景 |
| 私聊 1:1 语音 | LiveKit SFU | 所有场景 |
| 群组多人语音/视频 | LiveKit SFU(自适应订阅、动态编码) | 最多 100 人(上线前按带宽和并发开摄像头数压测) |
变声功能说明
语音消息、1v1 通话、群组通话均支持实时变声,3 档模式可选:
| 模式 | 倍速 | 效果说明 |
|---|---|---|
| 🐢 慢速 | 0.8x | 声音变低沉,适合匿名 |
| 🔊 正常 | 1.0x | 原声,无处理 |
| 🐇 快速 | 1.2x | 声音变尖锐,适合趣味聊天 |
技术实现:使用 Web Audio API 构建音频处理链(AudioContext → MediaStreamSource → ScriptProcessorNode → MediaStreamDestination),对麦克风采集的音频流进行实时音高/速度调整,处理后的音频流替换原始流发送给对方。
- 语音消息:录音时选择变声模式,发出的
.webm音频文件已包含变声效果,接收方无法还原原声,实现真正的匿名发送 - 1v1 / 群组通话:通话中点击变声按钮循环切换模式,通过
LiveKit LocalAudioTrack.replaceTrack()实时替换音频轨道
离线消息通知通过五通道推送,最大化消息送达率:
| 通道 | 适用场景 | 配置 |
|---|---|---|
| Web Push (VAPID) | 浏览器 (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID 密钥 |
| FCM (Firebase) | Capacitor 打包的原生 Android App | Firebase 服务账号 JSON |
| OneSignal | Median.co 打包的原生 Android/iOS App | OneSignal App ID + REST Key |
| ntfy | 国产安卓设备(华为/小米/OPPO/vivo 等无 Google 服务) | 无需配置(默认使用 ntfy.sh 公共服务) |
| APNS | Capacitor 打包的原生 iOS App | Apple .p8 Key 或 Push Relay |
配置 Web Push
- 生成 VAPID 密钥(仅需一次):
npx web-push generate-vapid-keys
- 填入
server/.env:
VAPID_PUBLIC_KEY=your_public_key_here
VAPID_PRIVATE_KEY=your_private_key_here
VAPID_SUBJECT=mailto:admin@your-domain.com
- 重启服务器,用户可在设置页开启通知
iOS 用户需先将应用「添加到主屏幕」,且仅 iOS 16.4+ 支持。
配置 OneSignal(Median.co 原生 App)
- 在 OneSignal Dashboard 创建 App 并配置 Firebase
- 在 Median.co 中启用 OneSignal 并填入 App ID
- 将 OneSignal 的 App ID 和 REST API Key 填入
server/.env:
ONESIGNAL_APP_ID=your_onesignal_app_id
ONESIGNAL_REST_KEY=your_onesignal_rest_api_key
配置 FCM(Capacitor 原生 Android App)
- 在 Firebase Console 创建项目并添加 Android 应用
- 进入 Project Settings → Service accounts → Generate new private key,下载 JSON 文件
- 从 JSON 文件中提取以下三个字段,填入
server/.env:
FCM_PROJECT_ID=your_firebase_project_id
FCM_CLIENT_EMAIL=firebase-adminsdk-xxx@your-project.iam.gserviceaccount.com
FCM_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvQ...base64...\n-----END PRIVATE KEY-----\n"
未配置时:推送功能静默禁用,不影响其他功能。
自建服务器用户:如果你没有 Firebase 凭据(使用别人发布的 Android App),可以通过 Push Relay 来实现 FCM 推送。
⚠️ FCM 私钥换行符问题
Firebase 服务账号 JSON 文件中的 private_key 字段包含 RSA 私钥,其 PEM 格式要求每 64 个字符有一个真实换行符(\n)。但在不同部署环境中,换行符的处理方式各不相同,这是 FCM 推送配置中最常见的失败原因。
问题本质:from_rsa_pem() 解析 PEM 格式时,要求私钥中的 \n 是真实的换行符(ASCII 0x0A),而不是字面上的两个字符 \ 和 n。如果环境变量中的 \n 被当作普通字符串存储,PEM 解析会静默失败,导致 FCM 推送完全不工作,且不会有任何报错日志。
服务端已做兼容处理:fcm.rs 会自动将字面量 \n 转换为真实换行符,因此以下两种格式都支持:
格式一:单行(推荐用于 Zeabur / Docker / CI 环境变量)
直接从 JSON 文件中复制 private_key 的值(保留 \n 转义符),粘贴为一行:
FCM_PRIVATE_KEY=-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQE...\n-----END PRIVATE KEY-----\n
服务端代码会自动将 \n 转换为真实换行符。
格式二:多行(适用于 .env 文件或支持多行的平台)
用引号包裹完整的 PEM 内容(每行一个真实换行):
FCM_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQE...
...base64 encoded content...
-----END PRIVATE KEY-----
"
各平台配置指南:
| 部署平台 | 推荐格式 | 注意事项 |
|---|---|---|
| Zeabur | 单行(\n 转义) | 在 Variables 面板直接粘贴 JSON 中的原始值即可 |
| Docker / docker-compose | 单行或多行均可 | YAML 多行用 | 语法;.env 文件建议用单行 |
| Vercel / Railway | 单行(\n 转义) | 环境变量输入框通常不支持真实多行 |
| Linux .env 文件 | 多行(引号包裹) | 确保引号闭合,注意 shell 转义 |
排查方法:如果配置了 FCM 环境变量但 Android 仍收不到推送,可检查服务端日志:
- 日志出现
[FCM] No access token available→ 私钥格式错误(换行符问题) - 日志出现
[FCM] ✅ Push sent to user xxx→ FCM 发送成功,问题在客户端 - 无任何 FCM 相关日志 →
FCM_PROJECT_ID未配置或fcm_tokens表中没有该用户的 token
配置 ntfy(国产安卓设备无 Google 服务)
对于华为、小米、OPPO、vivo 等无法使用 Google Mobile Services 的国产安卓设备,PaperPhonePlus 支持通过 ntfy 发送推送通知。
默认配置(零配置即可使用):使用 ntfy.sh 公共服务,无需任何额外配置。
可选配置(自建 ntfy 服务器时使用):
NTFY_BASE_URL=https://your-ntfy-server.com
NTFY_TOKEN=your_optional_auth_token
用户使用流程:
- 安装 ntfy App(Google Play / F-Droid / 直接下载)
- 在 PaperPhonePlus 设置页找到「ntfy 推送」卡片
- 复制显示的 topic 名称,在 ntfy App 中订阅
- 点击「注册推送」按钮完成注册
安全说明:ntfy 通知内容为明文(通知标题和摘要),不包含消息原文内容。如需更高安全性,可自建 ntfy 服务器。
配置 APNS(Capacitor 原生 iOS App)
APNS (Apple Push Notification Service) 用于向原生 iOS App 发送推送通知。有两种配置方式:
方式 A:直接配置(适用于 App 开发者自己的服务器)
- 登录 Apple Developer → Certificates, Identifiers & Profiles → Keys
- 点击 + 创建新 Key → 勾选 Apple Push Notifications service (APNs) → Register
- 下载
.p8文件(⚠️ 只能下载一次!),记录页面上的 Key ID - 在 Apple Developer 账号页面记录你的 Team ID(10 位字母数字)
- 填入
server/.env:
APNS_TEAM_ID=AB12CD34EF
APNS_KEY_ID=LH4Z9YN3P7
APNS_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIGTAgEA...(.p8 文件内容)...\n-----END PRIVATE KEY-----"
APNS_BUNDLE_ID=com.yourcompany.paperphoneplus
APNS_SANDBOX=false
APNS_SANDBOX:开发/TestFlight 构建设为true,App Store 生产包设为false。
方式 B:通过 Push Relay(适用于自建服务器用户)
如果你使用别人发布的 iOS App(如从 App Store 下载),你没有 App 开发者的 Apple 凭据,无法直接发送 APNS 推送。此时需要通过 Push Relay(推送中继) 来实现。
工作原理:
┌──────────────────────┐ ┌─────────────────────────┐ ┌─────────┐
│ 自建服务器 │ HTTP │ App 开发者的服务器 │ APNS │ Apple │
│ (无 Apple 凭据) │──────→│ (有 .p8 Key + Relay) │──────→│ ──→ 📱 │
│ │ │ │ └─────────┘
│ APNS_RELAY_URL=... │ │ APNS_TEAM_ID=... │
│ APNS_RELAY_KEY=... │ │ APNS_RELAY_SECRET=... │
└──────────────────────┘ └─────────────────────────┘
步骤 1:App 开发者启用 Relay 端点
App 开发者在自己的服务器上,除了配置 APNS 凭据外,还需设置一个 Relay 密钥:
# App 开发者的服务器 .env(已有 APNS_TEAM_ID 等凭据)
APNS_RELAY_SECRET=生成一个长随机字符串作为共享密钥
设置后,服务器会自动在 POST /api/push-relay/apns 开启推送中继端点。
步骤 2:自建用户配置 Relay
自建服务器用户只需设置两个变量,无需任何 Apple 凭据:
# 自建服务器 .env
APNS_RELAY_URL=https://app-developer-server.com
APNS_RELAY_KEY=与开发者约定的共享密钥
工作流程:
- 自建服务器收到离线消息 → 查询本地
apns_tokens表获取用户的 iOS 设备 token - 将设备 token + 推送标题/内容通过 HTTP POST 发送到 Relay
- Relay 验证密钥后,使用自己的 APNS 凭据发送到 Apple
- Relay 返回过期 token 列表,自建服务器自动清理本地数据库
优先级:本地 APNS 凭据 → Push Relay → 跳过(静默)。如果同时配置了本地 APNS 和 Relay,优先使用本地直连。
安全说明:Relay 仅传输推送通知标题和摘要(如「某某发来一条消息」),不包含消息原文。设备 token 本身无法用于读取用户数据。
Push Relay 推送中继
对于自建服务器用户,如果你使用别人发布的 App(如从 App Store/Google Play 下载),你没有 App 开发者的推送凭据(Apple .p8 Key / Firebase 服务账号 / OneSignal API Key),无法直接发送推送通知。
Push Relay 系统为 APNS、FCM、OneSignal 三个通道统一提供了推送中继能力:
┌──────────────────────┐ ┌─────────────────────────┐ ┌──────────────┐
│ 自建服务器 │ HTTP │ App 开发者的服务器 │ │ 推送服务 │
│ (无推送凭据) │──────→│ (有凭据 + Relay) │──────→│ Apple/Google │
│ │ │ │ │ OneSignal │
│ *_RELAY_URL=... │ │ *_RELAY_SECRET=... │ └──────────────┘
│ *_RELAY_KEY=... │ │ │
└──────────────────────┘ └─────────────────────────┘
App 开发者在自己的服务器上设置 Relay Secret 以启用中继端点:
# App 开发者的服务器 .env
APNS_RELAY_SECRET=一个长随机字符串
FCM_RELAY_SECRET=一个长随机字符串
ONESIGNAL_RELAY_SECRET=一个长随机字符串
自建用户只需配置指向 Relay 的 URL 和密钥,无需任何推送服务凭据:
# 自建服务器 .env
# APNS (iOS 原生推送)
APNS_RELAY_URL=https://app-developer-server.com
APNS_RELAY_KEY=与开发者约定的共享密钥
# FCM (Android 原生推送)
FCM_RELAY_URL=https://app-developer-server.com
FCM_RELAY_KEY=与开发者约定的共享密钥
# OneSignal (Median.co 打包的 App)
ONESIGNAL_RELAY_URL=https://app-developer-server.com
ONESIGNAL_RELAY_KEY=与开发者约定的共享密钥
优先级:本地凭据 → Push Relay → 跳过(静默)。如果同时配置了本地凭据和 Relay,优先使用本地直连。
安全说明:Relay 仅传输推送通知标题和摘要(如「某某发来一条消息」),不包含消息原文。
iOS 永久免签部署
- 部署到有 HTTPS 域名的服务器(WebRTC 和加密 API 需要 HTTPS)
- 用 Safari 打开
https://your.domain.com - 点击底部分享按钮 ⬆️
- 选择「添加到主屏幕」→「添加」
即可获得与原生 App 相同的体验,无需 Apple 企业证书,永久有效!
生产部署(Nginx)
server {
listen 443 ssl http2;
server_name your.domain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# 前端静态文件(或使用 Vercel 单独部署)
location / {
root /path/to/paperphoneplus/client/dist;
try_files $uri /index.html;
}
# API
location /api/ {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
}
# WebSocket 信令
location /ws {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
}
paperphoneplus/
├── docker-compose.yml
├── zeabur.yaml
├── server/ # Rust (Axum) 后端
│ ├── Cargo.toml
│ ├── Dockerfile
│ ├── .env.example
│ └── src/
│ ├── main.rs # Axum 应用入口
│ ├── config.rs # 环境变量管理
│ ├── db/
│ │ ├── mysql.rs # MySQL 连接池 (sqlx)
│ │ ├── redis.rs # Redis 连接池 (deadpool-redis)
│ │ └── schema.sql # 数据库 schema 与可靠性字段迁移(幂等)
│ ├── auth/
│ │ ├── jwt.rs # 短期 Access JWT 签名/验证(含 2FA pending token)
│ │ └── middleware.rs # Axum 鉴权中间件
│ ├── routes/
│ │ ├── auth.rs # 注册/登录(含 X3DH 公钥上传)
│ │ ├── users.rs # 用户搜索 / Prekey 下载
│ │ ├── friends.rs # 好友申请 / 接受
│ │ ├── groups.rs # 群组管理
│ │ ├── messages.rs # 消息历史分页与 server_seq 增量同步
│ │ ├── upload.rs # Cloudflare R2 文件上传
│ │ ├── files.rs # 文件代理(R2_PUBLIC_URL 未设时)
│ │ ├── moments.rs # 朋友圈(动态/点赞/评论/隐私控制)
│ │ ├── timeline.rs # 时间线(公开发帖/点赞/评论/匿名)
│ │ ├── calls.rs # LiveKit 私聊/会议令牌
│ │ ├── push.rs # 推送订阅管理
│ │ ├── push_relay.rs # APNS / FCM / OneSignal 推送中继端点
│ │ ├── stickers.rs # Telegram 贴纸包代理(缓存)
│ │ ├── totp.rs # TOTP 两步验证
│ │ ├── sessions.rs # 会话管理(多设备登录)
│ │ ├── tags.rs # 好友标签 CRUD
│ │ ├── report.rs # 内容举报
│ │ └── admin/ # 管理后台(内嵌 HTML SPA + API)
│ ├── services/
│ │ ├── push.rs # Web Push VAPID 服务
│ │ ├── fcm.rs # Firebase Cloud Messaging 服务(直连 + Relay)
│ │ ├── onesignal.rs # OneSignal REST API 服务(直连 + Relay)
│ │ ├── ntfy.rs # ntfy 推送服务(国产安卓)
│ │ └── apns.rs # APNS 推送服务(iOS 原生 + Relay)
│ └── ws/
│ └── server.rs # WebSocket 路由(消息/通话信令/已读/推送)
│
└── client/ # React + TypeScript + Vite 前端
├── package.json
├── Dockerfile
├── vercel.json # Vercel 部署配置(SPA 路由 + 安全头,无需环境变量)
├── vite.config.ts
├── tsconfig.json
├── index.html
└── src/
├── main.tsx # React 入口
├── App.tsx # 路由 + 鉴权守卫
├── index.css # Premium 设计系统(暗色/亮色,玻璃拟态)
├── vite-env.d.ts
├── store/
│ └── index.ts # Zustand 全局状态
├── api/
│ ├── http.ts # HTTP 客户端(自动刷新 Token 与请求重放)
│ ├── socket.ts # WebSocket 鉴权、双向心跳、重连与持久化发件箱
│ └── sync.ts # server_seq 游标增量同步
├── i18n/
│ ├── index.ts # 多语言引擎
│ └── locales/ # zh/en/ja/ko/fr/de/ru/es
├── crypto/
│ ├── ratchet.ts # ECDH + XSalsa20-Poly1305 加密
│ └── keystore.ts # 四层私钥持久化
├── hooks/
│ ├── useAuth.ts
│ ├── useI18n.ts
│ └── useSocket.ts
├── components/
│ └── TabBar.tsx # 底部导航栏
└── pages/
├── Login.tsx # 登录/注册(含密钥生成、2FA、语言切换)
├── Chats.tsx # 会话列表
├── Chat.tsx # 聊天窗口(消息气泡、输入框)
├── Contacts.tsx # 通讯录(好友/群组/好友请求)
├── Discover.tsx # 发现页
├── Profile.tsx # 设置(深色模式、语言、退出)
├── UserProfile.tsx # 联系人资料页
├── GroupInfo.tsx # 群信息
├── Moments.tsx # 朋友圈(图片网格、点赞、评论)
└── Timeline.tsx # 时间线(瀑布流布局)
数据库结构
共 19+ 张表,首次启动自动创建(CREATE TABLE IF NOT EXISTS):
| 表名 | 说明 |
|---|---|
users | 用户信息 + ECDH/OPK 公钥 |
prekeys | X3DH 一次性预密钥池 |
friends | 好友关系(pending/accepted/blocked) |
groups / group_members | 群组 + 成员(含免打扰状态) |
messages | 加密消息、客户端幂等 ID、全局同步序号与离线补偿 |
moments | 朋友圈动态(文字 ≤1024 字) |
moment_images | 动态图片(每条最多 9 张) |
moment_videos | 动态视频(封面图+时长,每条最多 1 个,≤10 分钟) |
moment_likes | 点赞(每用户每条唯一) |
moment_comments | 评论(最多 512 字/条) |
moment_visibility | 动态可见性规则 |
moment_privacy | 朋友圈用户级隐私设置(不看/不让看) |
push_subscriptions | Web Push 推送订阅(VAPID) |
fcm_tokens | FCM 设备令牌(Capacitor Android) |
onesignal_players | OneSignal 设备注册(Median.co) |
ntfy_subscriptions | ntfy 推送订阅(国产安卓设备) |
apns_tokens | APNS 设备令牌(Capacitor iOS) |
user_totp | TOTP 两步验证密钥与恢复码 |
sessions | 多设备会话、Refresh Token 哈希、长期凭证有效期与撤销状态 |
friend_tags / friend_tag_assignments | 好友标签系统 |
timeline_posts | 时间线帖子(文字 ≤2000 字,支持匿名) |
timeline_media | 时间线媒体(图片/视频,每帖最多 50 个) |
timeline_likes | 时间线点赞 |
timeline_comments | 时间线评论(支持匿名) |
group_invites | 群邀请链接(含有效期,用于二维码加群) |
reports | 用户举报记录(举报人、目标、原因、状态) |
user_blocks | 用户拉黑关系 |
安全模型
注册时:
设备本地生成 IK(身份密钥)+ SPK(签名预密钥)+ 20x OPK(一次性预密钥)
公钥上传服务器,私钥四层持久化,永不离开设备
发送消息时:
发送方下载接收方 IK 公钥
生成临时 ECDH 密钥对(每条消息独立)
X25519 ECDH → 共享秘密 → XSalsa20-Poly1305 加密
临时公钥附在消息 header 中,接收方解密后销毁
服务器所见:
✅ 密文 blob + 路由元数据(发件人/收件人 UUID)
❌ 明文 / 私钥 / 临时密钥 / 通话内容
环境变量参考
| 变量 | 说明 | 默认值 |
|---|---|---|
PORT | 服务端口 | 3000 |
JWT_SECRET | JWT 签名密钥(生产必改) | dev_secret |
DB_HOST / DB_PASS / DB_NAME | MySQL 连接配置 | — |
REDIS_HOST / REDIS_PASS | Redis 连接配置 | — |
R2_ACCOUNT_ID | Cloudflare 账号 ID | — |
R2_ACCESS_KEY_ID | R2 API Token 的 Access Key | — |
R2_SECRET_ACCESS_KEY | R2 API Token 的 Secret Key | — |
R2_BUCKET | R2 Bucket 名称 | — |
R2_PUBLIC_URL | R2 公开 URL(可选),设置后文件走 CDN 直链 | — |
LIVEKIT_URL | 所有音视频通话使用的 LiveKit 公网 WebSocket 地址 | — |
LIVEKIT_API_KEY | 服务端与 LiveKit 共享的 API Key | — |
LIVEKIT_API_SECRET | 服务端与 LiveKit 共享的 API Secret | — |
VAPID_PUBLIC_KEY | Web Push VAPID 公钥(可选) | — |
VAPID_PRIVATE_KEY | Web Push VAPID 私钥(可选) | — |
VAPID_SUBJECT | VAPID 联系邮箱(可选) | mailto:admin@paperphoneplus.app |
FCM_PROJECT_ID | Firebase 项目 ID(可选,Capacitor Android) | — |
FCM_CLIENT_EMAIL | Firebase 服务账号邮箱(可选) | — |
FCM_PRIVATE_KEY | Firebase 服务账号私钥(可选,支持 \n 转义和真实换行两种格式,详见FCM 配置说明) | — |
FCM_RELAY_SECRET | FCM 推送中继密钥(可选,在 Relay 主机上设置以启用中继端点) | — |
FCM_RELAY_URL | FCM 推送中继 URL(可选,自建服务器指向 Relay 主机) | — |
FCM_RELAY_KEY | FCM 推送中继认证密钥(可选,与 Relay 主机的 FCM_RELAY_SECRET 一致) | — |
ONESIGNAL_APP_ID | OneSignal App ID(可选,Median.co) | — |
ONESIGNAL_REST_KEY | OneSignal REST API Key(可选) | — |
ONESIGNAL_RELAY_SECRET | OneSignal 推送中继密钥(可选,在 Relay 主机上设置以启用中继端点) | — |
ONESIGNAL_RELAY_URL | OneSignal 推送中继 URL(可选,自建服务器指向 Relay 主机) | — |
ONESIGNAL_RELAY_KEY | OneSignal 推送中继认证密钥(可选,与 Relay 主机的 ONESIGNAL_RELAY_SECRET 一致) | — |
NTFY_BASE_URL | ntfy 服务器地址(可选,默认使用 ntfy.sh 公共服务) | https://ntfy.sh |
NTFY_TOKEN | ntfy 认证 Token(可选,自建服务器时使用) | — |
APNS_TEAM_ID | Apple Developer Team ID(可选,iOS 原生推送) | — |
APNS_KEY_ID | APNS 认证密钥 ID(可选) | — |
APNS_PRIVATE_KEY | APNS .p8 私钥内容(可选,支持 \n 转义) | — |
APNS_BUNDLE_ID | iOS App Bundle Identifier(可选) | — |
APNS_SANDBOX | APNS 沙盒模式(可选,开发/TestFlight 用 true) | false |
APNS_RELAY_SECRET | 推送中继密钥(可选,在 Relay 主机上设置以启用中继端点) | — |
APNS_RELAY_URL | 推送中继 URL(可选,自建服务器指向 Relay 主机) | — |
APNS_RELAY_KEY | 推送中继认证密钥(可选,与 Relay 主机的 APNS_RELAY_SECRET 一致) | — |
TELEGRAM_BOT_TOKEN | Telegram Bot Token(可选,贴纸包代理) | — |
STICKER_PACKS | 自定义贴纸包列表(可选,逗号分隔 包名:显示名) | 内置 13 个默认包 |
ADMIN_PATH | 管理后台 URL 路径 | /admin |
ADMIN_PASSWORD | 管理后台访问密码(生产必改) | admin123 |
官方推送中继服务
自建服务器用户可使用以下官方推送中继,无需自行配置推送凭据即可让 iOS/Android 用户收到推送通知:
# 2026-05-18
APNS_RELAY_URL=https://619.chat
APNS_RELAY_KEY=EzmpqftbsENaRUO6BTABxLV96q7RuEDyokXJr1DWdDjL54cLg7yXVUQqydCQvxrX
FCM_RELAY_URL=https://619.chat
FCM_RELAY_KEY=EzmpqftbsENaRUO6BTABxLV96q7RuEDyokXJr1DWdDjL54cLg7yXVUQqydCQvxrX
ONESIGNAL_RELAY_URL=https://619.chat
ONESIGNAL_RELAY_KEY=EzmpqftbsENaRUO6BTABxLV96q7RuEDyokXJr1DWdDjL54cLg7yXVUQqydCQvxrX
将以上内容添加到自建服务器的 .env 文件中即可。
如果这个项目对你有用的话,请我喝罐可乐吧。
Telegram群组:https://t.me/+vHJtvWJY_gEyMTUx
开源协议
本项目基于 GNU Affero General Public License v3.0 (AGPL-3.0) 开源。
简而言之:
- ✅ 个人和企业均可自由部署和使用
- ✅ 允许修改代码
- ⚠️ 修改后通过网络提供服务时,必须公开修改后的源代码
- ⚠️ 衍生作品必须使用相同协议