193 lines
10 KiB
Bash
Executable File
193 lines
10 KiB
Bash
Executable File
#!/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_PORT="$(env_get LIVEKIT_PORT)"; LK_PORT="${LK_PORT:-7880}"
|
||
LK_HTTP="http://${IP}:${LK_PORT}"
|
||
LK_WS="ws://${IP}:${LK_PORT}"
|
||
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
|
||
# OpenIM 所有 API 都要求 operationID 请求头(链路追踪用,每次请求唯一即可)
|
||
new_opid() { echo "selftest-$(date +%s%N 2>/dev/null || date +%s)-$RANDOM"; }
|
||
if command -v curl >/dev/null 2>&1; then
|
||
http_post() { curl -s -m 15 -X POST "$1" -H 'Content-Type: application/json' -H "operationID: $(new_opid)" ${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' -H "operationID: $(new_opid)" ${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}"; }
|
||
|
||
# 主叫发布仓库内置的 30 秒 opus 测试音频(--publish-demo 只有视频轨,这里用真实音频)
|
||
docker run -d --name lk-caller --network host -v "$PWD/scripts/assets:/data:ro" "$LK_CLI_IMAGE" \
|
||
--url "$LK_WS" --api-key "$LK_KEY" --api-secret "$LK_SECRET" \
|
||
room join --identity caller --publish /data/test-tone.ogg "$ROOM" >/dev/null 2>&1
|
||
# 被叫端必须显式开自动订阅(livekit-cli v2 的 --auto-subscribe 默认为 false)
|
||
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 --auto-subscribe "$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
|
||
|
||
# 主叫应已发布音频轨(kind=AUDIO)
|
||
if echo "$plist" | grep -qi "AUDIO"; then
|
||
pass "主叫音频轨道已发布"
|
||
else
|
||
note "未从成员列表确认音频轨道(可能输出格式差异),请人工核对: lk room participants list"
|
||
fi
|
||
|
||
# 被叫端日志应出现 track subscribed(媒体链路打通的直接证据)
|
||
if docker logs lk-callee 2>&1 | grep -qi "track subscribed"; 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
|