start.php 11.6 KB
<?php
use Workerman\Worker;
use Workerman\Timer;
use Tos\TosClient;
use Tos\Exception\TosClientException;
use Tos\Exception\TosServerException;
use Tos\Model\PutObjectInput;

require_once __DIR__ . '/vendor/autoload.php';

// Define Heartbeat Interval
define('HEARTBEAT_TIME', 30);

// Create a WebSocket worker
$ws_worker = new Worker("websocket://0.0.0.0:8888");

// Emulate simple routing/state for now
// In production, use Redis for distributed state
$ws_worker->count = 1; // Single process for dev simplicity

// Redis Connection
$redis = null;
try {
    $redis_host = getenv('REDIS_HOST') ?: '127.0.0.1';
    $redis_port = getenv('REDIS_PORT') ?: 6379;
    $redis_auth = getenv('REDIS_PASSWORD');
    $redis_prefix = getenv('REDIS_PREFIX') ?: 'ai_';

    $params = [
        'scheme' => 'tcp',
        'host' => $redis_host,
        'port' => $redis_port,
    ];

    if ($redis_auth) {
        $params['password'] = $redis_auth;
    }

    // Predis Client
    $redis = new Predis\Client($params, ['prefix' => $redis_prefix]);
    $redis->connect();
    echo "✅ Connected to Redis at $redis_host ($redis_prefix)\n";
} catch (Exception $e) {
    echo "⚠️ Redis Connection Failed: " . $e->getMessage() . "\n";
}

// Store connections (Memory for now, can move to Redis later)
$clients = []; // ClientID -> Connection
$devices = []; // DeviceID -> Connection

$ws_worker->onWorkerStart = function ($worker) {
    echo "Relay Server Started on 0.0.0.0:8888\n";

    // Heartbeat check
    Timer::add(10, function () use ($worker) {
        $time_now = time();
        foreach ($worker->connections as $connection) {
            // Check if connection is alive possibly? 
            // Workerman handles basic disconnects, but we can enforce ping logic here if needed
        }
    });
};

$ws_worker->onConnect = function ($connection) {
    echo "New connection: " . $connection->id . "\n";
    $connection->authVerified = false;
};

$ws_worker->onMessage = function ($connection, $data) use (&$clients, &$devices) {
    $msg = json_decode($data, true);
    if (!$msg || !isset($msg['type'])) {
        return;
    }

    // 1. Authenticate / Register
    if ($msg['type'] === 'register') {
        if ($msg['role'] === 'device') {
            // Device 注册
            $deviceId = $msg['id']; // TODO: Add secret validation
            $devices[$deviceId] = $connection;
            $connection->deviceId = $deviceId;
            $connection->role = 'device';
            $connection->authVerified = true;
            $connection->send(json_encode(['type' => 'ack', 'status' => 'registered']));
            echo "Device Registered: $deviceId\n";
        } elseif ($msg['role'] === 'client') {
            // Mini Program 注册
            // TODO: Validate Token
            $clientId = $msg['id'];
            $clients[$clientId] = $connection;
            $connection->clientId = $clientId;
            $connection->role = 'client';
            $connection->authVerified = true;
            $connection->send(json_encode(['type' => 'ack', 'status' => 'connected']));
            echo "Client Connected: $clientId\n";
        }
        return;
    }

    if (!$connection->authVerified) {
        $connection->close();
        return;
    }

    // 2. Proxy Logic

    // Client -> Device
    if ($msg['type'] === 'proxy' && $connection->role === 'client') {
        $targetDeviceId = $msg['targetDeviceId'] ?? null;
        if ($targetDeviceId && isset($devices[$targetDeviceId])) {
            $payload = $msg['payload'];
            // Wrap it so device knows who sent it
            $forwardMsg = [
                'type' => 'cmd:execute',
                'fromClientId' => $connection->clientId,
                'payload' => $payload
            ];
            $devices[$targetDeviceId]->send(json_encode($forwardMsg));
            echo "Forwarded msg from Client {$connection->clientId} to Device {$targetDeviceId}\n";
        } else {
            $connection->send(json_encode(['type' => 'error', 'msg' => 'Device offline or not found']));
        }
    }

    // Device -> Client
    if ($msg['type'] === 'proxy_response' && $connection->role === 'device') {
        $targetClientId = $msg['targetClientId'] ?? null;
        if ($targetClientId && isset($clients[$targetClientId])) {
            $payload = $msg['payload'];
            $forwardMsg = [
                'type' => 'response',
                'fromDeviceId' => $connection->deviceId,
                'payload' => $payload
            ];
            $clients[$targetClientId]->send(json_encode($forwardMsg));
            echo "Forwarded response from Device {$connection->deviceId} to Client {$targetClientId}\n";
        }
    }
};

$ws_worker->onClose = function ($connection) use (&$clients, &$devices) {
    if (isset($connection->role)) {
        if ($connection->role === 'device' && isset($connection->deviceId)) {
            unset($devices[$connection->deviceId]);
            echo "Device disconnected: {$connection->deviceId}\n";
        } elseif ($connection->role === 'client' && isset($connection->clientId)) {
            unset($clients[$connection->clientId]);
            echo "Client disconnected: {$connection->clientId}\n";
        }
    }
};

