From 33fa2339e22762d05976cfa706d4804c7ed9e7d1 Mon Sep 17 00:00:00 2001 From: KIMI Date: Sat, 8 Aug 2026 22:07:55 +0800 Subject: [PATCH] =?UTF-8?q?OpenIM=20+=20LiveKit=20=E5=86=85=E7=BD=91?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=8E=AF=E5=A2=83=E4=B8=80=E9=94=AE=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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: 部署记录(服务器要求/端口表/运维/接入参数) --- .env.example | 74 +++++++ .gitignore | 4 + README.md | 88 ++++++++ config/alertmanager.yml | 34 +++ config/email.tmpl | 36 +++ config/instance-down-rules.yml | 44 ++++ config/livekit.yaml | 15 ++ config/prometheus.yml | 119 ++++++++++ docker-compose.yaml | 387 +++++++++++++++++++++++++++++++++ scripts/deploy.sh | 52 +++++ scripts/restart.sh | 8 + scripts/selftest.sh | 187 ++++++++++++++++ 12 files changed, 1048 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config/alertmanager.yml create mode 100644 config/email.tmpl create mode 100644 config/instance-down-rules.yml create mode 100644 config/livekit.yaml create mode 100644 config/prometheus.yml create mode 100644 docker-compose.yaml create mode 100755 scripts/deploy.sh create mode 100755 scripts/restart.sh create mode 100755 scripts/selftest.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..046f6ac --- /dev/null +++ b/.env.example @@ -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://: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/" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9043519 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# 运行时生成,不入库 +.env +components/ +.selftest/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..7f00050 --- /dev/null +++ b/README.md @@ -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 兜底。 diff --git a/config/alertmanager.yml b/config/alertmanager.yml new file mode 100644 index 0000000..6c675ab --- /dev/null +++ b/config/alertmanager.yml @@ -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 diff --git a/config/email.tmpl b/config/email.tmpl new file mode 100644 index 0000000..824144e --- /dev/null +++ b/config/email.tmpl @@ -0,0 +1,36 @@ +{{ define "email.to.html" }} +{{ if eq .Status "firing" }} + {{ range .Alerts }} + +
+

OpenIM Alert

+

Alert Status: firing

+

Alert Program: Prometheus Alert

+

Severity Level: {{ .Labels.severity }}

+

Alert Type: {{ .Labels.alertname }}

+

Affected Host: {{ .Labels.instance }}

+

Affected Service: {{ .Labels.job }}

+

Alert Subject: {{ .Annotations.summary }}

+

Trigger Time: {{ .StartsAt.Format "2006-01-02 15:04:05" }}

+
+ {{ end }} + + +{{ else if eq .Status "resolved" }} + {{ range .Alerts }} + +
+

OpenIM Alert

+

Alert Status: resolved

+

Alert Program: Prometheus Alert

+

Severity Level: {{ .Labels.severity }}

+

Alert Type: {{ .Labels.alertname }}

+

Affected Host: {{ .Labels.instance }}

+

Affected Service: {{ .Labels.job }}

+

Alert Subject: {{ .Annotations.summary }}

+

Trigger Time: {{ .StartsAt.Format "2006-01-02 15:04:05" }}

+
+ {{ end }} + +{{ end }} +{{ end }} diff --git a/config/instance-down-rules.yml b/config/instance-down-rules.yml new file mode 100644 index 0000000..bcac7ba --- /dev/null +++ b/config/instance-down-rules.yml @@ -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." diff --git a/config/livekit.yaml b/config/livekit.yaml new file mode 100644 index 0000000..56a2bfe --- /dev/null +++ b/config/livekit.yaml @@ -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 diff --git a/config/prometheus.yml b/config/prometheus.yml new file mode 100644 index 0000000..a3c9574 --- /dev/null +++ b/config/prometheus.yml @@ -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 \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..20f9c96 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,387 @@ +# 内部通讯 App —— OpenIM + LiveKit 内网测试环境 +# 基于官方 openim-docker v3.8 裁剪: +# - 移除 openim-chat(GPLv3,本项目的注册登录后续自研) +# - 移除 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:语音/视频通话的 SFU,Apache 2.0,自部署。 + # 客户端通过 ws://: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 TCP(UDP 不通时的备用通道) + - "7882:7882/udp" # RTC 媒体(UDP 复用端口) + volumes: + - ./config/livekit.yaml:/etc/livekit.yaml + networks: + - openim diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..006705c --- /dev/null +++ b/scripts/deploy.sh @@ -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 diff --git a/scripts/restart.sh b/scripts/restart.sh new file mode 100755 index 0000000..697ba74 --- /dev/null +++ b/scripts/restart.sh @@ -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" diff --git a/scripts/selftest.sh b/scripts/selftest.sh new file mode 100755 index 0000000..c36bed6 --- /dev/null +++ b/scripts/selftest.sh @@ -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_token,platformID=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