#!/bin/bash
# ========================================================
# WordPress + Workerman WebSocket IM 一键部署脚本
# 功能:自动创建插件、服务、Nginx代理、前端测试页
# 使用:chmod +x install-wp-im.sh && ./install-wp-im.sh --domain 域名 --site-root 网站根目录
# ========================================================
set -e # 出错即停止
# ============ 参数解析 ============
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 "未知参数: $1" >&2; exit 1 ;;
esac
shift
done
if [ "$HELP" = true ]; then
echo "用法: $0 --domain <域名> --site-root <网站根目录>"
echo "示例: $0 --domain yfw.szrengjing.com --site-root /www/wwwroot/yfw_szrengjing_com"
exit 0
fi
if [ -z "$DOMAIN" ] || [ -z "$SITE_ROOT" ]; then
echo "错误: 必须指定 --domain 和 --site-root"
echo "运行 $0 --help 查看帮助"
exit 1
fi
# ============ 变量定义 ============
PLUGIN_DIR="$SITE_ROOT/wp-content/plugins/wp-im-plugin"
IM_SERVER="$PLUGIN_DIR/im-server.php"
NGINX_CONF="/www/server/panel/vhost/nginx/${DOMAIN}.conf"
SYSTEMD_SERVICE="/etc/systemd/system/wp-im-server.service"
FRONTEND_TEST="$SITE_ROOT/websocket-test.html"
LOG_DIR="/tmp"
WORKER_USER="www"
echo "🚀 开始部署 WebSocket IM 系统"
echo "域名: $DOMAIN"
echo "网站根目录: $SITE_ROOT"
echo "插件路径: $PLUGIN_DIR"
# ============ 1. 创建插件目录 ============
echo "📁 创建插件目录..."
mkdir -p "$PLUGIN_DIR"
# ============ 2. 生成 im-server.php (Workerman 服务) ============
cat > "$IM_SERVER" << 'EOF'
<?php
/**
* Plugin Name: WP IM Plugin
* Description: 基于 Workerman 的 WebSocket 实时通信服务
*/
use Workerman\Worker;
use Workerman\Lib\Timer;
use Workerman\Connection\TcpConnection;
require_once __DIR__ . '/Workerman/Autoloader.php';
// 初始化 worker
$worker = new Worker('websocket://0.0.0.0:2121');
$worker->name = 'WP_IM_Server';
$worker->count = 1; // 单进程足够
$worker->user = 'www'; // 运行用户
// 存储所有连接的客户端
$connections = [];
$worker->onWorkerStart = function () use (&$connections) {
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) use (&$connections) {
$conn->userId = null;
$conn->userName = null;
file_put_contents('/tmp/workerman_stdout.log', "新连接来自 {$conn->getRemoteIp()}:{$conn->getRemotePort()}\n", FILE_APPEND);
};
$worker->onMessage = function (TcpConnection $conn, $data) use (&$connections, &$worker) {
try {
$msg = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
if ($msg['type'] === 'register') {
$userId = $msg['userId'] ?? '';
$userName = htmlspecialchars($msg['userName'] ?? '');
// 验证是否为合法用户 ID(数字)
if (!is_numeric($userId) || intval($userId) <= 0) {
$conn->send(json_encode(['type' => 'error', 'message' => '非法用户ID']));
$conn->close();
return;
}
$conn->userId = $userId;
$conn->userName = $userName;
// 广播上线消息(除自己外)
$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_msg);
}
}
// 欢迎自己
$conn->send(json_encode([
'type' => 'welcome',
'message' => "欢迎回来,{$userName}"
]));
file_put_contents('/tmp/workerman_stdout.log', "用户 {$userId}({$userName}) 上线\n", FILE_APPEND);
}
if ($msg['type'] === 'chat') {
$content = htmlspecialchars($msg['content'] ?? '');
if (empty($content)) return;
$fromName = $conn->userName ?: '匿名';
$fromId = $conn->userId ?: '0';
$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) {
$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
echo "✅ 已生成 im-server.php"
# ============ 3. 下载 Workerman 库 ============
echo "⏬ 下载 Workerman 依赖..."
WORKERMAN_ZIP="/tmp/workerman.zip"
curl -o "$WORKERMAN_ZIP" -L https://github.com/walkor/Workerman/archive/refs/tags/v4.1.16.zip
unzip -qo "$WORKERMAN_ZIP" "Workerman-*/*" -d "$PLUGIN_DIR"
mv "$PLUGIN_DIR"/Workerman-*/* "$PLUGIN_DIR/"
rm -rf "$PLUGIN_DIR"/Workerman-*/
rm -f "$WORKERMAN_ZIP"
echo "✅ Workerman 依赖已安装"
# ============ 4. 生成 WordPress 插件主文件 ============
cat > "$PLUGIN_DIR/wp-im-plugin.php" << EOF
<?php
/**
* Plugin Name: WP IM Plugin
* Description: 实时聊天系统,集成 Workerman WebSocket
* Version: 1.0
* Author: Admin
*/
// 阻止直接访问
if (!defined('ABSPATH')) exit;
// 添加前端脚本钩子
function wp_im_enqueue_scripts() {
wp_enqueue_script('wp-im-client', get_site_url() . '/websocket-test.html?js', array(), '1.0', true);
}
add_action('wp_enqueue_scripts', 'wp_im_enqueue_scripts');
// 提供给前端获取当前用户信息的接口
function wp_im_get_current_user_json() {
if (is_user_logged_in()) {
\$user = wp_get_current_user();
echo '<script>window.WP_IM_USER = {id: "' . esc_js(\$user->ID) . '", name: "' . esc_js(\$user->display_name) . '"};</script>';
} else {
echo '<script>window.WP_IM_USER = null;</script>';
}
}
add_action('wp_head', 'wp_im_get_current_user_json');
EOF
echo "✅ 已生成 WordPress 插件主文件"
# ============ 5. 生成前端测试页面 ============
cat > "$FRONTEND_TEST" << EOF
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>WebSocket 测试</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
#logs { list-style: none; padding: 0; max-height: 400px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; }
li { margin: 5px 0; color: #333; }
.sent { color: #0073aa; }
.received { color: #51a351; }
</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="输入消息" style="width: 300px; padding: 5px;" />
<button onclick="sendMsg()">发送</button>
</div>
<script>
const WS_URL = "wss://$DOMAIN/im-ws";
const ws = new WebSocket(WS_URL);
let userId = "guest_" + Date.now();
let userName = "游客";
function log(msg, cls = "") {
const li = document.createElement("li");
li.className = cls;
li.textContent = new Date().toLocaleTimeString() + " - " + msg;
document.getElementById("logs").appendChild(li);
console.log(msg);
}
ws.onopen = () => {
log("✅ 连接成功!");
document.getElementById("status").textContent = "✅ 已连接";
// 尝试从全局变量获取 WordPress 用户
if (typeof WP_IM_USER !== "undefined" && WP_IM_USER) {
userId = WP_IM_USER.id;
userName = WP_IM_USER.name;
} else {
userId = "test_user_" + Date.now();
userName = "测试用户";
}
ws.send(JSON.stringify({
type: "register",
userId: userId,
userName: userName
}));
};
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
if (data.type === 'user_online') {
log("🟢 " + data.userName + " 上线了", "received");
} else if (data.type === 'user_offline') {
log("🔴 " + data.userName + " 下线了", "received");
} else if (data.type === 'chat_message') {
log("💬 [" + data.time + "] " + data.fromUserName + ": " + data.content, "received");
} else if (data.type === 'welcome') {
log("👋 " + data.message, "received");
}
};
ws.onerror = (e) => {
log("❌ 错误: " + JSON.stringify(e));
document.getElementById("status").textContent = "❌ 连接失败";
};
ws.onclose = (e) => {
log(\`⚠️ 连接关闭: \${e.code} \${e.reason}\`);
document.getElementById("status").textContent = "⚠️ 已断开";
};
function sendMsg() {
const input = document.getElementById("msgInput");
const val = input.value.trim();
if (val) {
ws.send(JSON.stringify({ type: "chat", content: val }));
log("你: " + val, "sent");
input.value = "";
}
}
// 回车发送
document.getElementById("msgInput").addEventListener("keypress", (e) => {
if (e.key === "Enter") sendMsg();
});
</script>
</body>
</html>
EOF
echo "✅ 已生成前端测试页面: $FRONTEND_TEST"
# ============ 6. 生成 Nginx 配置片段 ============
NGINX_SNIPPET=$(cat << 'EOF'
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;
}
EOF
)
echo "📌 Nginx 配置建议(请复制到 $NGINX_CONF 的 server 块中):"
echo
echo "$NGINX_SNIPPET"
echo
echo "📌 保存后重启 Nginx: systemctl restart nginx"
# ============ 7. 生成 systemd 服务 ============
cat > "$SYSTEMD_SERVICE" << EOF
[Unit]
Description=WordPress IM WebSocket Server
After=network.target
[Service]
Type=simple
User=$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:/var/log/wp-im-server.log
StandardError=append:/var/log/wp-im-server.log
[Install]
WantedBy=multi-user.target
EOF
echo "✅ 已生成 systemd 服务: $SYSTEMD_SERVICE"
echo "启用命令: sudo systemctl enable wp-im-server && sudo systemctl start wp-im-server"
# ============ 8. 设置日志权限 ============
touch /var/log/wp-im-server.log
chown $WORKER_USER:$WORKER_USER /var/log/wp-im-server.log
chmod 644 /var/log/wp-im-server.log
# ============ 9. 提示完成 ============
echo
echo "🎉 部署完成!下一步操作:"
echo
echo "1. 🔧 编辑 Nginx 配置:"
echo " nano $NGINX_CONF"
echo " 将上面的 location /im-ws {...} 块粘贴进去"
echo
echo "2. 🔄 重启 Nginx:"
echo " systemctl restart nginx"
echo
echo "3. ▶️ 启动 WebSocket 服务:"
echo " sudo systemctl start wp-im-server"
echo
echo "4. 🌐 访问测试页:"
echo " https://$DOMAIN/websocket-test.html"
echo
echo "5. 📂 查看日志:"
echo " tail -f /tmp/workerman_stdout.log"
echo " tail -f /var/log/wp-im-server.log"
echo
echo "💡 提示:确保宝塔防火墙或云服务器安全组放行 443 端口。"
# ============ 3. 使用 Composer 安装 Workerman ============
echo "📦 初始化 Composer 并安装 Workerman..."
cd "$PLUGIN_DIR"
# 创建 composer.json(如果不存在)
if [ ! -f "composer.json" ]; then
cat > composer.json << 'EOF'
{
"name": "your-company/wp-im-plugin",
"description": "WebSocket IM plugin for WordPress",
"type": "project",
"require": {
"workerman/workerman": "^4.1"
},
"autoload": {
"psr-4": {}
},
"config": {
"allow-plugins": {
"composer/installers": true
}
}
}
EOF
fi
# 安装依赖(仅生产环境需要 autoload)
if [ ! -d "vendor" ] || [ ! -f "vendor/autoload.php" ]; then
if ! command -v composer &> /dev/null; then
echo "⚠️ Composer 未安装,尝试全局安装..."
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
fi
# 执行安装(不带 dev 依赖)
COMPOSER_PROCESS_TIMEOUT=200 composer install --no-dev --optimize-autoloader
else
echo "✅ vendor 已存在,跳过 Composer 安装"
fi
echo "✅ Workerman 已通过 Composer 安装"
请生成完整版源文件