#!/bin/bash
# ========================================================
# WordPress + Workerman WebSocket IM 一键部署脚本(增强版)
# 功能:自动创建插件目录、安装 Composer、生成服务文件、前端测试页、Nginx 配置、systemd 服务
# 特性:支持 JWT 认证、用户绑定、日志追踪、权限控制、生产级配置
# 使用:chmod +x install-wp-im-enhanced.sh && ./install-wp-im-enhanced.sh --domain example.com --site-root /var/www/html
# ========================================================
set -euo pipefail # 更严格的错误处理
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo_red() { echo -e "${RED}$1${NC}"; }
echo_green() { echo -e "${GREEN}$1${NC}"; }
echo_yellow() { echo -e "${YELLOW}$1${NC}"; }
echo_blue() { echo -e "${BLUE}$1${NC}"; }
============ 参数解析 ============
DOMAIN=""
SITE_ROOT=""
HELP=false
while [[ "$#" -gt 0 ]]; do
case $1 in
--domain) DOMAIN="$2"; shift ;;
--site-root) SITE_ROOT="$2"; shift ;;
-h|--help) HELP=true; break ;;
*) echo_red "❌ 未知参数: $1"; exit 1 ;;
esac
shift
done
if [ "$HELP" = true ]; then
cat << 'EOF'
用法: ./install-wp-im-enhanced.sh --domain <域名> --site-root <网站根目录>
示例:
./install-wp-im-enhanced.sh --domain chat.example.com --site-root /www/wwwroot/chat_example_com
功能:
✅ 创建 WordPress 插件目录
✅ 自动生成 im-server.php (基于 Workerman + Composer)
✅ 自动安装 Composer 并引入 workerman/workerman
✅ 生成带用户识别的前端测试页面 websocket-test.html
✅ 生成 systemd 开机自启服务
✅ 输出 Nginx 反向代理配置片段
✅ 支持 JWT Token 身份认证(防止伪造连接)
注意事项:
- 确保已安装 PHP CLI 和允许 exec 函数
- 需要开放 443 (HTTPS) 和内部 2121 端口
- 推荐运行用户为 www 或 nginx
EOF
exit 0
fi
if [[ -z "$DOMAIN" || -z "$SITE_ROOT" ]]; then
echo_red "❌ 错误: 必须指定 --domain 和 --site-root"
echo "运行 $0 --help 查看帮助"
exit 1
fi
============ 变量定义 ============
PLUGIN_NAME="wp-im-plugin"
PLUGIN_DIR="$SITE_ROOT/wp-content/plugins/$PLUGIN_NAME"
IM_SERVER="$PLUGIN_DIR/im-server.php"
WP_PLUGIN_MAIN="$PLUGIN_DIR/$PLUGIN_NAME.php"
FRONTEND_TEST="$SITE_ROOT/websocket-test.html"
NGINX_CONF="/www/server/panel/vhost/nginx/${DOMAIN}.conf"
if [[ ! -f "$NGINX_CONF" ]]; then
NGINX_CONF="/etc/nginx/sites-available/$DOMAIN.conf"
fi
SYSTEMD_SERVICE="/etc/systemd/system/wp-im-server.service"
LOG_FILE="/var/log/wp-im-server.log"
WORKER_USER="www-data"
if id "www" &>/dev/null; then WORKER_USER="www"; fi
SECRET_KEY=$(openssl rand -base64 32 | tr -d '\n=' | cut -c1-24)
echo_blue "🚀 开始部署增强型 WebSocket IM 系统"
echo "🌐 域名: $DOMAIN"
echo "📁 网站根目录: $SITE_ROOT"
echo "🔌 插件路径: $PLUGIN_DIR"
============ 1. 创建插件目录 ============
echo_blue "📁 创建插件目录..."
mkdir -p "$PLUGIN_DIR"
============ 2. 生成 im-server.php (支持 JWT 认证) ============
cat > "$IM_SERVER" << EOF
#!/usr/bin/env php
<?php
/**
* Enhanced Workerman WebSocket Server with JWT Authentication
* 文件: $IM_SERVER
*/
use Workerman\\Worker;
use Workerman\\Connection\\TcpConnection;
use Firebase\\JWT\\JWT;
// 引入 Composer 自动加载器
require_once __DIR__ . '/vendor/autoload.php';
\$secret_key = '$SECRET_KEY'; // 应存储于 wp-config.php 或环境变量中
\$worker = new Worker('websocket://0.0.0.0:2121');
\$worker->name = 'WP_IM_Server';
\$worker->count = 1;
\$worker->user = '$WORKER_USER';
\$worker->connectionsById = [];
\$worker->onWorkerStart = function () {
file_put_contents('/tmp/workerman_stdout.log', "✅ IM Server started at " . date('Y-m-d H:i:s') . "\\n", FILE_APPEND);
};
\$worker->onConnect = function (TcpConnection \$conn) {
\$conn->userId = null;
\$conn->userName = '匿名';
file_put_contents('/tmp/workerman_stdout.log', "🔗 新连接来自 {\$conn->getRemoteIp()}:{\$conn->getRemotePort()}\\n", FILE_APPEND);
};
\$worker->onMessage = function (TcpConnection \$conn, \$data) use (\$worker, \$secret_key) {
try {
\$msg = json_decode(\$data, true, 512, JSON_THROW_ON_ERROR);
if (\$msg['type'] === 'auth') {
\$token = \$msg['token'] ?? '';
if (empty(\$token)) {
\$conn->send(json_encode(['type' => 'error', 'message' => '缺少认证 Token']));
return;
}
try {
\$decoded = JWT::decode(\$token, new Firebase\\JWT\\Key(\$secret_key, 'HS256'));
\$userId = \$decoded->sub;
\$userName = htmlspecialchars(\$decoded->name ?? "用户{\$userId}");
\$conn->userId = \$userId;
\$conn->userName = \$userName;
// 存储按 ID 查找连接
\$worker->connectionsById[\$userId] = \$conn;
// 广播上线消息
\$online_msg = json_encode([
'type' => 'user_online',
'userId' => \$userId,
'userName' => \$userName,
'time' => date('H:i')
]);
foreach (\$worker->connections as \$c) {
if (\$c !== \$conn) {
\$c->send(\$online_mux);
}
}
\$conn->send(json_encode([
'type' => 'authenticated',
'message' => "欢迎回来,{\$userName}"
]));
file_put_contents('/tmp/workerman_stdout.log', "👤 用户 {\$userId}({\$userName}) 已认证上线\\n", FILE_APPEND);
} catch (Exception \$e) {
\$conn->send(json_encode(['type' => 'error', 'message' => 'Token 无效']));
file_put_contents('/tmp/workerman_stdout.log', "⚠️ 认证失败: {\$e->getMessage()}\\n", FILE_APPEND);
\$conn->close();
}
return;
}
if (\$msg['type'] === 'chat') {
if (!\$conn->userId) {
\$conn->send(json_encode(['type' => 'error', 'message' => '请先登录']));
return;
}
\$content = htmlspecialchars(\$msg['content'] ?? '');
if (empty(\$content)) return;
\$fromName = \$conn->userName;
\$fromId = \$conn->userId;
\$chat_msg = json_encode([
'type' => 'chat_message',
'fromUserId' => \$fromId,
'fromUserName' => \$fromName,
'content' => \$content,
'time' => date('H:i')
]);
foreach (\$worker->connections as \$c) {
\$c->send(\$chat_msg);
}
file_put_contents('/tmp/workerman_stdout.log', "💬 聊天: [{\$fromName}] {\$content}\\n", FILE_APPEND);
}
} catch (Exception \$e) {
file_put_contents('/tmp/workerman_stdout.log', "⚠️ 消息解析失败: {\$e->getMessage()}\\n", FILE_APPEND);
}
};
\$worker->onClose = function (TcpConnection \$conn) use (\$worker) {
if (\$conn->userId && \$conn->userName) {
unset(\$worker->connectionsById[\$conn->userId]);
\$offline_msg = json_encode([
'type' => 'user_offline',
'userId' => \$conn->userId,
'userName' => \$conn->userName,
'time' => date('H:i')
]);
foreach (\$worker->connections as \$c) {
if (\$c !== \$conn) {
\$c->send(\$offline_msg);
}
}
file_put_contents('/tmp/workerman_stdout.log', "👋 用户 {\$conn->userId}({\$conn->userName}) 下线\\n", FILE_APPEND);
}
};
if (basename(__FILE__) == 'im-server.php') {
Worker::runAll();
}
EOF
chmod +x "$IM_SERVER"
echo_green "✅ 已生成并授权 im-server.php (支持 JWT)"
============ 3. 初始化 Composer 并安装依赖 ============
echo_blue "📦 初始化 Composer 并安装 workerman/workerman + firebase/php-jwt..."
cd "$PLUGIN_DIR"
if [ ! -f "composer.json" ]; then
cat > composer.json << 'EOF'
{
"name": "vendor/wp-im-plugin",
"description": "Real-time WebSocket IM for WordPress",
"type": "project",
"require": {
"workerman/workerman": "^4.1",
"firebase/php-jwt": "^6.0"
},
"config": {
"allow-plugins": true
},
"autoload": {
"psr-4": {
"IMPlugin\\": "src/"
}
}
}
EOF
echo_green "📄 已创建 composer.json"
else
echo_yellow "📄 composer.json 已存在,跳过创建"
fi
if [ ! -d "vendor" ] || [ ! -f "vendor/autoload.php" ]; then
if ! command -v composer &> /dev/null; then
echo_blue "⬇️ Composer 未安装,正在全局安装..."
curl -sS https://getcomposer.org/installer | php >/dev/null 2>&1
sudo mv composer.phar /usr/local/bin/composer
echo_green "✅ Composer 已安装到 /usr/local/bin/composer"
fi
echo_blue "⬇️ 正在执行 composer install..."
COMPOSER_PROCESS_TIMEOUT=300 composer install --no-dev --optimize-autoloader --quiet
echo_green "✅ Workerman 与 JWT 依赖安装完成"
else
echo_green "✅ vendor 目录已存在,跳过 Composer 安装"
fi
============ 4. 生成 WordPress 主插件文件(注入 JWT) ============
cat > "$WP_PLUGIN_MAIN" << EOF
<?php
/**
* Plugin Name: WP IM Plugin (Enhanced)
* Description: 实时聊天系统,集成 Workerman WebSocket 与 JWT 身份认证
* Version: 1.1
* Author: Admin
*/
if (!defined('ABSPATH')) exit;
function wp_im_generate_jwt_token() {
if (!is_user_logged_in()) return '';
\$user = wp_get_current_user();
\$payload = [
'sub' => \$user->ID,
'name' => \$user->display_name,
'iat' => time(),
'exp' => time() + 3600 // 1小时有效
];
\$secret = '$SECRET_KEY'; // 生产环境中应从 wp-config.php 获取
return \\Firebase\\JWT\\JWT::encode(\$payload, \$secret, 'HS256');
}
function wp_im_inject_client_script() {
\$token = wp_im_generate_jwt_token();
?>
<script>
window.WP_IM_CONFIG = {
wsUrl: "wss://$DOMAIN/im-ws",
token: "$token"
};
</script>
<?php
}
add_action('wp_head', 'wp_im_inject_client_script');
// 注册静态资源
add_action('wp_enqueue_scripts', function() {
wp_register_script('im-client', get_site_url() . '/websocket-test.js', [], '1.0', true);
});
EOF
echo_green "✅ 已生成 WordPress 插件主文件: $WP_PLUGIN_MAIN"
============ 5. 生成前端测试页面 HTML + JS ============
cat > "$FRONTEND_TEST" << 'EOF'
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>WebSocket IM 测试</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; background: #f4f6f9; }
#status { margin: 10px 0; font-weight: bold; color: #d9534f; }
#logs {
list-style: none; padding: 0; max-height: 400px; overflow-y: auto;
border: 1px solid #ddd; padding: 10px; background: white;
}
li { margin: 5px 0; padding: 5px; border-left: 3px solid #ccc; }
.sent { border-color: #0073aa; color: #0073aa; }
.received { border-color: #51a351; color: #51a351; }
input[type=text] { width: 300px; padding: 8px; margin-right: 10px; }
button { padding: 8px 15px; background: #0073aa; color: white; border: none; cursor: pointer; }
button:hover { background: #005a87; }
</style>
</head>
<body>
<h1>WebSocket IM 实时聊天测试</h1>
<div id="status">状态:等待连接...</div>
<ul id="logs"></ul>
<div style="margin-top: 20px;">
<input type="text" id="msgInput" placeholder="输入消息..." />
<button onclick="sendMsg()">发送</button>
</div>
<script>
const WS_URL = window.WP_IM_CONFIG?.wsUrl || "wss://DOMAIN/im-ws";
let ws = null;
let isConnected = false;
function log(msg, cls = "") {
const li = document.createElement("li");
li.className = cls;
li.textContent = new Date().toLocaleTimeString() + " - " + msg;
document.getElementById("logs").appendChild(li);
document.getElementById("logs").scrollTop = document.getElementById("logs").scrollHeight;
}
function connect() {
ws = new WebSocket(WS_URL);
document.getElementById("status").textContent = "🔄 连接中...";
document.getElementById("status").style.color = "#f0ad4e";
ws.onopen = () => {
log("✅ 连接成功!");
document.getElementById("status").textContent = "✅ 已连接";
document.getElementById("status").style.color = "#5cb85c";
const token = window.WP_IM_CONFIG?.token;
if (token) {
ws.send(JSON.stringify({ type: "auth", token }));
log("🔐 正在认证身份...");
} else {
log("ℹ️ 游客模式,无法发言", "received");
}
};
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
switch (data.type) {
case 'user_online':
log(`🟢 \${data.userName} 上线了`, "received"); break;
case 'user_offline':
log(`🔴 \${data.userName} 下线了`, "received"); break;
case 'chat_message':
log(\`\${data.time} [\${data.fromUserName}]: \${data.content}\`, "received"); break;
case 'authenticated':
log(\`👋 \${data.message}\`, "received"); break;
case 'error':
log(\`❌ 错误: \${data.message}\`, "received"); break;
}
};
ws.onerror = (e) => {
log("❌ WebSocket 错误发生");
document.getElementById("status").textContent = "❌ 连接出错";
document.getElementById("status").style.color = "#d9534f";
};
ws.onclose = (e) => {
log(\`⚠️ 连接关闭: \${e.code} \${e.reason}\`);
document.getElementById("status").textContent = "⚠️ 已断开 (尝试重连)";
setTimeout(connect, 3000); // 自动重连
};
}
function sendMsg() {
const input = document.getElementById("msgInput");
const val = input.value.trim();
if (!val) return alert("请输入内容");
if (!isConnected) return alert("尚未连接");
ws.send(JSON.stringify({ type: "chat", content: val }));
log("你: " + val, "sent");
input.value = "";
}
document.getElementById("msgInput").addEventListener("keypress", e => {
if (e.key === "Enter") sendMsg();
});
// 启动连接
connect();
</script>
</body>
</html>
EOF
sed -i "s|DOMAIN|$DOMAIN|g" "$FRONTEND_TEST"
echo_green "✅ 已生成前端测试页面: $FRONTEND_TEST"
============ 6. 生成 Nginx 配置建议 ============
echo_blue "📌【重要】请将以下 Nginx 配置添加到你的站点配置中:"
cat << 'EOF'
server {
listen 80;
server_name YOUR_DOMAIN;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name YOUR_DOMAIN;
ssl_certificate /ssl/fullchain.pem;
ssl_certificate_key /ssl/privkey.pem;
root /www/wwwroot/YOUR_SITE_ROOT;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$args;
}
# WebSocket 反向代理
location /im-ws {
proxy_pass http://127.0.0.1:2121;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
}
}
EOF
echo_green "📌 替换 YOUR_DOMAIN 和路径后保存,并重启 Nginx"
============ 7. 生成 systemd 服务 ============
cat > "$SYSTEMD_SERVICE" << EOF
[Unit]
Description=WordPress IM WebSocket Server
After=network.target
[Service]
Type=simple
User=$WORKER_USER
Group=$WORKER_USER
WorkingDirectory=$PLUGIN_DIR
ExecStart=/usr/bin/php im-server.php start
ExecStop=/usr/bin/php im-server.php stop
Restart=always
StandardOutput=append:$LOG_FILE
StandardError=append:$LOG_FILE
[Install]
WantedBy=multi-user.target
EOF
echo_green "✅ 已生成 systemd 服务: $SYSTEMD_SERVICE"
============ 8. 设置权限和日志 ============
sudo touch "$LOG_FILE"
sudo chown "$WORKER_USER:$WORKER_USER" "$LOG_FILE"
sudo chmod 644 "$LOG_FILE"
sudo chown -R "$WORKER_USER:$WORKER_USER" "$PLUGIN_DIR"
echo_green "✅ 日志文件已创建并设置权限"
echo_green "✅ 插件目录归属已设为 $WORKER_USER"
============ 9. 最终提示 ============
cat << EOF
🎉 =========================================
✅ WebSocket IM 系统部署完成!
你可以进行以下操作:
==========================================
🔧 1. 编辑 Nginx 配置:
sudo nano $NGINX_CONF
将上面的 location /im-ws {...} 块粘贴进 server {} 中
🔄 2. 重启 Nginx:
sudo systemctl restart nginx
▶️ 3. 启动 WebSocket 服务:
sudo systemctl daemon-reload
sudo systemctl enable wp-im-server
sudo systemctl start wp-im-server
🔍 4. 查看服务状态:
sudo systemctl status wp-im-server
📄 5. 查看运行日志:
tail -f /tmp/workerman_stdout.log
tail -f $LOG_FILE
🌐 6. 访问测试页面:
https://$DOMAIN/websocket-test.html
💡 注意事项:
- 🔐 SECRET_KEY: $SECRET_KEY (请备份或移至 wp-config.php)
- ⚠️ 确保防火墙放行 443 和本地 2121 端口
- 🛡️ 生产环境建议增加 IP 白名单或 WAF 规则
- 💬 扩展建议:可加入私聊、消息持久化、离线推送等
💪 你现在拥有一个安全、实时、可扩展的 IM 系统!
EOF
修复生成完整版源文件 配置部署