OpenIM + LiveKit 内网测试环境一键部署包

- docker-compose: openim-server v3.8.3-patch.12 + mongo/redis/etcd/kafka/minio + livekit v1.13.5
- 按约束移除 openim-chat(GPLv3) 及依赖它的 web/admin 前端; 监控组件收进 profile m
- scripts/deploy.sh 一键部署, scripts/selftest.sh 自测(注册/登录/文字/语音/文件/一对一通话), scripts/restart.sh 重启
- README: 部署记录(服务器要求/端口表/运维/接入参数)
This commit is contained in:
KIMI
2026-08-08 22:07:55 +08:00
commit 33fa2339e2
12 changed files with 1048 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
# ===== 服务器基础配置 =====
# 服务器内网 IP。scripts/deploy.sh 会自动探测并写入 .env,也可手动改。
SERVER_IP=127.0.0.1
# ===== 镜像版本 =====
MONGO_IMAGE=mongo:7.0
REDIS_IMAGE=redis:7.0.0
KAFKA_IMAGE=bitnamilegacy/kafka:3.5.1
MINIO_IMAGE=minio/minio:RELEASE.2024-01-11T07-46-16Z
ETCD_IMAGE=bitnamilegacy/etcd:3.5.13
OPENIM_SERVER_IMAGE=openim/openim-server:v3.8.3-patch.12
LIVEKIT_IMAGE=livekit/livekit-server:v1.13.5
# 国内拉镜像慢时,OpenIM 可换阿里云镜像:
#OPENIM_SERVER_IMAGE=registry.cn-hangzhou.aliyuncs.com/openimsdk/openim-server:release-v3.8.3
# 监控组件(可选,仅 docker compose --profile m up -d 时启动)
PROMETHEUS_IMAGE=prom/prometheus:v2.51.2
ALERTMANAGER_IMAGE=prom/alertmanager:v0.27.0
GRAFANA_IMAGE=grafana/grafana:11.0.1
NODE_EXPORTER_IMAGE=prom/node-exporter:v1.7.0
# ===== 数据目录(相对 docker-compose.yaml 所在目录)=====
DATA_DIR=./
# ===== MongoDB =====
MONGO_ADDRESS="mongo:27017"
MONGO_USERNAME=openIM
MONGO_PASSWORD=openIM123
# ===== Kafka =====
KAFKA_ADDRESS="kafka:9094"
# 默认不启用 Kafka 认证;如需启用取消下面两行注释(启动时的缺变量警告可忽略)
#KAFKA_USERNAME=openIM
#KAFKA_PASSWORD=openIM123
# ===== Etcd =====
ETCD_ADDRESS="etcd:2379"
# 默认不启用 Etcd 认证;如需启用取消下面四行注释
#ETCD_ROOT_USER=root
#ETCD_ROOT_PASSWORD=openIM123
#ETCD_USERNAME=openIM
#ETCD_PASSWORD=openIM123
# ===== Redis =====
REDIS_ADDRESS="redis:6379"
REDIS_PASSWORD=openIM123
# ===== MinIO(图片/语音/文件存储)=====
# 关键:必须是客户端(手机/PC)能访问到的地址,deploy.sh 会写入 http://<SERVER_IP>:10005
MINIO_EXTERNAL_ADDRESS="http://127.0.0.1:10005"
MINIO_INTERNAL_ADDRESS="minio:9000"
MINIO_ACCESS_KEY_ID=root
MINIO_SECRET_ACCESS_KEY=openIM123
MINIO_PORT=10005
MINIO_CONSOLE_PORT=10004
# ===== OpenIM 服务端 =====
OPENIM_SECRET=openIM123
OPENIM_API_PORT=10002
OPENIM_MSG_GATEWAY_PORT=10001
LOG_IS_STDOUT=true
LOG_LEVEL=3
# ===== LiveKit(一对一语音通话)=====
LIVEKIT_API_KEY=openimLKkey
LIVEKIT_API_SECRET=openimLKsecretChangeMe123
LIVEKIT_PORT=7880
# ===== 监控端口(可选)=====
PROMETHEUS_PORT=19090
ALERTMANAGER_PORT=19093
GRAFANA_PORT=13000
NODE_EXPORTER_PORT=19100
GRAFANA_URL="http://127.0.0.1:13000/"
+4
View File
@@ -0,0 +1,4 @@
# 运行时生成,不入库
.env
components/
.selftest/
+88
View File
@@ -0,0 +1,88 @@
# OpenIM + LiveKit 内网测试环境部署记录
内部通讯 App 的服务端测试环境。一键部署后两个测试账号可互发文字/语音/文件消息,并可打通一对一语音通话。
## 组件清单
| 组件 | 镜像 | 用途 |
| --- | --- | --- |
| OpenIM Server | `openim/openim-server:v3.8.3-patch.12` | IM 服务端(REST API + WebSocket 消息网关) |
| LiveKit | `livekit/livekit-server:v1.13.5` | 语音通话 SFU |
| MongoDB / Redis / Etcd / Kafka / MinIO | 见 `.env.example` | OpenIM 依赖组件 |
按任务约束做的裁剪(相对官方 openim-docker v3.8):
- 移除 `openim-chat`(GPLv3 授权不利改造,注册登录后续由"公司账号登录"卡自研)
- 移除 `openim-web-front` / `openim-admin-front`(这两个前端依赖 chat 服务)
- 监控组件(Prometheus/Grafana 等)保留在 compose profile `m` 下,默认不启动
未改动 OpenIM 服务端任何核心代码,全部通过环境变量配置(`IMENV_*`)。
## 服务器要求
- 内网闲置机器,4 核 8G 起步,磁盘 ≥ 20G
- 已安装 Docker(含 compose 插件,`docker compose version` 能跑通即可)
- 不需要公网入口
## 端口表(需要对客户端开放的只有前 5 个)
| 端口 | 协议 | 用途 |
| --- | --- | --- |
| 10001 | TCP/WS | OpenIM 消息网关(客户端长连接) |
| 10002 | TCP/HTTP | OpenIM REST API |
| 10005 | TCP/HTTP | MinIO(图片/语音/文件下载) |
| 7880 | TCP/WS | LiveKit 信令 |
| 7882 | UDP | LiveKit 语音媒体 |
| 7881 | TCP | LiveKit 媒体备用通道(UDP 不通时) |
| 10004 | TCP/HTTP | MinIO 控制台(运维用,可不开放) |
| 12379/12380 | TCP | Etcd(仅容器间用,建议不对外) |
## 部署步骤
```bash
git clone ssh://git@192.168.200.36:222/leefer/tongxunruanjian.git
cd tongxunruanjian
./scripts/deploy.sh # 自动探测本机内网 IP;也可 ./scripts/deploy.sh 192.168.x.x 指定
./scripts/selftest.sh # 自测(见下)
```
`deploy.sh` 做的事:首次从 `.env.example` 生成 `.env` → 把服务器 IP 写入 `MINIO_EXTERNAL_ADDRESS`(文件下载链接用这个地址,写错会导致文件发不出)→ 预建数据目录 → `docker compose pull && up -d` → 等待 openim-server 健康检查通过。
数据全部落在仓库目录下的 `components/` 里;`.env` 里的密码/密钥是测试环境默认值,正式环境前再换。
## 自测覆盖(scripts/selftest.sh
1. 管理员令牌获取(`/auth/get_admin_token`
2. 注册测试账号 `test001` / `test002``/user/user_register`
3. 签发用户令牌,即登录链路(`/auth/get_user_token`
4. 两账号互发文字消息(`/msg/send_msg`
5. 附件上传 MinIO 并发送语音消息、文件消息
6. 消息落库回查(`/msg/search_msg`,逐条核对刚发的文字/语音/文件)
7. LiveKit 一对一:两个客户端进同一房间,主叫发布音频轨,被叫端确认收到轨道
全部通过会打印客户端接入参数。失败项会标 `[失败]`,配合 `docker compose logs -f openim-server``docker compose logs -f livekit` 排查。
## 日常运维
```bash
./scripts/restart.sh # 重启全部服务
./scripts/restart.sh livekit # 只重启某个服务
docker compose ps # 查看状态
docker compose logs -f openim-server # 看日志
docker compose down # 停止(数据保留在 components/
docker compose up -d # 再启动
```
## 客户端接入参数(自测通过后)
- OpenIM API: `http://<服务器IP>:10002`
- OpenIM WebSocket: `ws://<服务器IP>:10001`
- LiveKit: `ws://<服务器IP>:7880`API Key/Secret 见 `.env``LIVEKIT_API_KEY` / `LIVEKIT_API_SECRET`
- 测试账号:`test001` / `test002`(由自测脚本注册);语音通话时两名用户以各自 identity 进同一房间即可
## 常见问题
- **文件/语音能发但打不开**`MINIO_EXTERNAL_ADDRESS` 没写成客户端能访问的 IP。改 `.env``docker compose up -d` 重建 openim-server。
- **启动报缺 `ETCD_USERNAME` / `KAFKA_USERNAME` 等警告**:未启用对应组件认证,官方说明可忽略。
- **openim-server 一直 unhealthy**`docker exec -it openim-server mage check` 看哪项依赖没通;首次启动需等 30-60 秒。
- **语音通话连不上**:确认服务器 UDP 7882 放行(内网防火墙/安全组);UDP 不通时会走 TCP 7881 兜底。
+34
View File
@@ -0,0 +1,34 @@
global:
resolve_timeout: 5m
smtp_from: alert@openim.io
smtp_smarthost: smtp.163.com:465
smtp_auth_username: alert@openim.io
smtp_auth_password: YOURAUTHPASSWORD
smtp_require_tls: false
smtp_hello: xxx
templates:
- /etc/alertmanager/email.tmpl
route:
group_by: [ 'alertname' ]
group_wait: 5s
group_interval: 5s
repeat_interval: 5m
receiver: email
routes:
- matchers:
- alertname = "XXX"
group_by: [ 'instance' ]
group_wait: 5s
group_interval: 5s
repeat_interval: 5m
receiver: email
receivers:
- name: email
email_configs:
- to: 'alert@example.com'
html: '{{ template "email.to.html" . }}'
headers: { Subject: "[OPENIM-SERVER]Alarm" }
send_resolved: true
+36
View File
@@ -0,0 +1,36 @@
{{ define "email.to.html" }}
{{ if eq .Status "firing" }}
{{ range .Alerts }}
<!-- Begin of OpenIM Alert -->
<div style="border:1px solid #ccc; padding:10px; margin-bottom:10px;">
<h3>OpenIM Alert</h3>
<p><strong>Alert Status:</strong> firing</p>
<p><strong>Alert Program:</strong> Prometheus Alert</p>
<p><strong>Severity Level:</strong> {{ .Labels.severity }}</p>
<p><strong>Alert Type:</strong> {{ .Labels.alertname }}</p>
<p><strong>Affected Host:</strong> {{ .Labels.instance }}</p>
<p><strong>Affected Service:</strong> {{ .Labels.job }}</p>
<p><strong>Alert Subject:</strong> {{ .Annotations.summary }}</p>
<p><strong>Trigger Time:</strong> {{ .StartsAt.Format "2006-01-02 15:04:05" }}</p>
</div>
{{ end }}
{{ else if eq .Status "resolved" }}
{{ range .Alerts }}
<!-- Begin of OpenIM Alert -->
<div style="border:1px solid #ccc; padding:10px; margin-bottom:10px;">
<h3>OpenIM Alert</h3>
<p><strong>Alert Status:</strong> resolved</p>
<p><strong>Alert Program:</strong> Prometheus Alert</p>
<p><strong>Severity Level:</strong> {{ .Labels.severity }}</p>
<p><strong>Alert Type:</strong> {{ .Labels.alertname }}</p>
<p><strong>Affected Host:</strong> {{ .Labels.instance }}</p>
<p><strong>Affected Service:</strong> {{ .Labels.job }}</p>
<p><strong>Alert Subject:</strong> {{ .Annotations.summary }}</p>
<p><strong>Trigger Time:</strong> {{ .StartsAt.Format "2006-01-02 15:04:05" }}</p>
</div>
{{ end }}
<!-- End of OpenIM Alert -->
{{ end }}
{{ end }}
+44
View File
@@ -0,0 +1,44 @@
groups:
- name: instance_down
rules:
- alert: InstanceDown
expr: up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Instance {{ $labels.instance }} down"
description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 1 minutes."
- name: database_insert_failure_alerts
rules:
- alert: DatabaseInsertFailed
expr: (increase(msg_insert_redis_failed_total[5m]) > 0) or (increase(msg_insert_mongo_failed_total[5m]) > 0)
for: 1m
labels:
severity: critical
annotations:
summary: "Increase in MsgInsertRedisFailedCounter or MsgInsertMongoFailedCounter detected"
description: "Either MsgInsertRedisFailedCounter or MsgInsertMongoFailedCounter has increased in the last 5 minutes, indicating failures in message insert operations to Redis or MongoDB,maybe the redis or mongodb is crash."
- name: registrations_few
rules:
- alert: RegistrationsFew
expr: increase(user_login_total[1h]) == 0
for: 1m
labels:
severity: info
annotations:
summary: "Too few registrations within the time frame"
description: "The number of registrations in the last hour is 0. There might be some issues."
- name: messages_few
rules:
- alert: MessagesFew
expr: (increase(single_chat_msg_process_success_total[1h])+increase(group_chat_msg_process_success_total[1h])) == 0
for: 1m
labels:
severity: info
annotations:
summary: "Too few messages within the time frame"
description: "The number of messages sent in the last hour is 0. There might be some issues."
+15
View File
@@ -0,0 +1,15 @@
# LiveKit 服务端配置(内网部署)
# API Key/Secret 通过环境变量 LIVEKIT_KEYS 注入(见 docker-compose.yaml
# 服务器对内 IP 通过启动参数 --node-ip 注入(deploy.sh 写入 .env 的 SERVER_IP
port: 7880
rtc:
tcp_port: 7881
udp_port: 7882
# 内网部署:不做公网 IP 探测,candidate 直接使用 --node-ip
use_external_ip: false
room:
# 房间内最后一人离开后保留 5 分钟再销毁,避免短暂掉线房间即失效
departure_timeout: 300
empty_timeout: 300
logging:
level: info
+119
View File
@@ -0,0 +1,119 @@
# my global config
global:
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
# scrape_timeout is set to the global default (10s).
# Alertmanager configuration
alerting:
alertmanagers:
- static_configs:
- targets: [127.0.0.1:19093]
# Load rules once and periodically evaluate them according to the global evaluation_interval.
rule_files:
- instance-down-rules.yml
# - first_rules.yml
# - second_rules.yml
# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
# The job name is added as a label "job=job_name" to any timeseries scraped from this config.
# Monitored information captured by prometheus
# prometheus fetches application services
- job_name: node_exporter
static_configs:
- targets: [ 127.0.0.1:19100 ]
- job_name: openimserver-openim-api
http_sd_configs:
- url: "http://127.0.0.1:10002/prometheus_discovery/api"
# static_configs:
# - targets: [ 127.0.0.1:12002 ]
# labels:
# namespace: default
- job_name: openimserver-openim-msggateway
http_sd_configs:
- url: "http://127.0.0.1:10002/prometheus_discovery/msg_gateway"
# static_configs:
# - targets: [ 127.0.0.1:12140 ]
# # - targets: [ 127.0.0.1:12140, 127.0.0.1:12141, 127.0.0.1:12142, 127.0.0.1:12143, 127.0.0.1:12144, 127.0.0.1:12145, 127.0.0.1:12146, 127.0.0.1:12147, 127.0.0.1:12148, 127.0.0.1:12149, 127.0.0.1:12150, 127.0.0.1:12151, 127.0.0.1:12152, 127.0.0.1:12153, 127.0.0.1:12154, 127.0.0.1:12155 ]
# labels:
# namespace: default
- job_name: openimserver-openim-msgtransfer
http_sd_configs:
- url: "http://127.0.0.1:10002/prometheus_discovery/msg_transfer"
# static_configs:
# - targets: [ 127.0.0.1:12020, 127.0.0.1:12021, 127.0.0.1:12022, 127.0.0.1:12023, 127.0.0.1:12024, 127.0.0.1:12025, 127.0.0.1:12026, 127.0.0.1:12027 ]
# # - targets: [ 127.0.0.1:12020, 127.0.0.1:12021, 127.0.0.1:12022, 127.0.0.1:12023, 127.0.0.1:12024, 127.0.0.1:12025, 127.0.0.1:12026, 127.0.0.1:12027, 127.0.0.1:12028, 127.0.0.1:12029, 127.0.0.1:12030, 127.0.0.1:12031, 127.0.0.1:12032, 127.0.0.1:12033, 127.0.0.1:12034, 127.0.0.1:12035 ]
# labels:
# namespace: default
- job_name: openimserver-openim-push
http_sd_configs:
- url: "http://127.0.0.1:10002/prometheus_discovery/push"
# static_configs:
# - targets: [ 127.0.0.1:12170, 127.0.0.1:12171, 127.0.0.1:12172, 127.0.0.1:12173, 127.0.0.1:12174, 127.0.0.1:12175, 127.0.0.1:12176, 127.0.0.1:12177 ]
## - targets: [ 127.0.0.1:12170, 127.0.0.1:12171, 127.0.0.1:12172, 127.0.0.1:12173, 127.0.0.1:12174, 127.0.0.1:12175, 127.0.0.1:12176, 127.0.0.1:12177, 127.0.0.1:12178, 127.0.0.1:12179, 127.0.0.1:12180, 127.0.0.1:12182, 127.0.0.1:12183, 127.0.0.1:12184, 127.0.0.1:12185, 127.0.0.1:12186 ]
# labels:
# namespace: default
- job_name: openimserver-openim-rpc-auth
http_sd_configs:
- url: "http://127.0.0.1:10002/prometheus_discovery/auth"
# static_configs:
# - targets: [ 127.0.0.1:12200 ]
# labels:
# namespace: default
- job_name: openimserver-openim-rpc-conversation
http_sd_configs:
- url: "http://127.0.0.1:10002/prometheus_discovery/conversation"
# static_configs:
# - targets: [ 127.0.0.1:12220 ]
# labels:
# namespace: default
- job_name: openimserver-openim-rpc-friend
http_sd_configs:
- url: "http://127.0.0.1:10002/prometheus_discovery/friend"
# static_configs:
# - targets: [ 127.0.0.1:12240 ]
# labels:
# namespace: default
- job_name: openimserver-openim-rpc-group
http_sd_configs:
- url: "http://127.0.0.1:10002/prometheus_discovery/group"
# static_configs:
# - targets: [ 127.0.0.1:12260 ]
# labels:
# namespace: default.
- job_name: openimserver-openim-rpc-msg
http_sd_configs:
- url: "http://127.0.0.1:10002/prometheus_discovery/msg"
# static_configs:
# - targets: [ 127.0.0.1:12280 ]
# labels:
# namespace: default
- job_name: openimserver-openim-rpc-third
http_sd_configs:
- url: "http://127.0.0.1:10002/prometheus_discovery/third"
# static_configs:
# - targets: [ 127.0.0.1:12300 ]
# labels:
# namespace: default
- job_name: openimserver-openim-rpc-user
http_sd_configs:
- url: "http://127.0.0.1:10002/prometheus_discovery/user"
# static_configs:
# - targets: [ 127.0.0.1:12320 ]
# labels:
# namespace: default
+387
View File
@@ -0,0 +1,387 @@
# 内部通讯 App —— OpenIM + LiveKit 内网测试环境
# 基于官方 openim-docker v3.8 裁剪:
# - 移除 openim-chatGPLv3,本项目的注册登录后续自研)
# - 移除 openim-web-front / openim-admin-front(均依赖 chat 服务)
# - 新增 livekit 服务(语音通话 SFU
# 监控组件(prometheus/alertmanager/grafana/node-exporter)保留在 profile "m" 下,
# 默认不启动;需要时执行 docker compose --profile m up -d。
networks:
openim:
driver: bridge
services:
mongo:
image: "${MONGO_IMAGE}"
container_name: mongo
command: >
bash -c '
docker-entrypoint.sh mongod --wiredTigerCacheSizeGB $$wiredTigerCacheSizeGB --auth &
until mongosh -u $$MONGO_INITDB_ROOT_USERNAME -p $$MONGO_INITDB_ROOT_PASSWORD --authenticationDatabase admin --eval "db.runCommand({ ping: 1 })" &>/dev/null; do
echo "Waiting for MongoDB to start..."
sleep 1
done &&
mongosh -u $$MONGO_INITDB_ROOT_USERNAME -p $$MONGO_INITDB_ROOT_PASSWORD --authenticationDatabase admin --eval "
db = db.getSiblingDB(\"$$MONGO_INITDB_DATABASE\");
if (!db.getUser(\"$$MONGO_OPENIM_USERNAME\")) {
db.createUser({
user: \"$$MONGO_OPENIM_USERNAME\",
pwd: \"$$MONGO_OPENIM_PASSWORD\",
roles: [{role: \"readWrite\", db: \"$$MONGO_INITDB_DATABASE\"}]
});
print(\"User created successfully: \");
print(\"Username: $$MONGO_OPENIM_USERNAME\");
print(\"Password: $$MONGO_OPENIM_PASSWORD\");
print(\"Database: $$MONGO_INITDB_DATABASE\");
} else {
print(\"User already exists in database: $$MONGO_INITDB_DATABASE, Username: $$MONGO_OPENIM_USERNAME\");
}
" &&
tail -f /dev/null
'
volumes:
- "${DATA_DIR}/components/mongodb/data/db:/data/db"
- "${DATA_DIR}/components/mongodb/data/logs:/data/logs"
- "${DATA_DIR}/components/mongodb/data/conf:/etc/mongo"
environment:
- TZ=Asia/Shanghai
- wiredTigerCacheSizeGB=1
- MONGO_INITDB_ROOT_USERNAME=root
- MONGO_INITDB_ROOT_PASSWORD=openIM123
- MONGO_INITDB_DATABASE=openim_v3
- MONGO_OPENIM_USERNAME=${MONGO_USERNAME}
- MONGO_OPENIM_PASSWORD=${MONGO_PASSWORD}
restart: always
networks:
- openim
redis:
image: "${REDIS_IMAGE}"
container_name: redis
volumes:
- "${DATA_DIR}/components/redis/data:/data"
- "${DATA_DIR}/components/redis/config/redis.conf:/usr/local/redis/config/redis.conf"
environment:
TZ: Asia/Shanghai
restart: always
sysctls:
net.core.somaxconn: 1024
command:
[
"redis-server",
"/usr/local/redis/config/redis.conf",
"--requirepass",
"${REDIS_PASSWORD}",
"--appendonly",
"yes",
]
networks:
- openim
etcd:
image: "${ETCD_IMAGE}"
container_name: etcd
ports:
- "12379:2379"
- "12380:2380"
environment:
- ETCD_NAME=s1
- ETCD_DATA_DIR=/etcd-data
- ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379
- ETCD_ADVERTISE_CLIENT_URLS=http://0.0.0.0:2379
- ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380
- ETCD_INITIAL_ADVERTISE_PEER_URLS=http://0.0.0.0:2380
- ETCD_INITIAL_CLUSTER=s1=http://0.0.0.0:2380
- ETCD_INITIAL_CLUSTER_TOKEN=tkn
- ETCD_INITIAL_CLUSTER_STATE=new
- ALLOW_NONE_AUTHENTICATION=no
## Optional: Enable etcd authentication by setting the following credentials
# - ETCD_ROOT_USER=root
# - ETCD_ROOT_PASSWORD=openIM123
# - ETCD_USERNAME=openIM
# - ETCD_PASSWORD=openIM123
volumes:
- "${DATA_DIR}/components/etcd:/etcd-data"
command: >
/bin/sh -c '
etcd &
export ETCDCTL_API=3
echo "Waiting for etcd to become healthy..."
until etcdctl --endpoints=http://127.0.0.1:2379 endpoint health &>/dev/null; do
echo "Waiting for ETCD to start..."
sleep 1
done
echo "etcd is healthy."
if [ -n "$${ETCD_ROOT_USER}" ] && [ -n "$${ETCD_ROOT_PASSWORD}" ] && [ -n "$${ETCD_USERNAME}" ] && [ -n "$${ETCD_PASSWORD}" ]; then
echo "Authentication credentials provided. Setting up authentication..."
echo "Checking authentication status..."
if ! etcdctl --endpoints=http://127.0.0.1:2379 auth status | grep -q "Authentication Status: true"; then
echo "Authentication is disabled. Creating users and enabling..."
# Create users and setup permissions
etcdctl --endpoints=http://127.0.0.1:2379 user add $${ETCD_ROOT_USER} --new-user-password=$${ETCD_ROOT_PASSWORD} || true
etcdctl --endpoints=http://127.0.0.1:2379 user add $${ETCD_USERNAME} --new-user-password=$${ETCD_PASSWORD} || true
etcdctl --endpoints=http://127.0.0.1:2379 role add openim-role || true
etcdctl --endpoints=http://127.0.0.1:2379 role grant-permission openim-role --prefix=true readwrite / || true
etcdctl --endpoints=http://127.0.0.1:2379 role grant-permission openim-role --prefix=true readwrite "" || true
etcdctl --endpoints=http://127.0.0.1:2379 user grant-role $${ETCD_USERNAME} openim-role || true
etcdctl --endpoints=http://127.0.0.1:2379 user grant-role $${ETCD_ROOT_USER} $${ETCD_USERNAME} root || true
echo "Enabling authentication..."
etcdctl --endpoints=http://127.0.0.1:2379 auth enable
echo "Authentication enabled successfully"
else
echo "Authentication is already enabled. Checking OpenIM user..."
# Check if openIM user exists and can perform operations
if ! etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_USERNAME}:$${ETCD_PASSWORD} put /test/auth "auth-check" &>/dev/null; then
echo "OpenIM user test failed. Recreating user with root credentials..."
# Try to create/update the openIM user using root credentials
etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_ROOT_USER}:$${ETCD_ROOT_PASSWORD} user add $${ETCD_USERNAME} --new-user-password=$${ETCD_PASSWORD} --no-password-file || true
etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_ROOT_USER}:$${ETCD_ROOT_PASSWORD} role add openim-role || true
etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_ROOT_USER}:$${ETCD_ROOT_PASSWORD} role grant-permission openim-role --prefix=true readwrite / || true
etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_ROOT_USER}:$${ETCD_ROOT_PASSWORD} role grant-permission openim-role --prefix=true readwrite "" || true
etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_ROOT_USER}:$${ETCD_ROOT_PASSWORD} user grant-role $${ETCD_USERNAME} openim-role || true
etcdctl --endpoints=http://127.0.0.1:2379 user grant-role $${ETCD_ROOT_USER} $${ETCD_USERNAME} root || true
echo "OpenIM user recreated with required permissions"
else
echo "OpenIM user exists and has correct permissions"
etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_USERNAME}:$${ETCD_PASSWORD} del /test/auth &>/dev/null
fi
fi
echo "Testing authentication with OpenIM user..."
if etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_USERNAME}:$${ETCD_PASSWORD} put /test/auth "auth-works"; then
echo "Authentication working properly"
etcdctl --endpoints=http://127.0.0.1:2379 --user=$${ETCD_USERNAME}:$${ETCD_PASSWORD} del /test/auth
else
echo "WARNING: Authentication test failed"
fi
else
echo "No authentication credentials provided. Running in no-auth mode."
echo "To enable authentication, set ETCD_ROOT_USER, ETCD_ROOT_PASSWORD, ETCD_USERNAME, and ETCD_PASSWORD environment variables."
fi
tail -f /dev/null
'
restart: always
networks:
- openim
kafka:
image: "${KAFKA_IMAGE}"
container_name: kafka
user: root
restart: always
volumes:
- "${DATA_DIR}/components/kafka:/bitnami/kafka"
environment:
#KAFKA_HEAP_OPTS: "-Xms128m -Xmx256m"
TZ: Asia/Shanghai
# Unique identifier for the Kafka node (required in controller mode)
KAFKA_CFG_NODE_ID: 0
# Defines the roles this Kafka node plays: broker, controller, or both
KAFKA_CFG_PROCESS_ROLES: controller,broker
# Specifies which nodes are controller nodes for quorum voting.
# The syntax follows the KRaft mode (no ZooKeeper): node.id@host:port
# The controller listener endpoint here is kafka:9093
KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 0@kafka:9093
# Specifies which listener is used for controller-to-controller communication
KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER
# Default number of partitions for new topics
KAFKA_NUM_PARTITIONS: 8
# Whether to enable automatic topic creation
KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: "true"
# Kafka internal listeners; Kafka supports multiple ports with different protocols
# Each port is used for a specific purpose: INTERNAL for internal broker communication,
# CONTROLLER for controller communication, EXTERNAL for external client connections.
# These logical listener names are mapped to actual protocols via KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP
# In short, Kafka is listening on three logical ports: 9092 for internal communication,
# 9093 for controller traffic, and 9094 for external access.
KAFKA_CFG_LISTENERS: "INTERNAL://:9092,CONTROLLER://:9093,EXTERNAL://:9094"
# Addresses advertised to clients. INTERNAL://kafka:9092 uses the internal Docker service name 'kafka',
# so other containers can access Kafka via kafka:9092.
# EXTERNAL://localhost:19094 is the address external clients (e.g., in the LAN) should use to connect.
# If Kafka is deployed on a different machine than IM, 'localhost'(or 'kafka') should be replaced with the LAN IP.
KAFKA_CFG_ADVERTISED_LISTENERS: "INTERNAL://kafka:9092,EXTERNAL://kafka:9094"
# Maps logical listener names to actual protocols.
# Supported protocols include: PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL
KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: "CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT,INTERNAL:PLAINTEXT"
# Defines which listener is used for inter-broker communication within the Kafka cluster
KAFKA_CFG_INTER_BROKER_LISTENER_NAME: "INTERNAL"
# Authentication configuration variables - comment out to disable auth
# KAFKA_USERNAME: "openIM"
# KAFKA_PASSWORD: "openIM123"
command: >
/bin/sh -c '
if [ -n "$${KAFKA_USERNAME}" ] && [ -n "$${KAFKA_PASSWORD}" ]; then
echo "=== Kafka SASL Authentication ENABLED ==="
echo "Username: $${KAFKA_USERNAME}"
# Set environment variables for SASL authentication
export KAFKA_CFG_LISTENERS="SASL_PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:9094"
export KAFKA_CFG_ADVERTISED_LISTENERS="SASL_PLAINTEXT://kafka:9092,EXTERNAL://kafka:9094"
export KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP="CONTROLLER:PLAINTEXT,EXTERNAL:SASL_PLAINTEXT,SASL_PLAINTEXT:SASL_PLAINTEXT"
export KAFKA_CFG_SASL_ENABLED_MECHANISMS="PLAIN"
export KAFKA_CFG_SASL_MECHANISM_INTER_BROKER_PROTOCOL="PLAIN"
export KAFKA_CFG_INTER_BROKER_LISTENER_NAME="SASL_PLAINTEXT"
export KAFKA_CLIENT_USERS="$${KAFKA_USERNAME}"
export KAFKA_CLIENT_PASSWORDS="$${KAFKA_PASSWORD}"
fi
# Start Kafka with the configured environment
exec /opt/bitnami/scripts/kafka/entrypoint.sh /opt/bitnami/scripts/kafka/run.sh
'
networks:
- openim
minio:
image: "${MINIO_IMAGE}"
ports:
- "${MINIO_PORT}:9000"
- "${MINIO_CONSOLE_PORT}:9090"
container_name: minio
volumes:
- "${DATA_DIR}/components/mnt/data:/data"
- "${DATA_DIR}/components/mnt/config:/root/.minio"
environment:
TZ: Asia/Shanghai
MINIO_ROOT_USER: "${MINIO_ACCESS_KEY_ID}"
MINIO_ROOT_PASSWORD: "${MINIO_SECRET_ACCESS_KEY}"
restart: always
command: minio server /data --console-address ':9090'
networks:
- openim
prometheus:
image: ${PROMETHEUS_IMAGE}
container_name: prometheus
restart: always
user: root
profiles:
- m
volumes:
- ./config/prometheus.yml:/etc/prometheus/prometheus.yml
- ./config/instance-down-rules.yml:/etc/prometheus/instance-down-rules.yml
- ${DATA_DIR}/components/prometheus/data:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--web.listen-address=:${PROMETHEUS_PORT}"
network_mode: host
alertmanager:
image: ${ALERTMANAGER_IMAGE}
container_name: alertmanager
restart: always
profiles:
- m
volumes:
- ./config/alertmanager.yml:/etc/alertmanager/alertmanager.yml
- ./config/email.tmpl:/etc/alertmanager/email.tmpl
command:
- "--config.file=/etc/alertmanager/alertmanager.yml"
- "--web.listen-address=:${ALERTMANAGER_PORT}"
network_mode: host
grafana:
image: ${GRAFANA_IMAGE}
container_name: grafana
user: root
restart: always
profiles:
- m
environment:
- GF_SECURITY_ALLOW_EMBEDDING=true
- GF_SESSION_COOKIE_SAMESITE=none
- GF_SESSION_COOKIE_SECURE=true
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
- GF_SERVER_HTTP_PORT=${GRAFANA_PORT}
volumes:
- ${DATA_DIR:-./}/components/grafana:/var/lib/grafana
network_mode: host
node-exporter:
image: ${NODE_EXPORTER_IMAGE}
container_name: node-exporter
restart: always
profiles:
- m
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- "--path.procfs=/host/proc"
- "--path.sysfs=/host/sys"
- "--path.rootfs=/rootfs"
- "--web.listen-address=:${NODE_EXPORTER_PORT}"
network_mode: host
openim-server:
image: ${OPENIM_SERVER_IMAGE}
container_name: openim-server
init: true
ports:
- "${OPENIM_MSG_GATEWAY_PORT}:10001"
- "${OPENIM_API_PORT}:10002"
healthcheck:
test: ["CMD", "sh", "-c", "mage check"]
interval: 5s
timeout: 60s
retries: 10
environment:
- IMENV_MONGODB_ADDRESS=${MONGO_ADDRESS}
- IMENV_MONGODB_USERNAME=${MONGO_USERNAME}
- IMENV_MONGODB_PASSWORD=${MONGO_PASSWORD}
- IMENV_KAFKA_ADDRESS=${KAFKA_ADDRESS}
- IMENV_KAFKA_USERNAME=${KAFKA_USERNAME}
- IMENV_KAFKA_PASSWORD=${KAFKA_PASSWORD}
- IMENV_DISCOVERY_ETCD_ADDRESS=${ETCD_ADDRESS}
- IMENV_REDIS_ADDRESS=${REDIS_ADDRESS}
- IMENV_REDIS_PASSWORD=${REDIS_PASSWORD}
- IMENV_DISCOVERY_ETCD_USERNAME=${ETCD_USERNAME}
- IMENV_DISCOVERY_ETCD_PASSWORD=${ETCD_PASSWORD}
- IMENV_MINIO_INTERNALADDRESS=${MINIO_INTERNAL_ADDRESS}
- IMENV_MINIO_EXTERNALADDRESS=${MINIO_EXTERNAL_ADDRESS}
- IMENV_MINIO_ACCESSKEYID=${MINIO_ACCESS_KEY_ID}
- IMENV_MINIO_SECRETACCESSKEY=${MINIO_SECRET_ACCESS_KEY}
- IMENV_SHARE_SECRET=${OPENIM_SECRET}
- IMENV_LOG_ISSTDOUT=${LOG_IS_STDOUT}
- IMENV_LOG_REMAINLOGLEVEL=${LOG_LEVEL}
- IMENV_OPENIM_API_PROMETHEUS_GRAFANAURL=${GRAFANA_URL}
restart: always
depends_on:
- mongo
- redis
- etcd
- kafka
- minio
networks:
- openim
# LiveKit:语音/视频通话的 SFUApache 2.0,自部署。
# 客户端通过 ws://<SERVER_IP>:7880 连接,凭 LIVEKIT_API_KEY/SECRET 签发的 token 进房。
livekit:
image: ${LIVEKIT_IMAGE}
container_name: livekit
restart: always
command: --config /etc/livekit.yaml --node-ip ${SERVER_IP}
environment:
LIVEKIT_KEYS: "${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}"
ports:
- "${LIVEKIT_PORT}:7880" # HTTP / WebSocket 信令
- "7881:7881" # RTC over TCPUDP 不通时的备用通道)
- "7882:7882/udp" # RTC 媒体(UDP 复用端口)
volumes:
- ./config/livekit.yaml:/etc/livekit.yaml
networks:
- openim
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# OpenIM + LiveKit 内网测试环境一键部署
# 用法: ./scripts/deploy.sh [服务器内网IP] (不传则自动探测)
set -euo pipefail
cd "$(dirname "$0")/.."
# 1. 首次运行从模板生成 .env
if [ ! -f .env ]; then
cp .env.example .env
echo "已从 .env.example 生成 .env"
fi
# 2. 确定服务器内网 IP
IP="${1:-}"
if [ -z "$IP" ]; then
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
fi
if [ -z "$IP" ]; then
echo "无法自动获取内网 IP,请手动传入: ./scripts/deploy.sh <服务器IP>"
exit 1
fi
echo "使用服务器 IP: $IP"
# 3. 把 IP 写进 .env(MinIO 外链地址必须是客户端能访问的地址)
sed -i "s|^SERVER_IP=.*|SERVER_IP=${IP}|" .env
sed -i "s|^MINIO_EXTERNAL_ADDRESS=.*|MINIO_EXTERNAL_ADDRESS=\"http://${IP}:10005\"|" .env
sed -i "s|^GRAFANA_URL=.*|GRAFANA_URL=\"http://${IP}:13000/\"|" .env
# 4. 预创建挂载目录(避免 docker 把文件路径自动建成目录)
mkdir -p components/redis/config components/mnt/data components/mongodb components/etcd components/kafka
touch components/redis/config/redis.conf
# 5. 拉镜像并启动
docker compose pull
docker compose up -d
# 6. 等待 openim-server 健康(首次启动约 30-60 秒)
echo "等待 openim-server 就绪(最长约 2 分钟)..."
status="unknown"
for _ in $(seq 1 24); do
status=$(docker inspect -f '{{.State.Health.Status}}' openim-server 2>/dev/null || echo "unknown")
[ "$status" = "healthy" ] && break
sleep 5
done
docker compose ps
echo
if [ "$status" = "healthy" ]; then
echo "部署完成。下一步运行自测: ./scripts/selftest.sh ${IP}"
else
echo "openim-server 尚未 healthy(当前: ${status})。可执行 docker compose logs -f openim-server 查看日志。"
fi
+8
View File
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# 重启测试环境。用法: ./scripts/restart.sh —— 重启全部服务
# ./scripts/restart.sh livekit —— 只重启指定服务
set -euo pipefail
cd "$(dirname "$0")/.."
docker compose restart "$@"
echo "已重启。OpenIM 完全就绪约需 30-60 秒,查看状态: docker compose ps"
+187
View File
@@ -0,0 +1,187 @@
#!/usr/bin/env bash
# OpenIM + LiveKit 测试环境自测脚本
# 覆盖:管理员令牌、注册两个测试账号、签发用户令牌(登录)、
# 互发文字/语音/文件消息、消息回查、LiveKit 一对一房间双端进房与媒体发布订阅。
# 用法: ./scripts/selftest.sh [服务器内网IP]
set -uo pipefail
cd "$(dirname "$0")/.."
# ---------- 参数与公共函数 ----------
env_get() { grep -E "^$1=" .env | head -1 | cut -d= -f2- | tr -d '"'; }
IP="${1:-$(env_get SERVER_IP)}"
IP="${IP:-127.0.0.1}"
API="http://${IP}:10002"
SECRET="$(env_get OPENIM_SECRET)"; SECRET="${SECRET:-openIM123}"
MINIO_AK="$(env_get MINIO_ACCESS_KEY_ID)"; MINIO_AK="${MINIO_AK:-root}"
MINIO_SK="$(env_get MINIO_SECRET_ACCESS_KEY)"; MINIO_SK="${MINIO_SK:-openIM123}"
MINIO_PORT="$(env_get MINIO_PORT)"; MINIO_PORT="${MINIO_PORT:-10005}"
LK_KEY="$(env_get LIVEKIT_API_KEY)"
LK_SECRET="$(env_get LIVEKIT_API_SECRET)"
LK_HTTP="http://${IP}:7880"
LK_WS="ws://${IP}:7880"
LK_CLI_IMAGE="livekit/livekit-cli:v2.18.2"
MC_IMAGE="minio/mc:latest"
FAILED=0
pass() { echo " [通过] $1"; }
fail() { echo " [失败] $1"; FAILED=1; }
note() { echo " [提示] $1"; }
# 优先用宿主机 curl;没有则在容器里跑 curl
if command -v curl >/dev/null 2>&1; then
http_post() { curl -s -m 15 -X POST "$1" -H 'Content-Type: application/json' ${TOKEN:+-H "token: $TOKEN"} -d "$2"; }
else
note "宿主机无 curl,使用容器化 curl"
http_post() { docker run --rm --network host curlimages/curl:8.10.1 -s -m 15 -X POST "$1" -H 'Content-Type: application/json' ${TOKEN:+-H "token: $TOKEN"} -d "$2"; }
fi
json_ok() { echo "$1" | tr -d ' \n' | grep -q '"errCode":0'; }
json_field() { echo "$1" | sed -n "s/.*\"$2\":\"\([^\"]*\)\".*/\1/p" | head -1; }
echo "==============================================="
echo " OpenIM 自测 (API: $API)"
echo "==============================================="
# ---------- 1. 管理员令牌 ----------
echo "1. 获取管理员令牌(/auth/get_admin_token"
TOKEN=""
resp=$(http_post "$API/auth/get_admin_token" "{\"secret\":\"${SECRET}\",\"userID\":\"imAdmin\"}")
if json_ok "$resp" && [ -n "$(json_field "$resp" token)" ]; then
TOKEN=$(json_field "$resp" token)
pass "管理员令牌获取成功"
else
fail "管理员令牌获取失败: $resp"
echo "OpenIM API 不可用,终止自测。"; exit 1
fi
# ---------- 2. 注册两个测试账号 ----------
echo "2. 注册测试账号 test001 / test002/user/user_register"
resp=$(http_post "$API/user/user_register" "{\"secret\":\"${SECRET}\",\"users\":[{\"userID\":\"test001\",\"nickname\":\"测试一号\"},{\"userID\":\"test002\",\"nickname\":\"测试二号\"}]}")
if json_ok "$resp"; then
pass "两个测试账号注册成功"
else
# 重复执行脚本时账号已存在,属于正常情况
note "注册接口返回非 0(通常是账号已存在,重复自测属正常): $(echo "$resp" | head -c 200)"
fi
# ---------- 3. 签发用户令牌(登录) ----------
echo "3. 签发用户令牌(/auth/get_user_tokenplatformID=5"
resp=$(http_post "$API/auth/get_user_token" "{\"secret\":\"${SECRET}\",\"platformID\":5,\"userID\":\"test001\"}")
T1=$(json_field "$resp" token)
resp=$(http_post "$API/auth/get_user_token" "{\"secret\":\"${SECRET}\",\"platformID\":5,\"userID\":\"test002\"}")
T2=$(json_field "$resp" token)
if [ -n "$T1" ] && [ -n "$T2" ]; then
pass "test001 / test002 均拿到用户令牌(登录链路可用)"
else
fail "用户令牌签发失败"
fi
# ---------- 4. 互发文字消息 ----------
MARK="selftest-$(date +%s)"
echo "4. test001 -> test002 发送文字消息(/msg/send_msg"
resp=$(http_post "$API/msg/send_msg" "{\"sendID\":\"test001\",\"recvID\":\"test002\",\"senderNickname\":\"测试一号\",\"senderPlatformID\":5,\"content\":{\"content\":\"自测文字 ${MARK}\"},\"contentType\":101,\"sessionType\":1}")
if json_ok "$resp"; then pass "文字消息发送成功"; else fail "文字消息发送失败: $(echo "$resp" | head -c 200)"; fi
echo " test002 -> test001 回复文字消息"
resp=$(http_post "$API/msg/send_msg" "{\"sendID\":\"test002\",\"recvID\":\"test001\",\"senderNickname\":\"测试二号\",\"senderPlatformID\":5,\"content\":{\"content\":\"收到 ${MARK}\"},\"contentType\":101,\"sessionType\":1}")
if json_ok "$resp"; then pass "回复消息发送成功"; else fail "回复消息发送失败: $(echo "$resp" | head -c 200)"; fi
# ---------- 5. 上传文件到 MinIO 并发送语音/文件消息 ----------
echo "5. 上传附件到 MinIO 并发送语音、文件消息"
mkdir -p .selftest
echo "openim selftest file ${MARK}" > .selftest/selftest-file.txt
head -c 4096 /dev/urandom 2>/dev/null > .selftest/selftest-voice.amr || printf 'fake-amr-%s' "$MARK" > .selftest/selftest-voice.amr
FILE_SIZE=$(wc -c < .selftest/selftest-file.txt | tr -d ' ')
VOICE_SIZE=$(wc -c < .selftest/selftest-voice.amr | tr -d ' ')
MCHOST="http://${MINIO_AK}:${MINIO_SK}@${IP}:${MINIO_PORT}"
mc_run() { docker run --rm --network host -v "$PWD/.selftest:/data" -e "MC_HOST_local=${MCHOST}" --entrypoint mc "$MC_IMAGE" "$@"; }
if mc_run cp "/data/selftest-file.txt" "local/openim/selftest/${MARK}/" >/dev/null 2>&1 \
&& mc_run cp "/data/selftest-voice.amr" "local/openim/selftest/${MARK}/" >/dev/null 2>&1; then
FILE_URL=$(mc_run share download --expire 168h "local/openim/selftest/${MARK}/selftest-file.txt" 2>/dev/null | grep -o 'http[^ ]*' | head -1)
VOICE_URL=$(mc_run share download --expire 168h "local/openim/selftest/${MARK}/selftest-voice.amr" 2>/dev/null | grep -o 'http[^ ]*' | head -1)
if [ -n "$FILE_URL" ] && [ -n "$VOICE_URL" ]; then
pass "附件已上传 MinIO 并生成访问链接"
resp=$(http_post "$API/msg/send_msg" "{\"sendID\":\"test001\",\"recvID\":\"test002\",\"senderNickname\":\"测试一号\",\"senderPlatformID\":5,\"content\":{\"uuid\":\"\",\"soundPath\":\"\",\"sourceUrl\":\"${VOICE_URL}\",\"dataSize\":${VOICE_SIZE},\"duration\":3},\"contentType\":103,\"sessionType\":1}")
if json_ok "$resp"; then pass "语音消息发送成功"; else fail "语音消息发送失败: $(echo "$resp" | head -c 200)"; fi
resp=$(http_post "$API/msg/send_msg" "{\"sendID\":\"test001\",\"recvID\":\"test002\",\"senderNickname\":\"测试一号\",\"senderPlatformID\":5,\"content\":{\"filePath\":\"\",\"uuid\":\"\",\"sourceUrl\":\"${FILE_URL}\",\"fileName\":\"selftest-file.txt\",\"fileSize\":${FILE_SIZE}},\"contentType\":105,\"sessionType\":1}")
if json_ok "$resp"; then pass "文件消息发送成功"; else fail "文件消息发送失败: $(echo "$resp" | head -c 200)"; fi
else
fail "MinIO 生成下载链接失败"
fi
else
fail "附件上传 MinIO 失败(检查 minio 容器与 ${IP}:${MINIO_PORT} 连通性)"
fi
# ---------- 6. 消息回查 ----------
echo "6. 消息落库回查(/msg/search_msg"
sleep 2
resp=$(http_post "$API/msg/search_msg" "{\"sendID\":\"test001\",\"recvID\":\"test002\",\"contentType\":0,\"sendTime\":\"\",\"sessionType\":1,\"pagination\":{\"pageNumber\":1,\"showNumber\":20}}")
if json_ok "$resp" && echo "$resp" | grep -q "$MARK"; then
pass "文字消息已落库可回查"
else
fail "消息回查未找到刚发送的内容: $(echo "$resp" | head -c 200)"
fi
if echo "$resp" | grep -q "selftest-voice.amr"; then pass "语音消息已落库可回查"; else fail "语音消息回查未找到"; fi
if echo "$resp" | grep -q "selftest-file.txt"; then pass "文件消息已落库可回查"; else fail "文件消息回查未找到"; fi
# ---------- 7. LiveKit 一对一语音房间 ----------
echo "==============================================="
echo " LiveKit 自测 (信令: $LK_WS)"
echo "==============================================="
ROOM="selftest-$(date +%s)"
echo "7. 两名用户进入同一房间,主叫发布音频,验证双向可见"
cleanup_lk() { docker rm -f lk-caller lk-callee >/dev/null 2>&1 || true; }
trap cleanup_lk EXIT
cleanup_lk
lk() { docker run --rm --network host "$LK_CLI_IMAGE" --url "$1" --api-key "$LK_KEY" --api-secret "$LK_SECRET" "${@:2}"; }
docker run -d --name lk-caller --network host "$LK_CLI_IMAGE" \
--url "$LK_WS" --api-key "$LK_KEY" --api-secret "$LK_SECRET" \
room join --identity caller --publish-demo "$ROOM" >/dev/null 2>&1
docker run -d --name lk-callee --network host "$LK_CLI_IMAGE" \
--url "$LK_WS" --api-key "$LK_KEY" --api-secret "$LK_SECRET" \
room join --identity callee "$ROOM" >/dev/null 2>&1
sleep 10
plist=$(lk "$LK_HTTP" room participants list "$ROOM" 2>&1)
if echo "$plist" | grep -q "caller" && echo "$plist" | grep -q "callee"; then
pass "双方已进入同一房间(房间: $ROOM"
else
fail "房间成员不符,list 输出: $(echo "$plist" | head -c 300)"
fi
# 主叫发布了媒体轨道(publish-demo 会发布音视频 demo 轨)
if echo "$plist" | grep -qi "MICROPHONE\|AUDIO"; then
pass "主叫音频轨道已发布"
else
note "未从成员列表确认音频轨道(可能输出格式差异),请人工核对: lk room participants list"
fi
# 被叫端日志应出现订阅到轨道的记录
if docker logs lk-callee 2>&1 | grep -qi "track"; then
pass "被叫端已接收到主叫的媒体轨道(一对一语音链路打通)"
else
fail "被叫端日志未见轨道订阅记录: $(docker logs lk-callee 2>&1 | tail -5)"
fi
cleanup_lk
trap - EXIT
# ---------- 汇总 ----------
echo "==============================================="
if [ "$FAILED" = "0" ]; then
echo " 自测全部通过 ✔"
echo " 客户端接入参数:"
echo " OpenIM API : $API"
echo " OpenIM WS : ws://${IP}:10001"
echo " LiveKit WS : $LK_WS (key: $LK_KEY)"
else
echo " 自测存在失败项,请根据上方 [失败] 排查(docker compose logs -f openim-server / livekit"
exit 1
fi