服务器运维
服务器安装
使用 Docker Compose 在一台公网服务器上安装和运营 Pabal 服务器的方法。
本文介绍如何把 Pabal 服务器安装到一台公网服务器上。用 Docker Compose 启动 Pabal 服务器 · PostgreSQL · Caddy(HTTPS)三个容器,所有不能丢失的数据都绑定挂载到主机目录。从上到下依次照做即可。
域名 pabal.me,服务器公网 IP 203.0.113.10(用于说明的地址——阅读时请换成实际 IP),源代码位置 /opt/pabal,数据位置 /srv/pabal,操作系统 Ubuntu 24.04 LTS。所需文件在仓库的 deploy/ 文件夹中。
整体概览
图示说明
- 三个区域:互联网上的用户 → 生产服务器上的三个容器 → 主机磁盘上的绑定目录。容器随时可以删除重建,需要保留的数据全部在
/srv/pabal中。 - 入口只有两个:应用通过 MTProto 端口直接连接服务器(自带加密,因此不需要 HTTPS),网站、文档和 Bot API 则经过 Caddy 的 443。Caddy 会拦截管理用的路径(
/admin、/health…),只转发其余请求。 - 红色虚线是运营方专用的路径。管理页面只在服务器的
127.0.0.1:8080上开放,只能通过 SSH 隧道进入。PostgreSQL 则根本没有对外开放的端口。 - 两个红色方框是最重要的数据。
pabal_server/keys中的 RSA 密钥在构建时内置于应用中,一旦丢失就必须重新分发所有应用;postgres_data/中保存着所有账号和对话。它们是备份的第一优先级。 - 灰色箭头表示写入存储:服务器写入密钥、照片、设置和日志,PostgreSQL 写入事件,Caddy 写入证书,各自写到自己的目录中。
准备工作
| 项目 | 内容 |
|---|---|
| 服务器 | Ubuntu 24.04 LTS,起步配置 2 vCPU · 4 GB RAM · 40 GB SSD。用户和消息增多时,请优先增加内存 |
| 公网 IPv4 | 固定 IP。应用在构建时以 IP 形式内置服务器地址,服务器也以 IP 形式告知应用 |
| 域名 | 一个可以修改 DNS 的域名(A 记录) |
| 注册验证码的发送方式 | 一个短信(Twilio · Solapi · Webhook)或电子邮件(SMTP)账号 |
| Mac | 一台用于为这台服务器构建应用的 Mac(如果还要构建 Windows 应用,则另需一台 Windows PC) |
| Firebase 项目(可选) | 需要向 Android 应用发送推送通知时。用 Google 账号即可免费创建(第 10 步) |
端口
| 端口 | 使用者 | 开放范围 | 说明 |
|---|---|---|---|
| 22/tcp | 运营方 | 建议只对运营方 IP 开放 | SSH |
| 80/tcp | Caddy | 所有人 | 签发证书 · 跳转到 HTTPS |
| 443/tcp, 443/udp | Caddy | 所有人 | 网站 · 文档 · Bot API(udp 用于 HTTP/3) |
| 8443/tcp | 服务器 | 所有人 | 应用连接(MTProto) |
| 8080/tcp | 服务器 | 不开放 | 管理页面 · 健康检查——仅限服务器的 127.0.0.1 |
| 5432/tcp | PostgreSQL | 不开放 | 仅限容器内部网络 |
443 由网站(HTTPS)使用。MTProto 既不是 HTTP 也不是 TLS,无法在同一个端口上分流接收。如果因为公司或学校的网络会拦截不常见的端口,而希望应用也通过 443 连接,请再申请一个 IP,把那个 IP 的 443 交给服务器(在 .env 中把 PUBLIC_IP 设为第二个 IP、设置 MTPROTO_PORT=443,并在 Compose 文件的端口前加上 IP 加以区分)。服务器会告诉应用“连接这个 IP 的这个端口”,因此容器内外的应用端口要保持一致。
1. 准备服务器
sudo apt update && sudo apt -y upgrade
timedatectl # 确认显示 "System clock synchronized: yes"
时间同步是必需的。MTProto 的消息编号由时间生成,服务器时钟不准时,应用会反复重新连接。如果显示 no,请执行 sudo timedatectl set-ntp true。
# 防火墙
sudo ufw allow OpenSSH # 如果可以:sudo ufw allow from <运营方 IP> to any port 22
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw allow 8443/tcp
sudo ufw enable
# Docker(官方安装脚本)
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER # 重新登录后无需 sudo 即可使用 docker
docker compose version # v2 及以上
Docker 开放的(ports:)端口不受 ufw 规则的约束。因此 Compose 文件只开放需要公开的端口(80 · 443 · 8443),管理端口只以 127.0.0.1:8080 开放,PostgreSQL 则不开放。修改 ports: 时请记住这一点。
2. 获取源代码
sudo mkdir -p /opt/pabal && sudo chown $USER: /opt/pabal
git clone <仓库地址> /opt/pabal
如果从开发电脑复制,不要把密钥、数据和日志传过去(rsync --exclude 'keys/' --exclude 'data/' --exclude 'logs/' --exclude '**/target/')。生产服务器的 RSA 密钥会在生产服务器首次启动时生成——如果在生产环境中使用开发用的密钥,那台开发电脑就能解密生产流量。
3. 数据目录(绑定挂载)
sudo mkdir -p /srv/pabal/{postgres_data,pabal_server/keys,pabal_server/data,pabal_server/logs,caddy/data,caddy/config,backups}
sudo chown -R 1000:1000 /srv/pabal/pabal_server # 服务器容器以 uid 1000 运行
sudo chown -R $USER: /srv/pabal/backups
sudo chmod 700 /srv/pabal/pabal_server/keys /srv/pabal/backups
| 目录 | 容器内路径 | 存放的内容 | 丢失后 |
|---|---|---|---|
pabal_server/keys | /app/keys | private.pem(服务器 RSA 私钥)、private.pem.pub、(可选)firebase-service-account.json(Android 推送用的 Firebase 私钥) | 需要重新构建并分发所有应用(Firebase 密钥在控制台重新获取即可) |
pabal_server/data | /app/data | 照片(media/)、admin-token、operations.json(注册验证码、短信、SMTP 设置,包含机密值) | 照片、运营设置 |
pabal_server/logs | /app/logs | 服务器日志(每天压缩) | 只丢失日志 |
postgres_data | /var/lib/postgresql/data | 账号、对话、消息、登录、机器人、Webhook 设置等全部数据 | 整个服务 |
caddy/data, caddy/config | /data, /config | HTTPS 证书 | 会重新签发 |
backups | (仅限主机) | backup.sh 的输出 | — |
4. 配置文件 .env
cd /opt/pabal/deploy
cp .env.example .env
chmod 600 .env
openssl rand -base64 30 | tr -d '/+=' | cut -c1-32 # 把输出的值填入 POSTGRES_PASSWORD
nano .env
DOMAIN=pabal.me
PUBLIC_IP=203.0.113.10
POSTGRES_PASSWORD=(上面生成的值)
PABAL_HOME=/srv/pabal
PABAL_SOURCE=..
PABAL_NETWORK=ssemiya-net
MTPROTO_PORT=8443
HTTP_PORT=80
HTTPS_PORT=443
ADMIN_PORT=8080
ADMIN_TOKEN=
JAVA_OPTS="-Xms512m -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=100"
FCM_CREDENTIALS=
FCM_PROJECT_ID=
PUSH_PREVIEWS=true
| 键 | 填写内容 | 备注 |
|---|---|---|
DOMAIN | 网站域名 | 应用生成的链接(pabal.me/用户名)、文档中的地址、链接预览也都使用这个地址 |
PUBLIC_IP | 服务器的公网 IPv4 | 如果是云服务器,请填写控制台中显示的公网 IP(不是服务器内部的私有 IP) |
POSTGRES_PASSWORD | 随机值 | 请在首次启动之前设定。PostgreSQL 只在首次创建数据目录时使用这个值 |
PABAL_HOME | /srv/pabal | 第 3 步中的目录 |
PABAL_SOURCE | .. | 构建服务器镜像所用的代码仓库。Compose 文件在仓库的 deploy/ 中时为 ..,放在仓库外时填写源码路径 |
PABAL_NETWORK | ssemiya-net | 三个容器共同加入的 Docker 网络。需预先创建(第 6 步) |
MTPROTO_PORT | 8443 | 修改时,防火墙和应用构建也要一起修改 |
ADMIN_TOKEN | 留空 | 留空时,首次启动会在 pabal_server/data/admin-token 中生成。如果要自行设定,至少 16 个字符 |
JAVA_OPTS | 默认值 | 内存不足时请调高 -Xmx(最多约为服务器内存的一半) |
FCM_CREDENTIALS | 留空 | Android 推送通知。启用时填写 /app/keys/firebase-service-account.json(第 10 步)。留空则不带推送运行 |
FCM_PROJECT_ID | 留空 | 仅当要发送到密钥文件中 project_id 以外的 Firebase 项目时填写 |
PUSH_PREVIEWS | true | 设为 false 时,通知中不显示消息内容,只显示发送者 |
生产环境的 Compose 文件会在关闭测试号码的状态下(TELEGRAM_TEST_NUMBERS=false)启动服务器。服务器读取的全部设置项,请参阅管理与设置——环境变量。
5. DNS
| 名称 | 类型 | 值 |
|---|---|---|
pabal.me | A | 203.0.113.10 |
www.pabal.me | A | 203.0.113.10 |
dig +short pabal.me # 应该输出 203.0.113.10(生效需要几分钟到几小时)
在 DNS 指向这台服务器之前启动也没关系。Caddy 会自动重试,直到拿到证书为止。
6. 构建并启动
cd /opt/pabal/deploy
docker network inspect ssemiya-net >/dev/null 2>&1 || docker network create ssemiya-net # 只需一次
docker compose up -d --build # 第一次需要几分钟(Maven 会下载依赖库)
docker compose ps # pabal-server、pabal-postgres、pabal-caddy 为 running (healthy)
docker compose logs -f pabal-server # 按 Ctrl+C 退出
首次启动时,日志中会出现如下几行。
WARN ServerKeys - Generated a new RSA key at /app/keys/private.pem (fingerprint -3898654385185406269). Clients must embed /app/keys/private.pem.pub
INFO TelegramServer - JDBC persistence active (jdbc:postgresql://pabal-postgres:5432/pabal)
INFO BotFather - BotFather is user 100000
INFO AdminToken - Created the admin token in /app/data/admin-token
INFO TelegramServer - Website: http://0.0.0.0:8080/ (published at https://pabal.me/)
- 第一行虽然是 WARN,但属于正常情况(只是告知首次生成了密钥,只会出现一次)。之后会显示
Loaded RSA key … (fingerprint …)。fingerprint 因服务器而异。 - 数据表会在服务器启动时自动创建。
7. 验证
# 在外部(自己的电脑)执行
curl -sI https://pabal.me/ | head -1 # HTTP/2 200
curl -s -o /dev/null -w '%{http_code}\n' https://pabal.me/docs/ # 200 — 本文档
curl -s -o /dev/null -w '%{http_code}\n' https://pabal.me/admin/ # 404 — 管理页面不对外公开
curl -s https://pabal.me/docs/server-key.pem | head -1 # -----BEGIN RSA PUBLIC KEY-----
nc -vz 203.0.113.10 8443 # succeeded — 应用端口
# 在服务器上执行
curl -s http://127.0.0.1:8080/health # {"status":"UP",…}
ls -l /srv/pabal/pabal_server/keys # private.pem(600), private.pem.pub
8. 管理页面与初始设置
# 在自己的电脑上执行(保持运行)
ssh -N -L 8080:127.0.0.1:8080 <用户>@203.0.113.10
# 令牌在服务器上查看
sudo cat /srv/pabal/pabal_server/data/admin-token
在浏览器中打开 http://localhost:8080/admin/,输入令牌。正式上线前务必完成的设置(详见管理与设置):
- 把验证码发送方式(코드 전달 방식)设为短信或电子邮件。“管理页面”(관리 화면)方式需要运营方逐一告知验证码。
- 填入 SMS 或电子邮件(SMTP)(이메일 (SMTP))的设置值并保存 → 用测试发送(테스트 발송)确认确实能够收到。
- 确认测试号码(테스트 번호)已关闭。开启后,任何人都能用
+99966…号码登录。 - 如有需要,可以先关闭允许新注册(새 가입 허용)再开始,只让受邀的人先加入。
9. 让应用连接这台服务器
应用在构建时会内置服务器 IP · 端口 · 公钥。必须用这台服务器的公钥重新构建,应用才能连接这台服务器。在 Mac 上执行:
# 1. 这台服务器的公钥(是公钥而不是私钥——也可以从文档网站下载)
curl -s -o keys/production.pem.pub https://pabal.me/docs/server-key.pem
# 2. 让应用源代码指向这台服务器 + 应用品牌
python3 scripts/tdesktop/point_to_server.py ~/Developer/tdesktop \
--host 203.0.113.10 --port 8443 --key keys/production.pem.pub
python3 scripts/tdesktop/apply_branding.py ~/Developer/tdesktop
然后执行仓库中 docs/tdesktop-build-guide.md 的第 5 步(configure)和第 6 步(构建)。请把生成的 out/Debug/Pabal.app 以其他名称另行复制保存——用同一份源代码为另一台服务器重新构建时,它会被覆盖。
目前的构建是没有签名和公证的调试构建,收到的人第一次打开时需要右键 → 打开。调试构建会把数据存放在应用旁边的文件夹(tdata/)中,如果那里无法写入,就会使用与真正的 Telegram Desktop 相同的数据文件夹。请引导用户把应用放在用户文件夹中(例如 ~/Applications/Pabal/)。用于正式分发的发布构建(分离数据文件夹、Apple 签名和公证)是另外一项工作。
Android、iPhone、Windows 应用的做法相同(用脚本写入服务器地址和公钥 → 构建),见仓库中的 docs/mobile-apps-guide.md。要让 Android 应用带上推送通知,构建之前必须先完成第 10 步中的 Firebase 应用配置。
10. Android 推送通知(可选)
Android 很快就会让离开屏幕的应用进入休眠。如果不启用推送,只有在应用打开期间才会显示消息、响起来电。推送通过 Google 的 Firebase Cloud Messaging(FCM)发送(免费),通知内容(发送者名称、消息预览)由服务器用只有那部手机持有的密钥加密,因此 Google 无法读取。
图示说明
- 三个区域:运营方创建的 Firebase 项目 → 构建应用、运行服务器的地方 → 手机和 Google。从一个 Firebase 项目中获取两个文件。
- 灰色箭头是只需做一次的准备。绿色框中的
google-services.json会放进应用、随每个 APK 一起分发,所以不是机密;红色框中的服务账号密钥是只放在服务器keys/中的机密。两个文件必须来自同一个项目。 - 蓝色箭头是通知的传递路径:① 首次打开应用时,手机把 FCM 令牌和只有这部手机持有的密钥登记到服务器;② 有新消息或来电时,服务器用该密钥加密通知并交给 FCM;③ FCM 唤醒手机后,应用解密并显示通知,再重新连接服务器,取回错过的内容。
- Google 能看到的只有设备令牌、发送时间、优先级之类的传递信息。发送者名称和消息都是密文,Google 无法读取。
- 不会向正在查看应用的手机发送推送——它已经在通过连接接收了。已关闭通知的对话,其推送会以非紧急优先级发送,由应用自行过滤。
服务器的密钥只能向用同一个 Firebase 项目构建的应用发送推送。别人构建的应用(例如从 Pabal.me 下载的应用)绑定的是那个人的 Firebase 项目,而且本来就是用那个人服务器的地址和公钥构建的。你自己服务器的应用请自己构建,Firebase 项目也请自己创建。
创建 Firebase 项目
- 在 Firebase 控制台中点击添加项目。名称随意,不需要 Google Analytics。
- 项目设置 → 常规 → 您的应用 → 添加应用 → Android。Android 软件包名称填
me.pabal.android(应用构建脚本给应用起的名称)。推送不需要证书指纹(SHA-1)。 - 下载这个应用的
google-services.json。这是放进应用的配置,不是机密。 - 在项目设置 → 服务账号 → 生成新的私钥中下载 JSON 文件,并把文件名改为
firebase-service-account.json。这个文件是机密——不要把它的内容放进代码仓库、聊天或.env。 - 在项目设置 → Cloud Messaging 中确认 Firebase Cloud Messaging API(V1)已启用(新项目通常默认启用)。
把应用配置放进应用
构建 Android 应用之前,把 google-services.json 放到构建脚本查找的位置。脚本会先检查这个文件(是否是登记了 me.pabal.android 的项目、是否误放了私钥),然后把它放进应用源代码。
mkdir -p ~/.pabal/android
cp ~/Downloads/google-services.json ~/.pabal/android/ # 如要放在别处,给 apply_branding.py 加上 --google-services <路径>
python3 scripts/android/point_to_server.py <Android 源代码> \
--host 203.0.113.10 --port 8443 --key keys/production.pem.pub
python3 scripts/android/apply_branding.py <Android 源代码>
# 最后应当显示 "Firebase: project <项目 ID>"。如果看到 pabal-placeholder,构建出的应用就没有推送
之后按照仓库中 docs/mobile-apps-guide.md 的“Android 应用”一节进行构建。之前在用加入推送前的旧版应用的手机,也必须更新到新构建的应用才能收到通知。
把私钥放到服务器上
sudo cp firebase-service-account.json /srv/pabal/pabal_server/keys/
sudo chown 1000:1000 /srv/pabal/pabal_server/keys/firebase-service-account.json # 服务器容器中的用户
sudo chmod 600 /srv/pabal/pabal_server/keys/firebase-service-account.json
在 .env 中加入一行。在容器内,pabal_server/keys 就是 /app/keys。
FCM_CREDENTIALS=/app/keys/firebase-service-account.json
docker compose up -d pabal-server
docker compose logs pabal-server | grep -i push
INFO TelegramServer - Push notifications on: Firebase project <项目 ID>, 0 device registrations, message text included (encrypted per device)
- 看到这一行就说明已经启用。如果文件无法读取或格式不对,
Push notifications are off: …中会给出原因(文件内容不会出现在日志中)。 - 设为
PUSH_PREVIEWS=false时,这一行的末尾会变成message text left out,通知中不显示消息内容,只显示发送者。 - 容器必须能向
oauth2.googleapis.com、fcm.googleapis.com(443)发起出站连接。
确认
- 在手机上安装(或更新)新构建的应用,并打开一次。服务器日志中会出现
Push device registered: user … type 2 token … (push key 256 bytes)。令牌只以指纹的形式记录。 - 在最近使用的应用列表中把应用划掉。不要使用设置中的“强行停止”——在你自己再次打开应用之前,Android 不会把推送交给它。
- 用另一个账号发一条消息,日志中会出现
Push MESSAGE_TEXT to user …: sent,手机上会弹出通知。
- 隐私政策:在管理页面(관리 화면)→ 设置(설정)→ 运营方信息(운영자 정보)的委托处理(처리 위탁)和跨境传输(국외 이전)中填写 Google LLC(Firebase Cloud Messaging,美国)。政策正文中已经有经由 FCM 的说明。Google Play 的“数据安全”表单中也要填写相同内容。
- 如果密钥泄露,请在 Firebase 控制台中删除该密钥,重新获取并替换文件,然后重启服务器。无需重新构建应用。
iPhone 不用 Firebase,而是用 Apple 开发者账号的 APNs 密钥(.p8)另行启用——见紧接着的第 11 步。macOS、Windows 应用没有推送,在运行期间接收消息。
11. iPhone 推送通知(可选)
iPhone 同样会很快停止离开屏幕的应用。如果不启用推送,只有在应用打开期间才能收到消息和来电。推送通过 Apple 推送通知服务(APNs)发送,并且和 Android 一样,通知内容用只有那部 iPhone 持有的密钥加密。Apple 能看到的通知文字只有应用名称和“You have a new notification”这类不含内容的话,由 iPhone 应用中的通知扩展(Notification Service Extension)解密密文,换成发送者和消息显示出来。
- 登记:应用每次启动时,都会为每个账号登记两样东西——通知用(type 1)和来电用(VoIP,type 9)。服务器会随登记一起记住应用的语言(外层文字所用的语言)和构建号。
- 消息以通知发送。已关闭通知的对话不发送任何内容。在其他设备上已读或删除的内容,用静默的后台推送告知,由应用清除已显示的通知。
- 来电如果用 VoIP 发送,即使在锁屏状态下也会弹出 CallKit 来电界面。不过,收到 VoIP 的应用必须立即向 CallKit 报告来电;做不到的话 iOS 会终止应用,反复发生后就不再投递 VoIP。因此要用
APNS_VOIP_MIN_BUILD指定,只从按此要求构建的版本起启用 VoIP;在此之前,来电也以通知发送。
与 Firebase 不同,没有需要放进应用的配置文件——应用在构建时就已经能使用 APNs(通知权限、App Group、通知扩展),只需把密钥放到服务器上。不过,服务器的密钥只能向由创建该密钥的团队签名、且 Bundle ID 与 APNS_TOPIC 相同的应用发送推送。如果你用自己的 Apple 开发者账号为自己服务器的 iPhone 应用签名,APNs 密钥也要在该账号中创建。
创建 APNs 密钥
- Apple Developer → Certificates, IDs & Profiles → Keys → +。必须是为应用签名的团队的账号。
- 填写名称,勾选 Apple Push Notifications service (APNs)。环境选 Sandbox & Production,范围选整个团队(Team Scoped)——因为要发往通知和 VoIP 两处。
- Register → Download。
AuthKey_<密钥 ID>.p8只能下载一次。这个文件是机密——不要把它的内容放进代码仓库、聊天或.env。
把密钥放到服务器上
sudo cp AuthKey_<密钥 ID>.p8 /srv/pabal/pabal_server/keys/
sudo chown 1000:1000 /srv/pabal/pabal_server/keys/AuthKey_<密钥 ID>.p8 # 服务器容器中的用户
sudo chmod 600 /srv/pabal/pabal_server/keys/AuthKey_<密钥 ID>.p8
写入 .env。在容器内,pabal_server/keys 就是 /app/keys。
APNS_KEY=/app/keys/AuthKey_<密钥 ID>.p8
APNS_TEAM_ID=4Z8R468M2D
APNS_TOPIC=me.pabal.ios
APNS_MIN_BUILD=4
APNS_VOIP_MIN_BUILD=
| 键 | 填写内容 | 备注 |
|---|---|---|
APNS_KEY | 密钥文件的路径 | 密钥 ID 从文件名中读取。如果文件用了别的名称,请在 APNS_KEY_ID 中填写密钥 ID |
APNS_TEAM_ID | 为应用签名的团队 ID | Pabal 应用为 4Z8R468M2D。自己构建并签名的应用则填该团队 |
APNS_TOPIC | 应用的 Bundle ID | Pabal 应用为 me.pabal.ios。来电(VoIP)发往在此后加上 .voip 的主题 |
APNS_MIN_BUILD | 4 | 低于此值的构建不发送任何内容(0 表示全部发送)。4 用于排除在测试推送之前制作的 Pabal 1.0 (3) |
APNS_VOIP_MIN_BUILD | 留空 | 留空则来电也以通知发送。填写第一个会立即向 CallKit 报告来电的构建号后,从该构建起显示 CallKit 来电界面 |
docker compose up -d pabal-server
docker compose logs pabal-server | grep -i push
INFO TelegramServer - iPhone push notifications on: APNs key <密钥 ID> of team 4Z8R468M2D, topic me.pabal.ios, builds from 4, calls as alerts (TELEGRAM_APNS_VOIP_MIN_BUILD not set)
- 看到这一行就说明已经启用。如果密钥文件无法读取或格式不对,
iPhone push notifications are off: …中会给出原因(文件内容不会出现在日志中)。它与 Android(FCM)分别启用和关闭——只启用其中一个也可以。 - 容器必须能向
api.push.apple.com(443,HTTP/2)发起出站连接。用 Xcode 直接安装到设备上的调试构建会发往api.sandbox.push.apple.com(应用在登记时会告知)。
确认
- 在真实的 iPhone 上打开一次应用并允许通知。服务器日志中会出现
Push device registered: user … type 1 token … (push key 256 bytes, build …)和type 9的行。无法用模拟器确认——模拟器既没有 APNs 令牌,也没有 CallKit。 - 向上划动关闭应用。
- 用另一个账号发一条消息,日志中会出现
Push MESSAGE_TEXT to user …: sent,锁屏上会显示发送者和内容。
- 隐私政策:在管理页面(관리 화면)→ 设置(설정)→ 运营方信息(운영자 정보)的委托处理(처리 위탁)和跨境传输(국외 이전)中填写 Apple Inc.(Apple 推送通知服务,美国)。政策正文中已经有经由 APNs 的说明。在 App Store Connect 的 App 隐私问卷中,也要确认设备 ID(推送令牌)一项。
- 如果密钥泄露,请在开发者账号中 Revoke 该密钥,重新创建并替换文件,然后重启服务器。无需重新构建应用。
12. 语音机器人(可选)
语音机器人是指有人在应用中给机器人打电话时由 AI 语音模型接听的功能(面向机器人所有者的说明见语音机器人)。服务器还需要再接入两个组件,并且法律文件和应用版本都准备好之后才会开启。默认是关闭的。
语音组件不在 deploy/docker-compose.yml 中,而在附加文件 deploy/docker-compose.voice.yml 中——服务器的语音设置和链路专用网络、网关 pabal-voice(profile voice)、手表的音频入口 pabal-voice-edge(profile voice-watch)。.env 不引用这个文件时,就根本没有语音设置。这套配置已用回声引擎运行过,但还没有在生产服务器上开启过语音,也还没有通过生产环境的中继(TURN)完整地打通过一次语音机器人通话——端到端测试是用 Docker 网络内的测试中继完成的(scripts/voice-e2e/run.sh)。开启前可以用代码仓库中的 deploy/voice_readiness.py 检查缺少什么。正式开放前的条件(法律审查、提供商的书面答复、应用发布)由运营方判断,代码只拦住其中一部分。
| 组件 | 作用 | 缺少时 |
|---|---|---|
| pabal-voice(网关,镜像 0.11.0) | 代替机器人成为通话的一端,完成密钥交换,并与 AI 模型交换声音。Python 3.12 + ntgcalls。它与服务器通过链路(WebSocket,有认证但不加密)通信 | 所有机器人通话都是忙音(no_gateway) |
| coturn(通话中继) | 机器人通话只走中继:应用和网关都关闭直连,只通过中继传送声音。与人与人之间通话使用的中继相同(TURN_SECRET) | 所有机器人通话都是忙音(no_relay),网关的连接也会被拒绝(4008) |
| Vertex AI 密钥 | Google Cloud 服务账号 JSON 密钥。只有网关的引擎工作进程会打开它 | 只能运行不带模型的回声模式(VOICE_ENGINE=echo,声音不会离开本机) |
| (可选)主密钥 | 用来封装机器人所有者自有密钥和音频流模式机密值的密钥 | 自有密钥和音频流模式关闭 |
| (可选)MCP 组件 | pabal-mcp · 签名器 · 出站代理(见下文) | MCP 服务器关闭 |
开启条件
| 服务器检查的内容 | 缺少时 |
|---|---|
链路机密值文件(TELEGRAM_VOICE_LINK_SECRET_FILE,32 字节) | 语音关闭——链路端口也不会打开 |
通话中继(TELEGRAM_TURN_HOST·TELEGRAM_TURN_SECRET) | 所有机器人通话都是忙音 |
网站隐私政策(ko 和 en 两种)中的 id="bot-calls" 一节——应用的确认窗口会指向这里 | 任何机器人都不接听(ERROR 日志)。草稿在 docs/voice-bots/p0/privacy-*.draft.html |
服务条款中的 id="voice-bot-owners" 一节 | 谁都无法同意机器人所有者条款 → 文字记录、自有密钥、音频流模式和 MCP 都无法使用 |
政策中的 id="bot-calls-stream" · id="bot-calls-mcp" 两节 | 音频流模式 · MCP 关闭 |
| 在管理页面中允许机器人(默认仅限已允许的机器人)以及正式开放(默认仅限测试者,开放需要输入确认短语) | 未被允许的机器人不接听,正式开放前只有测试者可以拨打 |
来电者的应用版本:桌面版 pabal-bc2(在确认窗口只有韩语和英语期间也包括 pabal-bc1)· Android 62151 · iPhone 5 及以上 · Apple Watch 1 及以上(TELEGRAM_VOICE_MIN_BUILD_*) | 这个人看不到通话按钮,拨打也是忙音 |
(可选)Apple Watch:服务器的手表 API(TELEGRAM_WATCH_API_PORT)和音频地址(TELEGRAM_WATCH_AUDIO_URL)、音频入口 pabal-voice-edge、声明了 direct 的网关(见下文) | 不能用手表拨打(手机和电脑应用不受影响) |
(可选)保存对话:政策中的 id="bot-calls-keep-text" 一节、声明了 keep_text 的网关(VOICE_KEEP_TEXT,见下文) | 即使勾选了复选框,对话也不会保存 |
(可选)机器人打来的电话:政策中的 id="bot-calls-outbound" 一节和条款中的 id="voice-bot-owners-outbound" 一节、证据密钥文件夹、外呼最低版本、声明了 outbound 的网关、管理页面的全局开关(见下文) | 任何机器人都不能给人打电话 |
只有在测试服务器(TELEGRAM_TEST_NUMBERS=true)上,才能用 TELEGRAM_VOICE_NOTICE_CHECK=off 跳过法律条款的检查,并用 TELEGRAM_VOICE_BOTS(所有机器人用 *)·TELEGRAM_VOICE_TESTERS=* 让任何人都能测试。请不要在生产环境中使用——生产服务器会忽略 TELEGRAM_VOICE_BOTS 并给出警告。
通话中继(coturn)
- 如果是 Linux 服务器,就使用 Compose 中的
pabal-turn(COMPOSE_PROFILES=turn)。在.env中设置TURN_SECRET(openssl rand -hex 32),并在路由器和防火墙上开放TURN_PORT(3478,udp 和 tcp)以及TURN_MIN_PORT~TURN_MAX_PORT(49160~49200,udp)。扩大端口范围时,路由器和.env要一起修改。 - 不要在 macOS 的 Docker Desktop 上以容器方式运行 coturn。所有来电者都会显示为
192.168.65.1,中继许可对不上,表面上一切正常,声音却全部被丢弃。在 Mac 服务器上,请直接安装 coturn(deploy/turnserver.conf.example、docs/turn-relay-guide.md)。 - 交给网关的中继地址是服务器的局域网地址:在服务器的
TELEGRAM_VOICE_TURN_HOST和网关的VOICE_TURN_HOST中填入相同的值。用host.docker.internal连不上,用127.0.0.1分配无法完成。应用拿到的仍是公网地址(TURN_HOST)。 - 从外部网络用
deploy/check_relay.py --data确认声音确实能穿过中继。有时 STUN 有应答、分配也成功,声音却传不过去。
Vertex AI 密钥
- 在 Google Cloud 项目中启用 Agent Platform API(旧称 Vertex AI API,
aiplatform.googleapis.com)。必须关联结算账号。不使用 Gemini Developer API(AI Studio 密钥)。 - 创建一个服务账号,只授予
roles/aiplatform.user这一个角色(想进一步收窄,可以用只含aiplatform.endpoints.predict一项权限的自定义角色)。生产用的密钥要与开发用的分开创建。 - 下载 JSON 密钥,放在代码仓库和
.env之外,例如/srv/pabal/pabal_voice/secrets/vertex-sa.json,并且只让网关的用户可以读取(Linux:chown 10001:10001、chmod 400)。如果任何人都能读取,网关会拒绝启动。 - 实时模型只在
us-central1(美国艾奥瓦州)提供(2026-09-26 确认)。隐私政策和确认窗口必须说明通话声音会发往美国,而且只有当应用确认窗口上写的地区与网关的地区(VOICE_VERTEX_REGION)一致时,服务器才会把通话交给该网关。 - 待办:预算提醒、并发会话配额(网关容量的两倍),以及 Vertex 的数据缓存(开启时会把输入输出在内存中保留最长 24 小时)——确认窗口的说明是“Google 在生成回答后不保留声音和文字”,所以在关闭缓存之前,这句话并不属实。建议:在向所有人开放之前关闭数据缓存(代码仓库
docs/voice-bots/vertex-setup.md第 9 节),并把密钥的角色收窄到只有预测这一项权限(同一文档第 10 节)。
链路与服务器设置
服务器和网关从文件中读取同一个链路机密值。链路不加密,所以只能放在只有这两个容器接入的专用网络上——不要放在与其他服务栈共用的 ssemiya-net 或局域网上。不要公开端口。
(umask 077; openssl rand -hex 32 > /tmp/voice-link.secret)
sudo install -m 400 -o 1000 /tmp/voice-link.secret /srv/pabal/pabal_server/keys/voice-link.secret # 服务器(uid 1000)用
sudo install -d -m 700 -o 10001 /srv/pabal/pabal_voice/secrets
sudo install -m 400 -o 10001 /tmp/voice-link.secret /srv/pabal/pabal_voice/secrets/voice-link.secret # 网关(uid 10001)用
shred -u /tmp/voice-link.secret
这些值写在 .env 中。附加文件用它们为 pabal-server 加上语音设置(TELEGRAM_VOICE_*)和链路网络。
COMPOSE_FILE=docker-compose.yml:docker-compose.voice.yml
COMPOSE_PROFILES=voice # 若还有其他 profile(turn 等),用逗号并列
VOICE_LINK_SECRET_FILE=/app/keys/voice-link.secret # 容器内的路径;为空则语音关闭
VOICE_TURN_HOST=192.168.1.64 # 服务器的局域网地址(coturn 监听的地方)
VOICE_TESTERS=100001 # 正式开放前可以拨打的人的 id(管理页面也可添加)
- 附加文件把服务器接入内部专用网络
pabal-voice-link(10.231.53.0/29——服务器.2,网关.3),服务器只在这个网络上自己的地址监听(写0.0.0.0会在同时接入的共享网络上也开放)。如果这个范围与局域网、VPN 或其他 Docker 网络重叠,请把VOICE_LINK_SUBNET、VOICE_LINK_SERVER_IP、VOICE_LINK_GATEWAY_IP三个一起改。 - 要绑定到非回环地址,必须设置
TELEGRAM_VOICE_LINK_ANY_ADDRESS=true,而且链路绝不会绑定到公网地址。服务器日志会告诉你链路看到的是哪个网络。 - 认证之前的连接,升级限 3 秒、hello 限 3 秒,每个地址最多 2 个,总共最多 16 个连接。
- 与语音有关的全部服务器设置见管理与设置——语音机器人环境变量。
网关(pabal-voice)
cd /opt/pabal
docker build -t pabal-voice:latest voice/ # 适用于本机的 CPU
docker buildx build --platform linux/amd64 -t pabal-voice:latest --load voice/ # 适用于 Intel 服务器
cd deploy && docker compose up -d # 按 .env 中的 COMPOSE_FILE、COMPOSE_PROFILES 启动
镜像使用按哈希固定的 Python 3.12,不含编译器和 pip,以 uid 10001 运行。ntgcalls 只有 Linux x86_64/aarch64 和 macOS arm64 版本,所以在 Intel Mac 上只能用 Docker 运行。附加文件中的 pabal-voice 直接使用这个镜像而不构建它(image: pabal-voice:latest)。要在另一台电脑上为服务器的 CPU 构建并发送过去,请用 deploy/ship.sh <user@host> <目录> --voice(被替换的镜像保留为 pabal-voice:previous)。下面是该文件中网关服务的节选。
pabal-voice: # 摘自 deploy/docker-compose.voice.yml(节选)
image: pabal-voice:latest
profiles: [voice]
init: true
stop_grace_period: 40s # 要比 VOICE_DRAIN_SECONDS(20)长
security_opt: ["no-new-privileges:true"]
cap_drop: [ALL]
mem_limit: ${VOICE_MEMORY:-1g}
ulimits:
core: 0
environment:
VOICE_LINK_URL: ws://${VOICE_LINK_SERVER_IP:-10.231.53.2}:8095/voice/link
VOICE_LINK_SECRET_FILE: /run/secrets/voice-link.secret
VOICE_ENGINE: gemini
VOICE_VERTEX_KEY_FILE: /run/secrets/vertex-sa.json
VOICE_VERTEX_REGION: us-central1
VOICE_TURN_HOST: ${VOICE_TURN_HOST:-} # 与服务器的 TELEGRAM_VOICE_TURN_HOST 相同的值
VOICE_CAPACITY: ${VOICE_CAPACITY:-2}
VOICE_DAILY_MODEL_MINUTES: ${VOICE_DAILY_MODEL_MINUTES:-60}
VOICE_SAFETY_THRESHOLD: ${VOICE_SAFETY_THRESHOLD:-MEDIUM} # 测试者通话:需要运营方确认的假设(见下文)
VOICE_BYOK: "false"
VOICE_STREAM: "false"
volumes:
- ${PABAL_HOME:-/srv/pabal}/pabal_voice/secrets:/run/secrets:ro # 链路机密值和 Vertex 密钥——不在 backup.sh 中
- pabal_voice_state:/var/lib/pabal-voice
networks:
pabal-voice-link: # 只与服务器共用的链路网络(internal)
ipv4_address: ${VOICE_LINK_GATEWAY_IP:-10.231.53.3}
pabal-voice-out: {} # 通往外部(Google、中继)的自有网络
- 除了链路,网关还需要通往外部的路径:到 Google(
us-central1-aiplatform.googleapis.com:443)和通话中继(UDP)。链路网络是internal: true,无法访问外部,所以要像上面那样再接入一个pabal-voice-out网络。 - Landlock:使用模型或音频流模式的网关,如果没有 Linux 内核的 Landlock(5.13 及以上,Docker 默认 seccomp 下可用)就会拒绝启动(退出码 2)。这是因为用原生代码处理来电者所发内容的媒体工作进程会把自己锁住,使其无法读取密钥文件。在容器中,不要把机密文件挂载到
/app·/opt·/usr·/lib·/etc下面(/run/secrets可以——如上例所示)。VOICE_ALLOW_NO_LANDLOCK=true只用于开发用的 Mac。尚未确认 macOS 的 Docker Desktop 中能否使用 Landlock。 - 内存:每通电话需要媒体 60~100 MB + 引擎 30~40 MB。容量为 8(
VOICE_CAPACITY)再加一组备用时,约 1.3 GB。附加文件的默认值是容量 2、内存 1 GB(VOICE_MEMORY)。 - 安全过滤器
VOICE_SAFETY_THRESHOLD的网关默认值是LOW(最严格),但在测量中,它把 172 句正常的韩语话语中的 13 句变成了英语拒绝(MEDIUM为 69 句中 0 句)。代码仓库的假设是测试者通话要明确设置MEDIUM,修改默认值之前需要进行红队测试。 - 限额:网关的默认值是每通电话 600 秒、40 万 token、每分钟 30 轮,每天(UTC)模型 300 分钟、800 万 token;附加文件把每天设为 60 分钟、160 万 token(
VOICE_DAILY_MODEL_*,超过后新通话为忙音)。OpenAI 引擎只用假服务器测试过,处于关闭状态,引擎之间没有自动切换。 - 语言:网关接入服务器时会声明自己能用哪些机器人语言通话(从 0.7.0 起支持全部 9 种),服务器只把机器人的通话交给能用该机器人的语言通话的网关——不声明语言的旧网关(0.6.0)只会收到韩语和英语机器人的通话。提示录音位于镜像的
assets/中,哪些录音已审查完毕由assets/assets.json决定。缺少这个文件时,网关无法加载 AI 提示,也就无法接听通话(启动日志中会有 ERROR)。尚未审查完毕的录音(目前是韩语、英语以外的所有语言)不会播放,这些语言的来电者听到的是英语提示。VOICE_DRAFT_NOTICES=true是连草稿录音也播放的测试服务器专用开关,如果没有同时设置VOICE_ALLOW_TEST_URLS=true,网关不会启动。VOICE_NOTICE_LANG(服务器没有告知来电者语言时使用的提示语言,默认ko)必须是 10 种录音语言(ko、en、ja、zh、fr、es、id、vi、km、ar)之一,VOICE_LANGUAGE(没有机器人所有者设置的通话所用的机器人语言,默认ko-KR)必须是机器人语言列表中的 id(语音机器人——音色和语言)之一。 - 日志中不会留下链路机密值、密钥、声音、文字记录或指令。收到 SIGTERM 时不再接听新通话,给正在进行的通话
VOICE_DRAIN_SECONDS(默认 20 秒)后结束。
验证
docker compose logs pabal-server | grep -E "Voice|voice"
docker compose logs pabal-voice | head -20
INFO TelegramServer - Voice bots on: gateways connect to ws://10.231.53.2:8095/voice/link; bots 0 listed, 2 calls each at most
INFO VoiceGatewayLink - Voice gateway link listening on ws://10.231.53.2:8095/voice/link (网络)
INFO TelegramServer - Voice bot callers: patched apps only (desktop pabal-bc1+, Android 62151+, iOS 5+), recipients google.vertex.gemini-live@us-central1, testers 0 listed + the admin page's
INFO VoiceGatewayLink - Voice gateway pabal-voice-1 (boot 823c9600d05e811847ce95a8f1ba9efa) connected: new session, capacity 2, features […], audio to google.vertex.gemini-live@us-central1
Voice bots are off: …表示链路机密值不存在、无法读取或不是 32 字节;行末的— but without a TURN relay every bot call is busy表示没有中继。- 如果看到 ERROR
Voice bots are on, but no bot is callable: the privacy policy … has no id="bot-calls" section,需要先在网站的政策中发布机器人通话一节。 - 网关连上后,服务器日志的
features中会出现bot_settings·end_call·context·tools,能运行所有者密钥的网关还会有byok,带有文字记录提示语音的会有transcripts(顺序每次不同)。audio to必须与应用确认窗口中说明的接收方一致。 - 网关一侧的第一行应为
starting as pabal-voice-1 (capacity 2, … engine gemini, audio to google.vertex.gemini-live@us-central1),模型那一行应为session resumption off; no failover,并且应出现media workers sandbox themselves with Landlock (ABI N)。recordings for the callers' notices in: Korean, English表示能为来电者播放提示的语言(在连草稿也播放的测试服务器上,后面会加上(drafts included: …))。 - 然后在管理页面的语音(음성)标签页(管理与设置——语音标签页)中查看网关是否已连接,添加测试者,并允许机器人。
主密钥(自有密钥 · 音频流模式)
机器人所有者的模型密钥和音频流模式的机密值,都会用这个密钥封装后保存。它必须放在不会随备份一起被复制的地方。
sudo install -d -m 700 -o 1000 -g 1000 /etc/pabal/voice # /srv/pabal 之外,700
sudo sh -c 'umask 077; openssl rand -hex 32 > /etc/pabal/voice/voice-master.key'
sudo chown 1000:1000 /etc/pabal/voice/voice-master.key
TELEGRAM_VOICE_KEYS_MASTER_FILE: /app/secrets/voice-master.key
volumes:
- /etc/pabal/voice:/app/secrets:ro
- 如果它位于数据文件夹或 RSA 密钥文件夹之内或旁边,或者文件夹对其他用户有任何开放(不是 700),服务器就不会使用它(
Voice keys off: …)。如果文件不存在,服务器会在收到第一个密钥时创建(如果是只读挂载,请像上面那样自己创建)。 - 不要放进备份,另外单独保存一份离线副本。丢失后,所有机器人所有者都需要重新设置密钥。更换时,把旧密钥交给
TELEGRAM_VOICE_KEYS_PREVIOUS_MASTER_FILE并启动,检查日志后再删除旧文件。 - 即使有主密钥,在应用显示自有密钥机器人的确认窗口(第 2 版)并设置
TELEGRAM_VOICE_OWN_KEYS_APPS_READY=true之前,也不接受自有密钥。音频流模式需要通过TELEGRAM_VOICE_STREAM_MIN_BUILD_DESKTOP·_ANDROID·_IOS告知包含音频流确认窗口的应用版本后才能拨打(默认 0 = 谁都不能拨打),并且网关上需要VOICE_STREAM=true以及本部署的公网 IP(VOICE_STREAM_DENY_ADDRESSES)。
MCP 组件(可选)
这是在通话中调用机器人所有者注册的 MCP 服务器的组件。默认关闭,用 deploy/docker-compose.mcp.yml(配置档 mcp)添加三个容器。
| 容器 | 作用 |
|---|---|
pabal-mcp(uid 1101) | MCP 客户端。与服务器只通过套接字文件连接;因为只有一个内部网络,只能经由出站代理访问外部。只读、无特权、512 MB |
pabal-mcp-signer(uid 1102) | 签名器。完全没有网络,只通过套接字应答,并写出公开的签发者文档 |
pabal-mcp-egress(uid 1103) | 出站代理。只走 443,不会访问 Pabal 自身的地址 |
docker compose -f docker-compose.yml -f docker-compose.mcp.yml --profile mcp up -d
- 要开启,需要
TELEGRAM_MCP_EGRESS_OWN_ADDRESSES(Pabal 的公网地址,必填)和密钥文件(链路机密值、K_grant、安装密钥——都放在/srv/pabal之外权限为 700 的文件夹中),网站上还需要政策中的bot-calls-mcp一节和条款中的机器人所有者一节。只需做一次的设置按顺序写在docker-compose.mcp.yml开头的注释中。保管库和签名密钥不要放进备份。 - 等应用能在确认窗口中显示机器人的 MCP 接收方后,再设置
TELEGRAM_MCP_APPS_READY=true。在此之前只能注册,任何通话都不会带上 MCP 工具。 - 为 MCP 请求单独设置专用出站地址不是代码中的条件,而是运营上的决定(因为这是来电者的话流向第三方服务器的出口)。
- 开启后,日志中会出现
MCP tools on: pabal-mcp dials …; issuer …; ports …; own addresses N; clock by NTP …。deploy/ship.sh <服务器> <文件夹> --mcp只是构建并发送三个镜像、复制 override 文件,不会开启任何东西(尚未在 Linux 的 Docker 上运行过)。
Apple Watch 入口(可选,准备中)
Apple Watch 应用不使用 MTProto,而是通过服务器的手表 API(HTTPS JSON,/api/watch/v1/)登录和拨打电话,通话声音经过音频入口(pabal-voice-edge,WSS)到达网关。不使用通话中继(coturn)。默认关闭。
deploy/docker-compose.voice.yml 中有音频入口 pabal-voice-edge(profile voice-watch,不带机密值,内部专用网络),但还没有服务器的手表 API 设置(TELEGRAM_WATCH_*)和反向代理中的手表路径,所以外部的手表无法访问。以下是设置值的说明,尚未在生产配置中运行过(只在开发服务器上测试过)。
| 设置 | 默认值 | 含义 |
|---|---|---|
TELEGRAM_WATCH_API_PORT | — | 手表 API 端口。没有则手表 API 关闭 |
TELEGRAM_WATCH_API_HOST、TELEGRAM_WATCH_API_ANY_ADDRESS | 127.0.0.1、— | 监听地址。不是回环地址时需要 ANY_ADDRESS=true,并在前面放 TLS 代理 |
TELEGRAM_WATCH_AUDIO_URL | — | 告诉手表的音频入口地址。路径必须正好是 wss://<主机>/api/watch/v1/audio。缺少或有误时,手表通话关闭 |
TELEGRAM_WATCH_ALLOW_PLAIN | — | 允许 ws:// 地址。仅限开发(家中测试) |
TELEGRAM_WATCH_SESSION_DAYS | 7 | 手表登录保持的天数(1~365) |
TELEGRAM_WATCH_AUTH_PER_MINUTE | 30 | 整个服务器的登录验证码请求上限(每分钟)。每个地址另有上限 |
TELEGRAM_VOICE_MIN_BUILD_WATCHOS | 1 | 显示完整确认窗口的第一个手表应用版本 |
- 网关:
VOICE_DIRECT=true和VOICE_EDGE_DIR(与入口共用的套接字文件夹,默认/run/pabal-voice-edge)。手表通话只能用 16 kHz 引擎(Gemini)或回声引擎。 - 音频入口:用同一个镜像的第二个服务运行
python -m pabal_voice.edge(VOICE_EDGE_HOST·VOICE_EDGE_PORT,默认127.0.0.1·8096,VOICE_EDGE_DIR)。入口是最先解开来自互联网内容的地方,所以不要挂载任何机密,并把它放在只有代理和网关能访问的内部网络中。 - 反向代理只转发两个手表路径——
/api/watch/v1/audio(WebSocket,转给入口)和/api/watch/v1/(转给手表 API)。手表 API 一侧,让代理把客户端地址追加到X-Forwarded-For的末尾(服务器按这个值统计每个地址的登录上限)。入口一侧,删除X-Forwarded-For、X-Real-IP。 - 不能在手表上注册,也不能给使用工具、MCP、自有密钥、音频流模式或机器人所有者文字记录的机器人打电话(
WATCH_NOT_YET)。
保存对话(可选,准备中)
打电话的人在确认窗口中选择后,把通话中的对话以文字只保存在本人聊天中的功能(使用应用——语音机器人)。没有运营方的允许开关,以下条件全部具备时即会开启。
- 隐私政策中的
id="bot-calls-keep-text"一节(草稿:代码仓库的docs/voice-bots/p0/privacy-*.draft.html)。这一节本身就是开关。应用通过help.getAppConfig的pabal_voice_keep_text得知是否显示复选框。 - 网关 0.9.0 的
VOICE_KEEP_TEXT:使用模型引擎时默认true。在回声引擎(包括仅用于音频流的网关)上设为true属于配置错误,网关不会启动。 - 带复选框的应用版本。复选框文案要完成法律审查后才会放进正式版本。手表不显示未经审查的文案;
TELEGRAM_VOICE_KEEP_TEXT_UNREVIEWED=true是显示未审查文案的仅限内部测试服务器的开关。 - 在开启了它的服务器上,所有非音频流模式机器人通话的
voice_call ended最多晚 10 秒,并且会预先占用通话记录之后的 30 个消息编号(让机器人所有者无法知道来电者的选择)。 - 通话结束后等待写入的对话文字只在内存中,如果这期间服务器重启,该通话的文字就不会保存。
机器人打来的电话(可选,等待法律审查)
语音机器人主动给许可了该机器人的人打电话的功能(语音机器人——机器人打来的电话)。条件比打给机器人的电话更多,在完成法律审查之前请不要开启。开启顺序:
- 服务器:用
scripts/voice-evidence-key.py的new创建证据密钥文件夹(TELEGRAM_VOICE_EVIDENCE_KEY_DIR——700,放在会被备份的文件夹之外,比如数据和密钥文件夹之外;更换用rotate,检查用check),并把带许可窗口的第一个应用版本设为外呼最低版本,例如TELEGRAM_VOICE_OUTBOUND_MIN_BUILD_ANDROID(环境变量)。只写在.env里的TELEGRAM_VOICE_EVIDENCE_KEY_DIR不会传到服务器——请把它加到deploy/docker-compose.voice.yml中pabal-server的environment(容器内的路径)和volumes(主机上的那个文件夹)。 - 应用:发布带许可窗口的应用版本。应用的许可链接规则只认识
pabal.me,如果是其他服务器,必须把该服务器的主机加入应用版本的链接规则,机器人发来的链接才会打开窗口(菜单和设置列表照常可用)。 - 法律:发布隐私政策的
id="bot-calls-outbound"一节和服务条款的id="voice-bot-owners-outbound"一节(机器人所有者要同意条款,还需voice-bot-owners一节)。草稿在仓库的docs/voice-bots/p0/中。许可窗口的文字和外呼提示录音(notice-out、apology-rule)的审查和批准也在这一步。 - 开关:在管理页面语音标签的机器人打来的电话中设定国家和限额,用确认文字开启(管理页面——语音标签)。每个机器人另有运营方的外呼审核。
- 网关:只有
VOICE_OUTBOUND=true的网关才会拨打机器人的电话。使用模型引擎时,四段外呼提示录音必须已批准才会启动;草稿录音(VOICE_OUTBOUND_DRAFT_NOTICES)仅限测试服务器。 - 请不要在生产环境的
.env中放TELEGRAM_VOICE_CALLME_UNREVIEWED、TELEGRAM_VOICE_TEST_DIAL这类仅限测试和开发的名字。 - 机器人打来的电话同样不是端到端加密的——声音会经过本服务器的网关和 AI 提供方。
13. 位置消息中的地图图片(可选)
发送位置时,Android 应用和桌面(macOS、Windows)应用不会自己画出气泡中的地图,而是向服务器请求该地点的地图图片(upload.getWebFile)。服务器从 Google 的 Maps Static API 获取图片后转交。不开启时,Android 显示占位图片,桌面应用只在空白框中显示一个图钉。iPhone 在手机上自己绘制地图,与此设置无关;发送位置本身以及点开位置后的大地图,无论是否开启都能使用。
- 在 Google Cloud 控制台中选择一个已关联结算账号的项目,启用 Maps Static API。
- 创建 API 密钥:应用限制设为 IP 地址(服务器访问互联网时使用的公网 IP),API 限制只选 Maps Static API。这个密钥只放在服务器上,不会放进应用。
- 写入
.env,再用docker compose up -d pabal-server重启服务器。 - 在日志中确认。若处于关闭状态,会显示
Maps in location messages are off: …及原因(密钥的值不会出现在日志中)。
STATIC_MAPS_KEY=AIza… # 不要把密钥的值留在聊天、日志或代码仓库中
STATIC_MAPS_PER_DAY=300 # 每天新获取的图片数(0 = 不限)
STATIC_MAPS_PER_USER_HOUR=120 # 一个人每小时能让服务器新获取的图片数
INFO TelegramServer - Maps in location messages from Google Static Maps (120 new per person an hour, 300 a day)
- 费用:Maps Static API 每月 1 万次以内免费。服务器会把获取的图片记住一天,再次交给请求同一地图的设备,再次交出的图片不计入限额。按默认限额(每天 300 次),每月约 9 千次,在免费范围内。超过限额的请求会以
FLOOD_WAIT拒绝,在此期间应用显示占位图片。 - 隐私政策:共享位置的坐标(纬度、经度)会发送给 Google——不会发送是谁发送的。请在管理页面 → 设置 → 运营方信息的委托处理和境外转移中填写 Google LLC(Maps Static API,美国)。
- 如果日志中出现
Map picture not made: Google Static Maps answered 403: …,请检查密钥的限制(IP、API)或结算账号。
运维
状态与日志
cd /opt/pabal/deploy
docker compose ps
docker compose logs --since 1h pabal-server
tail -f /srv/pabal/pabal_server/logs/telegram-server.log
更新
cd /opt/pabal && git pull
cd deploy
./backup.sh # 先备份
docker compose up -d --build pabal-server # 用新镜像只替换服务器
docker compose logs --since 5m pabal-server | grep -E "Website|ERROR"
在服务器重启的几十秒内,应用会断开,然后无需重新登录就会自动重新连接。数据表的变更会在服务器启动时自动应用。绑定目录与镜像无关,因此 docker compose down 或删除镜像都不会删除数据——只要不删除 /srv/pabal 就行。
回滚:即使回到上一个镜像,新版本已经写入数据库的内容也会保留。旧版本可能读不懂新版本写的记录(例如带有语音机器人代码的版本,即使不开启语音,也会写入聊天中的通话记录等新事件),所以回滚时要同时恢复更新前那份备份中的数据库(恢复)。
备份
./backup.sh # → /srv/pabal/backups/<日期-时间>/{pabal.dump, server-keys-data.tar.gz}
crontab -e # 每天 03:00:
# 0 3 * * * /opt/pabal/deploy/backup.sh >> /srv/pabal/backups/backup.log 2>&1
- 超过 14 天的备份会被自动删除(可以像
KEEP_DAYS=30 ./backup.sh这样修改)。 - 请同时复制到其他地方。放在同一块磁盘上的备份扛不住磁盘故障。
- 备份中包含 RSA 私钥以及短信、SMTP 的机密值。存放备份的地方也要像服务器一样严加保护。
恢复
cd /opt/pabal/deploy
B=/srv/pabal/backups/20260920-030000 # 要恢复的备份
docker compose stop pabal-server pabal-caddy
docker compose exec -T pabal-postgres dropdb -U pabal pabal
docker compose exec -T pabal-postgres createdb -U pabal pabal
docker compose exec -T pabal-postgres pg_restore -U pabal -d pabal --no-owner < $B/pabal.dump
docker compose exec -T pabal-postgres psql -U pabal -d pabal -c "ALTER DATABASE pabal SET search_path = public" # 数据库的默认路径——恢复不会带回它,需重新设置
sudo tar -C /srv/pabal/pabal_server -xzf $B/server-keys-data.tar.gz
sudo chown -R 1000:1000 /srv/pabal/pabal_server
docker compose up -d
迁移到新服务器时也一样:完成第 1~5 步后,不执行第 6 步,而是执行上面的恢复再启动。恢复同一个密钥后,就不需要重新构建应用(如果服务器 IP 变了,则需要重新构建)。
启动与停止
docker compose restart pabal-server # 只重启服务器
docker compose stop # 全部停止(数据保持不变)
docker compose up -d # 重新启动
即使服务器主机重启,容器也因为设置了 restart: unless-stopped 而会自动重新启动。
安全检查清单
.env权限为 600,POSTGRES_PASSWORD为随机值/srv/pabal/pabal_server/keys权限为 700,私钥除了备份之外不存在于其他任何地方- (如果启用了推送)
firebase-service-account.json权限为 600,代码仓库和.env中没有它的内容 - (如果启用了 iPhone 推送)
AuthKey_<密钥 ID>.p8权限为 600,代码仓库和.env中没有它的内容 - 防火墙:只开放 22(尽量只对运营方 IP)、80、443、8443
- 从外部访问
https://域名/admin/返回 404 - 管理页面:测试号码(테스트 번호)已关闭,验证码发送方式(코드 전달 방식)为短信/电子邮件,测试发送(테스트 발송)成功
- 没有开启
TELEGRAM_WEBHOOK_ALLOW_LOCAL(防止机器人 Webhook 访问内部网络) - (如果开启了语音机器人)链路机密值、Vertex 密钥和主密钥只能由各自读取它的用户读取(400/600),且主密钥位于
/srv/pabal之外,不会进入备份。链路端口没有公开,链路网络只有服务器和网关使用。生产服务器上没有TELEGRAM_VOICE_NOTICE_CHECK·TELEGRAM_VOICE_TESTERS=* - SSH:关闭密码登录,只允许密钥登录
- 确认备份 cron 和外部复制,并演练一次恢复
- 开启服务器操作系统的自动安全更新(
unattended-upgrades)
故障排除
| 症状 | 原因 → 解决方法 |
|---|---|
| 应用一直停在“正在连接…” | ① 8443 被拦截 → nc -vz IP 8443,检查 ufw 和云安全组 ② 应用是用其他公钥构建的 → 用这台服务器的密钥重新执行第 9 步 ③ PUBLIC_IP 有误 → 修改 .env 后执行 docker compose up -d pabal-server |
| 一开始能连上,但很快断开 | 应用会转到服务器告知的地址(PUBLIC_IP:MTPROTO_PORT),而这个地址有误 → 检查 PUBLIC_IP |
| HTTPS 证书错误 | DNS 尚未指向这台服务器,或者 80 端口被拦截 → dig +short 域名、docker compose logs pabal-caddy |
server 不断重启,password authentication failed | 在创建数据之后修改了 POSTGRES_PASSWORD → 改回原来的值,或者在数据库中执行 ALTER USER pabal PASSWORD '…' |
AccessDeniedException: /app/keys/… | 绑定目录的所有者不对 → sudo chown -R 1000:1000 /srv/pabal/pabal_server |
OutOfMemoryError、运行变慢 | 调高 JAVA_OPTS 中的 -Xmx,然后执行 docker compose up -d pabal-server |
| 收不到注册验证码 | 查看管理页面注册验证码(가입 코드)标签页中的发送状态和失败原因 → 检查设置中的短信、SMTP 值,并进行测试发送 |
| Android 收不到通知 | ① 日志中有 Push notifications are off → 检查 FCM_CREDENTIALS 路径和文件权限 ② 没有 Push device registered → 应用构建时没有 Firebase 配置(pabal-placeholder),或者还没有打开新应用 ③ 显示 sent 却没有弹出 → 检查手机的通知权限和电池设置,以及应用是否被“强行停止” ④ SENDER_ID_MISMATCH … Registrations are kept → 服务器的密钥和应用的 google-services.json 来自不同的项目 ⑤ answered 404 without naming the token → FCM_PROJECT_ID 有误 ⑥ refused an access token → 密钥已被废弃,或是其他项目的密钥(5 分钟内不会再次请求) |
| iPhone 收不到通知 | ① 日志中有 iPhone push notifications are off → 检查 APNS_KEY 路径、文件权限和文件名(AuthKey_<密钥 ID>.p8) ② 没有 type 1 登记 → 没有在应用中允许通知,或者用的是模拟器 ③ 有登记却什么都没发出 → 该构建低于 APNS_MIN_BUILD(登记行中的 build),或者是已关闭通知的对话 ④ APNs refused the provider token → 密钥已被废弃,或 APNS_TEAM_ID 与密钥所属的团队不同(5 分钟内不会再次发送) ⑤ DeviceTokenNotForTopic … Registrations are kept → APNS_TOPIC 与应用的 Bundle ID 不同 ⑥ 显示 sent 却只看到“You have a new notification” → 通知扩展没能解密(如果重装过应用,请打开一次以重新登记) ⑦ 来电只以通知到达 → APNS_VOIP_MIN_BUILD 为空,或构建低于该值(这是预期行为) |
| 无法进入管理页面 | SSH 隧道是否开着;自己电脑上的 8080 是否被其他程序占用(改用 -L 18080:127.0.0.1:8080,并访问 localhost:18080) |
network ssemiya-net declared as external, but could not be found | 网络尚未创建 → docker network create ssemiya-net(若用其他名称,设置 .env 中的 PABAL_NETWORK) |
已知限制
- 单服务器架构。启动时会把所有账号、对话和消息加载到内存中。暂不支持拆分到多台服务器的水平扩展,数据增多时需要增加内存。
- 等待中的注册验证码、机器人尚未取走的更新、等待写入的保存对话文字都保存在内存中,服务器重启后就会丢失。
- 注册验证码的请求有间隔和每日上限,但其他所有请求都还没有频率限制。刚对外公开后,请经常查看日志和仪表盘。
- 推送通知只发往手机应用——Android(第 10 步)和 iPhone(第 11 步)。iPhone 只发往
APNS_MIN_BUILD及以上的构建(Pabal 1.0 (3) 不在其内),来电的 CallKit 界面要在设置APNS_VOIP_MIN_BUILD之后才会启用。macOS、Windows 应用在运行期间接收消息。 - 按对话设置的“关闭通知”只保存在服务器内存中。已关闭通知的对话的推送,发往 Android 时使用非紧急优先级(由手机自行过滤),不发往 iPhone;但服务器重启后,两者都会照常发出(应用只在更改设置时才会重新告知服务器)。如果已关闭的应用从被唤醒到连上服务器用了 20 秒以上,来电可能会在拨打方那边以无人接听结束。
- 消息附件支持照片和文件,能在应用内直接播放的视频和语音消息尚未支持。频道、超级群组、群组通话和两步验证也尚未支持。
- 授权密钥以明文形式保存在数据库中(服务器解密时需要)。请像保护 RSA 密钥一样保护数据库和备份。
文件
| 文件 | 作用 |
|---|---|
deploy/docker-compose.yml | 生产配置(pabal-server · pabal-postgres · pabal-caddy,绑定挂载) |
deploy/Caddyfile | HTTPS、拦截管理路径、转发网站、文档和 Bot API |
deploy/.env.example | 配置样例 → deploy/.env |
deploy/backup.sh | 数据库转储 + 密钥和数据归档,清理旧备份 |
Dockerfile | 服务器镜像(构建 → JRE 21,uid 1000) |
voice/ | 语音网关 pabal-voice(源代码、Dockerfile、README.md、链路规范 PROTOCOL.md) |
deploy/docker-compose.mcp.yml | 语音机器人的三个 MCP 组件容器(配置档 mcp) |
deploy/docker-compose.voice.yml | 添加语音机器人组件的文件——服务器的语音设置和链路网络、网关 pabal-voice(profile voice)、手表的音频入口 pabal-voice-edge(profile voice-watch) |
deploy/ship.sh | 在另一台电脑上为服务器的 CPU 构建服务器镜像(加 --voice 时也构建网关镜像),通过 SSH 载入并复制部署文件 |
deploy/voice_readiness.py | 开启语音前的检查(Vertex 密钥、法律条款、应用版本、中继……)——不输出机密值 |
deploy/turnserver.conf.example, deploy/check_relay.py | 直接安装在 Mac 服务器上的 coturn 配置样例,以及检查中继的工具 |