Commit 81f3045fbb3813486091db31c24235ad5636e108

Authored by 谭苏航
0 parents

Initial commit

  1 +DB_HOST=140.210.199.111
  2 +DB_PORT=3306
  3 +DB_USER=root
  4 +DB_PASSWORD=Qnbar123!
  5 +DB_NAME=ai
  6 +
  7 +REDIS_HOST=10.10.1.232
  8 +REDIS_PORT=6379
  9 +REDIS_PASSWORD=qnbarcom
  10 +REDIS_PREFIX=ai_
  1 +FROM php:8.2-cli
  2 +
  3 +# Install dependencies and extensions
  4 +RUN apt-get update && apt-get install -y \
  5 + git \
  6 + unzip \
  7 + libzip-dev \
  8 + && docker-php-ext-install pcntl posix sockets zip pdo_mysql
  9 +
  10 +# Install Composer
  11 +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
  12 +
  13 +# Set working directory
  14 +WORKDIR /app
  15 +
  16 +# Copy dependency definition first
  17 +COPY composer.json ./
  18 +
  19 +# Install dependencies (if composer.lock exists it would be better, but we start fresh)
  20 +RUN composer install --no-scripts --no-autoloader
  21 +
  22 +# Copy application source
  23 +COPY . .
  24 +
  25 +# Generate autoloader
  26 +RUN composer dump-autoload --optimize
  27 +
  28 +# Expose WebSocket port
  29 +EXPOSE 8888
  30 +
  31 +# Command to run the Workerman server
  32 +CMD ["php", "start.php", "start"]
  1 +{
  2 + "name": "moltbot/relay-server",
  3 + "description": "IoT Relay Server for Clawdbot WeChat Integration",
  4 + "type": "project",
  5 + "license": "MIT",
  6 + "require": {
  7 + "php": ">=8.0",
  8 + "workerman/workerman": "^4.1",
  9 + "firebase/php-jwt": "^6.10",
  10 + "predis/predis": "^2.3"
  11 + },
  12 + "autoload": {
  13 + "psr-4": {
  14 + "Moltbot\\Relay\\": "src/"
  15 + }
  16 + }
  17 +}
  1 +services:
  2 + relay:
  3 + build: .
  4 + container_name: moltbot-relay
  5 + ports:
  6 + - "8888:8888"
  7 + env_file:
  8 + - .env
  9 + environment:
  10 + - REDIS_HOST=redis
  11 + - REDIS_PORT=6379
  12 +
  13 + redis:
  14 + image: redis:alpine
  15 + container_name: moltbot-redis
  16 + ports:
  17 + - "6379:6379"
  1 +<?php
  2 +require_once __DIR__ . '/vendor/autoload.php';
  3 +
  4 +// Load .env (simple loader since we don't have dotenv lib installed in the basic container,
  5 +// or relying on docker-compose env injection)
  6 +// Actually docker-compose injects env vars, so getenv() works.
  7 +
  8 +$host = getenv('DB_HOST');
  9 +$user = getenv('DB_USER');
  10 +$pass = getenv('DB_PASSWORD');
  11 +$name = getenv('DB_NAME');
  12 +$port = getenv('DB_PORT') ?: 3306;
  13 +
  14 +echo "Connecting to MySQL at $host...\n";
  15 +
  16 +try {
  17 + $dsn = "mysql:host=$host;port=$port;charset=utf8mb4";
  18 + $pdo = new PDO($dsn, $user, $pass, [
  19 + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
  20 + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
  21 + ]);
  22 +
  23 + echo "Connected successfully.\n";
  24 +
  25 + // Create Database if not exists
  26 + $pdo->exec("CREATE DATABASE IF NOT EXISTS `$name` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
  27 + $pdo->exec("USE `$name`");
  28 + echo "Selected database '$name'.\n";
  29 +
  30 + $sql = <<<SQL
  31 +-- Devices Table
  32 +CREATE TABLE IF NOT EXISTS `devices` (
  33 + `id` VARCHAR(64) NOT NULL COMMENT '设备ID (Device ID)',
  34 + `secret` VARCHAR(128) NOT NULL COMMENT '连接密钥 (Hash)',
  35 + `name` VARCHAR(50) DEFAULT NULL COMMENT '设备昵称',
  36 + `owner_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '绑定用户ID (NULL=未绑定)',
  37 + `status` ENUM('online', 'offline') DEFAULT 'offline' COMMENT '在线状态',
  38 + `last_seen` TIMESTAMP NULL DEFAULT NULL COMMENT '最后心跳时间',
  39 + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  40 + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  41 + PRIMARY KEY (`id`),
  42 + KEY `idx_owner` (`owner_id`)
  43 +) ENGINE=InnoDB COMMENT='设备管理表';
  44 +
  45 +-- Users Table
  46 +CREATE TABLE IF NOT EXISTS `users` (
  47 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  48 + `phone` VARCHAR(20) NOT NULL COMMENT '手机号',
  49 + `password` VARCHAR(255) NOT NULL COMMENT '密码 Hash',
  50 + `wx_openid` VARCHAR(64) DEFAULT NULL COMMENT '微信 OpenID',
  51 + `nickname` VARCHAR(50) DEFAULT NULL COMMENT '显示的昵称',
  52 + `avatar_url` VARCHAR(255) DEFAULT NULL,
  53 + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  54 + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  55 + PRIMARY KEY (`id`),
  56 + UNIQUE KEY `uk_phone` (`phone`),
  57 + UNIQUE KEY `uk_wx_openid` (`wx_openid`)
  58 +) ENGINE=InnoDB COMMENT='用户表';
  59 +
  60 +-- Device Logs Table
  61 +CREATE TABLE IF NOT EXISTS `device_logs` (
  62 + `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  63 + `device_id` VARCHAR(64) NOT NULL,
  64 + `level` VARCHAR(10) DEFAULT 'INFO',
  65 + `message` TEXT NOT NULL,
  66 + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  67 + PRIMARY KEY (`id`),
  68 + KEY `idx_device_time` (`device_id`, `created_at`)
  69 +) ENGINE=InnoDB COMMENT='设备日志表';
  70 +SQL;
  71 +
  72 + $pdo->exec($sql);
  73 + echo "Tables created successfully.\n";
  74 +
  75 + // Insert sample device if not exists
  76 + $pdo->exec("INSERT IGNORE INTO `devices` (`id`, `secret`, `name`, `status`) VALUES ('dev_test_001', '123456', 'My First Moltbot', 'offline')");
  77 + echo "Sample data inserted.\n";
  78 +
  79 +} catch (PDOException $e) {
  80 + echo "Database Error: " . $e->getMessage() . "\n";
  81 + exit(1);
  82 +}
  1 +# Relay Server 部署打包脚本
  2 +# 运行: .\package-deploy.ps1
  3 +
  4 +$DeployDir = "$PSScriptRoot\deploy-package"
  5 +$ZipFile = "$PSScriptRoot\relay-server-deploy.zip"
  6 +
  7 +Write-Host "=== 打包 Relay Server 部署文件 ===" -ForegroundColor Cyan
  8 +
  9 +# 创建部署目录
  10 +if (Test-Path $DeployDir) { Remove-Item $DeployDir -Recurse -Force }
  11 +New-Item -ItemType Directory -Path $DeployDir | Out-Null
  12 +
  13 +# 复制必要文件
  14 +$files = @(
  15 + ".env",
  16 + "composer.json",
  17 + "docker-compose.yml",
  18 + "Dockerfile",
  19 + "migrate.php",
  20 + "start.php"
  21 +)
  22 +
  23 +foreach ($file in $files) {
  24 + Copy-Item "$PSScriptRoot\$file" "$DeployDir\" -Force
  25 + Write-Host " + $file" -ForegroundColor Green
  26 +}
  27 +
  28 +# 创建部署说明
  29 +$readme = @"
  30 +# Relay Server 部署指南
  31 +
  32 +## 1. 上传整个目录到服务器
  33 + scp -r . user@server:/opt/moltbot-relay
  34 +
  35 +## 2. 修改 .env 配置
  36 + - DB_HOST: 数据库地址
  37 + - DB_PASSWORD: 数据库密码
  38 + - REDIS_HOST: Redis 地址(使用 docker 内部网络可设为 redis)
  39 +
  40 +## 3. 启动服务
  41 + cd /opt/moltbot-relay
  42 + docker-compose up -d --build
  43 +
  44 +## 4. 查看日志
  45 + docker-compose logs -f
  46 +
  47 +## 5. 开放防火墙
  48 + firewall-cmd --permanent --add-port=8888/tcp
  49 + firewall-cmd --reload
  50 +
  51 +## 6. 修改本地插件配置
  52 + 编辑 ~/.clawdbot/clawdbot.json:
  53 + {
  54 + "channels": {
  55 + "wechat": {
  56 + "enabled": true,
  57 + "relayUrl": "ws://服务器IP:8888"
  58 + }
  59 + }
  60 + }
  61 +"@
  62 +$readme | Set-Content "$DeployDir\README.md" -Encoding UTF8
  63 +
  64 +# 打包成 zip
  65 +if (Test-Path $ZipFile) { Remove-Item $ZipFile -Force }
  66 +Compress-Archive -Path "$DeployDir\*" -DestinationPath $ZipFile
  67 +
  68 +Write-Host ""
  69 +Write-Host "=== 打包完成 ===" -ForegroundColor Green
  70 +Write-Host "部署包: $ZipFile" -ForegroundColor Yellow
  71 +Write-Host ""
  72 +Write-Host "将 zip 文件上传到服务器后解压并运行 docker-compose up -d --build" -ForegroundColor White
  73 +
  74 +# 清理临时目录
  75 +Remove-Item $DeployDir -Recurse -Force
No preview for this file type
  1 +<?php
  2 +use Workerman\Worker;
  3 +use Workerman\Timer;
  4 +
  5 +require_once __DIR__ . '/vendor/autoload.php';
  6 +
  7 +// Define Heartbeat Interval
  8 +define('HEARTBEAT_TIME', 30);
  9 +
  10 +// Create a WebSocket worker
  11 +$ws_worker = new Worker("websocket://0.0.0.0:8888");
  12 +
  13 +// Emulate simple routing/state for now
  14 +// In production, use Redis for distributed state
  15 +$ws_worker->count = 1; // Single process for dev simplicity
  16 +
  17 +// Redis Connection
  18 +$redis = null;
  19 +try {
  20 + $redis_host = getenv('REDIS_HOST') ?: '127.0.0.1';
  21 + $redis_port = getenv('REDIS_PORT') ?: 6379;
  22 + $redis_auth = getenv('REDIS_PASSWORD');
  23 + $redis_prefix = getenv('REDIS_PREFIX') ?: 'ai_';
  24 +
  25 + $params = [
  26 + 'scheme' => 'tcp',
  27 + 'host' => $redis_host,
  28 + 'port' => $redis_port,
  29 + ];
  30 +
  31 + if ($redis_auth) {
  32 + $params['password'] = $redis_auth;
  33 + }
  34 +
  35 + // Predis Client
  36 + $redis = new Predis\Client($params, ['prefix' => $redis_prefix]);
  37 + $redis->connect();
  38 + echo "✅ Connected to Redis at $redis_host ($redis_prefix)\n";
  39 +} catch (Exception $e) {
  40 + echo "⚠️ Redis Connection Failed: " . $e->getMessage() . "\n";
  41 +}
  42 +
  43 +// Store connections (Memory for now, can move to Redis later)
  44 +$clients = []; // ClientID -> Connection
  45 +$devices = []; // DeviceID -> Connection
  46 +
  47 +$ws_worker->onWorkerStart = function ($worker) {
  48 + echo "Relay Server Started on 0.0.0.0:8888\n";
  49 +
  50 + // Heartbeat check
  51 + Timer::add(10, function () use ($worker) {
  52 + $time_now = time();
  53 + foreach ($worker->connections as $connection) {
  54 + // Check if connection is alive possibly?
  55 + // Workerman handles basic disconnects, but we can enforce ping logic here if needed
  56 + }
  57 + });
  58 +};
  59 +
  60 +$ws_worker->onConnect = function ($connection) {
  61 + echo "New connection: " . $connection->id . "\n";
  62 + $connection->authVerified = false;
  63 +};
  64 +
  65 +$ws_worker->onMessage = function ($connection, $data) use (&$clients, &$devices) {
  66 + $msg = json_decode($data, true);
  67 + if (!$msg || !isset($msg['type'])) {
  68 + return;
  69 + }
  70 +
  71 + // 1. Authenticate / Register
  72 + if ($msg['type'] === 'register') {
  73 + if ($msg['role'] === 'device') {
  74 + // Device 注册
  75 + $deviceId = $msg['id']; // TODO: Add secret validation
  76 + $devices[$deviceId] = $connection;
  77 + $connection->deviceId = $deviceId;
  78 + $connection->role = 'device';
  79 + $connection->authVerified = true;
  80 + $connection->send(json_encode(['type' => 'ack', 'status' => 'registered']));
  81 + echo "Device Registered: $deviceId\n";
  82 + } elseif ($msg['role'] === 'client') {
  83 + // Mini Program 注册
  84 + // TODO: Validate Token
  85 + $clientId = $msg['id'];
  86 + $clients[$clientId] = $connection;
  87 + $connection->clientId = $clientId;
  88 + $connection->role = 'client';
  89 + $connection->authVerified = true;
  90 + $connection->send(json_encode(['type' => 'ack', 'status' => 'connected']));
  91 + echo "Client Connected: $clientId\n";
  92 + }
  93 + return;
  94 + }
  95 +
  96 + if (!$connection->authVerified) {
  97 + $connection->close();
  98 + return;
  99 + }
  100 +
  101 + // 2. Proxy Logic
  102 +
  103 + // Client -> Device
  104 + if ($msg['type'] === 'proxy' && $connection->role === 'client') {
  105 + $targetDeviceId = $msg['targetDeviceId'] ?? null;
  106 + if ($targetDeviceId && isset($devices[$targetDeviceId])) {
  107 + $payload = $msg['payload'];
  108 + // Wrap it so device knows who sent it
  109 + $forwardMsg = [
  110 + 'type' => 'cmd:execute',
  111 + 'fromClientId' => $connection->clientId,
  112 + 'payload' => $payload
  113 + ];
  114 + $devices[$targetDeviceId]->send(json_encode($forwardMsg));
  115 + echo "Forwarded msg from Client {$connection->clientId} to Device {$targetDeviceId}\n";
  116 + } else {
  117 + $connection->send(json_encode(['type' => 'error', 'msg' => 'Device offline or not found']));
  118 + }
  119 + }
  120 +
  121 + // Device -> Client
  122 + if ($msg['type'] === 'proxy_response' && $connection->role === 'device') {
  123 + $targetClientId = $msg['targetClientId'] ?? null;
  124 + if ($targetClientId && isset($clients[$targetClientId])) {
  125 + $payload = $msg['payload'];
  126 + $forwardMsg = [
  127 + 'type' => 'response',
  128 + 'fromDeviceId' => $connection->deviceId,
  129 + 'payload' => $payload
  130 + ];
  131 + $clients[$targetClientId]->send(json_encode($forwardMsg));
  132 + echo "Forwarded response from Device {$connection->deviceId} to Client {$targetClientId}\n";
  133 + }
  134 + }
  135 +};
  136 +
  137 +$ws_worker->onClose = function ($connection) use (&$clients, &$devices) {
  138 + if (isset($connection->role)) {
  139 + if ($connection->role === 'device' && isset($connection->deviceId)) {
  140 + unset($devices[$connection->deviceId]);
  141 + echo "Device disconnected: {$connection->deviceId}\n";
  142 + } elseif ($connection->role === 'client' && isset($connection->clientId)) {
  143 + unset($clients[$connection->clientId]);
  144 + echo "Client disconnected: {$connection->clientId}\n";
  145 + }
  146 + }
  147 +};
  148 +
  149 +Worker::runAll();
Please register or login to post a comment