// ---------------------------------------------------------
// [New] HTTP Server for file uploads and static serving
// ---------------------------------------------------------
$http_worker = new Worker("http://0.0.0.0:8889");
$http_worker->count = 1; // Single process for uploads
$http_worker->onMessage = function ($connection, $request) {
    // 1. Static File Serving (Simple implementation)
    $path = $request->path();
    if (strpos($path, '/uploads/') === 0) {
        $file = __DIR__ . $path;
        if (is_file($file)) {
            $connection->send(new \Workerman\Protocols\Http\Response(
                200,
                ['Content-Type' => mime_content_type($file)],
                file_get_contents($file)
            ));
            return;
        }
    }

    // 2. Upload Handler
    if ($path === '/upload') {
        $files = $request->file();

        if (empty($files['file'])) {
            $connection->send(new \Workerman\Protocols\Http\Response(400, [], json_encode(['ok' => false, 'error' => 'No file'])));
            return;
        }

        $file = $files['file'];

        // Validate Size (50MB)
        if ($file['size'] > 50 * 1024 * 1024) {
            $connection->send(new \Workerman\Protocols\Http\Response(400, [], json_encode(['ok' => false, 'error' => 'File too large (Max 50MB)'])));
            return;
        }

        // Validate Extension
        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        // Supported: PDF, Excel, Image, Video
        $allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'mp4', 'pdf', 'xls', 'xlsx'];
        if (!in_array($ext, $allowed)) {
            $connection->send(new \Workerman\Protocols\Http\Response(400, [], json_encode(['ok' => false, 'error' => 'File type not allowed'])));
            return;
        }

        // TOS Configuration (Keys from hsobs.php)
        $ak = 'AKLTZjkyMzliYjQ5N2IyNDFjNDliMTBiY2E2ZmU5ODhjNTM';
        $sk = 'WldKbE5XUmpPRGxqWmpZM05EUTBObUpqTTJSa01qVTNNMkprWmpsbU9Uaw==';
        $endpoint = 'tos-cn-shanghai.volces.com';
        $region = 'cn-shanghai';
        $bucket = 'ocxun';

        try {
            $client = new TosClient([
                'region' => $region,
                'endpoint' => $endpoint,
                'ak' => $ak,
                'sk' => $sk,
            ]);

            // Generate Key
            $uuid = bin2hex(random_bytes(8));
            // TODO: 暂时使用 'guest',等待后续对接用户手机号功能
            $userPhone = 'guest';
            $objectKey = "clawdbot/{$userPhone}/{$uuid}.{$ext}";

            // Read file content
            $contentFn = fopen($file['tmp_name'], 'r');

            // Upload using Object Input
            $input = new PutObjectInput($bucket, $objectKey, $contentFn);
            $input->setACL('public-read');

            $client->putObject($input);

            if (is_resource($contentFn)) {
                fclose($contentFn);
            }

            // Generate URL
            $url = "https://{$bucket}.{$endpoint}/{$objectKey}";

            $connection->send(json_encode([
                'ok' => true,
                'url' => $url
            ]));

        } catch (Exception $e) {
            $connection->send(new \Workerman\Protocols\Http\Response(500, [], json_encode(['ok' => false, 'error' => 'Upload failed: ' . $e->getMessage()])));
        }
        return;
    }

    // 3. Generate Pre-Signed URL for Direct Upload
    if (strpos($path, '/tos/sign') === 0) {
        $query = $request->get();
        $filename = $query['filename'] ?? 'file_' . time();
        $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));

        // Validation - Keep it simple for demo
        $allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'mp4', 'pdf', 'xls', 'xlsx'];
        if (!in_array($ext, $allowed) && $ext !== '') {
            // If no extension providing, we might just allow it or fail. 
            // Ideally client should provide full filename with extension.
        }

        // TOS Configuration (Keys from hsobs.php)
        $ak = 'AKLTZjkyMzliYjQ5N2IyNDFjNDliMTBiY2E2ZmU5ODhjNTM';
        $sk = 'WldKbE5XUmpPRGxqWmpZM05EUTBObUpqTTJSa01qVTNNMkprWmpsbU9Uaw==';
        $endpoint = 'tos-cn-shanghai.volces.com';
        $region = 'cn-shanghai';
        $bucket = 'ocxun';

        try {
            $client = new TosClient([
                'region' => $region,
                'endpoint' => $endpoint,
                'ak' => $ak,
                'sk' => $sk,
            ]);

            $uuid = bin2hex(random_bytes(8));
            // TODO: User ID from token
            $userPhone = 'guest';
            // Ensure filename is safe
            $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '', $filename);
            if (!$safeName)
                $safeName = 'unnamed';

            $objectKey = "clawdbot/{$userPhone}/direct_{$uuid}_{$safeName}";

            // Generate Pre-Signed PUT URL (Valid for 15 mins)
            // Note: The SDK method name might vary slightly based on version, 
            // but `preSignedURL` is standard for TOS PHP SDK v2.
            $input = new \Tos\Model\PreSignedURLInput(
                'PUT',
                $bucket,
                $objectKey,
                300 // 5 minutes validity
            );

            // Add content-type if known? client will send it. 
            // For simple PUT, we just sign the method and resource.

            $output = $client->preSignedURL($input);
            $signedUrl = $output->getSignedUrl();

            // Public Access URL (Assuming bucket is public-read or we use signed Get URL)
            // For this project, we used public-read ACL in previous code, so we assume public access.
            $publicUrl = "https://{$bucket}.{$endpoint}/{$objectKey}";

            $connection->send(json_encode([
                'ok' => true,
                'uploadUrl' => $signedUrl,
                'publicUrl' => $publicUrl,
                'key' => $objectKey
            ]));

        } catch (Exception $e) {
            $connection->send(new \Workerman\Protocols\Http\Response(500, [], json_encode(['ok' => false, 'error' => $e->getMessage()])));
        }
        return;
    }

    $connection->send("Moltbot Relay HTTP Server");
};

Worker::runAll();