moshaorui 1 giorno fa
parent
commit
d6b1aab6f6

+ 6 - 5
app/Distributor/Controllers/SmmPostController.php

@@ -38,13 +38,13 @@ class SmmPostController extends AdminDistController
     protected function form()
     {
         return Form::make(new SmmPost(), function (Form $form) {
-            $form->title('本地素材发布');
+            $form->title(admin_trans_label('local_materials'));
             $form->action('ssm-post');
             $form->disableListButton();
             $form->multipleSteps()
                 ->remember()
                 ->width('950px')
-                ->add('选择本地媒体', function ($step) {
+                ->add(admin_trans_label('select_local_media'), function ($step) {
                     $step->radio('send_type',admin_trans_label('send_type'))
                         ->when([1], function ($step) {
                             $step->datetime('send_time', '<span style="color:#bd4147;">*</span> '.admin_trans_label('send_time'))->placeholder(' ');
@@ -78,7 +78,7 @@ class SmmPostController extends AdminDistController
                         ->options([ 0=>admin_trans_label('images'), 1=>admin_trans_label('videos')])->default(0);
                     $this->stepLeaving($step,0);
                 })
-                ->add('选择传单社媒平台', function ($step) {
+                ->add(admin_trans_label('choose_platforms'), function ($step) {
                     $rootAccounts = SmmUserAccount::getUserAccounts();
                     $listBoxOptions = [];
                     foreach ($rootAccounts as $account) {
@@ -142,8 +142,9 @@ class SmmPostController extends AdminDistController
             $timer->createLog();
             //最后一步
             $data = [
-                'title'       => '操作成功',
-                'description' => '系统已生成发送队列,详情请查看日志。 ',
+                'title'       => admin_trans_label('operation_successful'),
+                'description' => admin_trans_label('send_post_description'),
+                'continue_publishing' => admin_trans_label('continue_publishing'),
             ];
             return view('distributor.form_custom.completion-page', $data);
         }

+ 1 - 2
app/Distributor/Controllers/SmmUserAccountController.php

@@ -29,8 +29,7 @@ class SmmUserAccountController extends AdminDistController
 //
 //        exit;
         return $content
-            ->header('帐号管理')
-            ->description('全部')
+            ->header(admin_trans_label('account_management'))
             ->body($this->grid());
     }
 

+ 306 - 141
app/Services/Smm/TwitterService.php

@@ -12,83 +12,102 @@ use Illuminate\Support\Facades\Log;
 class TwitterService implements SmmPlatformInterface
 {
     protected $configData;
-    protected $twitterOAuth;
 
-    protected $consumerKey;
-    protected $consumerSecret;
+    private $clientId;
+    private $clientSecret;
 
-    protected $oauthCallbackUrl;
+    private $access_token;
+    private $media_upload_url = 'https://api.x.com/2/media/upload';
+    private $tweet_url = 'https://api.x.com/2/tweets';
+    private $chunk_size = 4 * 1024 * 1024; // 分块上传的大小,不能大于5M,否则会报错
 
     public function __construct($configData)
     {
+        $this->clientId = env('SSM_X_CLIENT_ID');
+        $this->clientSecret = env('SSM_X_CLIENT_SECRET');
         $this->configData = $configData;
-
-        $this->consumerKey = env('SSM_X_CONSUMER_KEY');
-        $this->consumerSecret = env('SSM_X_CONSUMER_SECRET');
-        $this->oauthCallbackUrl = env('DIST_SITE_URL') . '/dist/callback/twitter';
-
-        if (isset($configData['accountInfo'])) {
-            //初始化1.1版本的配置信息
-            $access_token = $configData['accountInfo']['access_token'];
-            $backup_field1 = json_decode($configData['accountInfo']['backup_field1'],true);
-            $access_token_secret = $backup_field1['accessTokenSecret'];
-            if (!empty($access_token) && !empty($access_token_secret)) {
-                $this->twitterOAuth = new TwitterOAuth(
-                    $this->consumerKey,
-                    $this->consumerSecret,
-                    $access_token,
-                    $access_token_secret
-                );
-
-            }
-        }
     }
 
     public function login()
     {
-        $connection = new TwitterOAuth($this->consumerKey, $this->consumerSecret);
-        $requestToken = $connection->oauth('oauth/request_token', ['oauth_callback' => $this->oauthCallbackUrl]);
-
-        session(['twitter_oauth_token' => $requestToken['oauth_token']]);
-        session(['twitter_oauth_token_secret' => $requestToken['oauth_token_secret']]);
-
-        $url = $connection->url('oauth/authorize', ['oauth_token' => $requestToken['oauth_token']]);
-
+        $redirectUri = env('DIST_SITE_URL') . '/dist/callback/twitter';
+        $scopes = ['tweet.read','tweet.write','dm.write', 'users.read', 'offline.access','media.write']; // 需要的权限范围
+        $oauthState = bin2hex(random_bytes(16));
+        $authUrl = 'https://twitter.com/i/oauth2/authorize?' . http_build_query([
+                'response_type' => 'code',
+                'client_id' => $this->clientId,
+                'redirect_uri' => $redirectUri,
+                'scope' => implode(' ', $scopes),
+                'state' => $oauthState,
+                'code_challenge' => 'challenge', // 如果使用 PKCE 需要生成
+                'code_challenge_method' => 'plain'
+            ]);
         return [
             'status' => true,
-            'data' => ['url' => $url]
+            'data' => ['url' => $authUrl]
         ];
     }
 
     public function loginCallback(Request $request)
     {
         try {
-            $oauthToken = $request->get('oauth_token');
-            $oauthVerifier = $request->get('oauth_verifier');
-
-            $requestToken = session('twitter_oauth_token');
-            $requestTokenSecret = session('twitter_oauth_token_secret');
-
-            $connection = new TwitterOAuth(
-                $this->consumerKey,
-                $this->consumerSecret,
-                $requestToken,
-                $requestTokenSecret
-            );
-            $accessToken = $connection->oauth('oauth/access_token', [
-                'oauth_verifier' => $oauthVerifier
-            ]);
-            $expiresAt = Carbon::now()->addDays(90)->format('Y-m-d H:i:s');
-            return [
-                'status' => true,
-                'data' => [
-                    'accessToken' => $accessToken['oauth_token'],
-                    'backupField1' => json_encode(['accessTokenSecret'=>$accessToken['oauth_token_secret']]),
-                    'userId' => $accessToken['user_id'],
-                    'userName' => $accessToken['screen_name'],
-                    'accessToken_expiresAt' => $expiresAt,
-                ]
+            $tokenUrl = 'https://api.twitter.com/2/oauth2/token';
+            $redirectUri = env('DIST_SITE_URL') . '/dist/callback/twitter';
+            $params = [
+                'code' => $_GET['code'],
+                'grant_type' => 'authorization_code',
+                'client_id' => $this->clientId,
+                'redirect_uri' => $redirectUri,
+                'code_verifier' => 'challenge' // 如果使用 PKCE 需要对应
             ];
+            $ch = curl_init();
+            curl_setopt($ch, CURLOPT_URL, $tokenUrl);
+            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+            curl_setopt($ch, CURLOPT_POST, true);
+            curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
+            curl_setopt($ch, CURLOPT_HTTPHEADER, [
+                'Authorization: Basic ' . base64_encode($this->clientId . ':' . $this->clientSecret),
+                'Content-Type: application/x-www-form-urlencoded'
+            ]);
+            $auth = curl_exec($ch);
+            curl_close($ch);
+            $auth = json_decode($auth, true);
+            if (isset($auth['access_token']) == false) {
+                return ['status' => false, 'data' => '获取token失败'];
+            }
+            $this->access_token = $auth['access_token'];
+            $url = 'https://api.twitter.com/2/users/me?user.fields=id,username,name';
+            $result = $this->twitterApiRequest($url, 'GET');
+           // var_dump($result);
+           // exit;
+            if ($result['code'] != 200) {
+                return ['status' => false, 'data' => '获取token失败'];
+            }
+
+
+            if (isset($auth['access_token'])) {
+                $userData = $result['response']['data'];
+                $userId = $userData['id'];
+                $username = $userData['username'];
+                $name = $userData['name'];
+
+                $refresh_token = $auth['refresh_token'] ?? '';
+                $expires_in = $auth['expires_in'] ?? 0;
+                $expiresAt = Carbon::now()->addSeconds($expires_in)->format('Y-m-d H:i:s');
+
+                return [
+                    'status' => true,
+                    'data' => [
+                        'accessToken' => $auth['access_token'],
+                        'backupField1' => json_encode(['refresh_token'=>$refresh_token]),
+                        'userId' => $userId,
+                        'userName' => $username,
+                        'accessToken_expiresAt' => $expiresAt,
+                    ]
+                ];
+            } else {
+                return ['status' => false, 'data' => '获取token失败'];
+            }
         } catch (\Exception $e) {
             return ['status' => false, 'data' => $e->getMessage()];
         }
@@ -97,40 +116,22 @@ class TwitterService implements SmmPlatformInterface
 
     public function postImage($message, $imagePaths, $accessToken = null)
     {
-        Log::info('开始发布图片帖子');
         try {
-            $this->twitterOAuth->setApiVersion('1.1'); // 强制使用 v1.1 API
-            $this->twitterOAuth->setTimeouts(15, 60);
-            $mediaIds = [];
+            $this->access_token = $accessToken;
+            $videoPaths = [];
             foreach ($imagePaths as $imagePath) {
-                $imagePath = toStoragePath($imagePath);
-                if (!file_exists($imagePath)) {
-                    throw new \Exception("图片不存在: {$imagePath}");
-                }
-                //1.1版本上传媒体文件
-                $uploadedMedia = $this->twitterOAuth->upload('media/upload', [
-                    'media' => $imagePath
-                ]);
-                Log::info('1.1版本上传媒体文件完成');
-                if (isset($uploadedMedia->error)) {
-                    throw new \Exception('媒体上传失败: ' . json_encode($uploadedMedia));
-                }
-                $mediaIds[] = $uploadedMedia->media_id_string;
+                $videoPaths[] = toStoragePath($imagePath);//用本地路径上传
             }
-            // 2.0版本发布推文
-            $this->twitterOAuth->setApiVersion('2');
-            $tweet = $this->twitterOAuth->post('tweets', [
-                'text' => $message,
-                'media' => !empty($mediaIds) ? ['media_ids' => $mediaIds] : null
-            ]);
-            Log::info('2.0版本发布推文完成');
-            if (isset($tweet->errors)) {
-                throw new \Exception('推文发布失败: ' . json_encode($tweet->errors));
+            $mediaResult = $this->uploadAndPost($videoPaths, $message);
+            if (isset($mediaResult['response']['data']['id'])) {
+                return ['status' => true,
+                    'data' => [
+                        'responseIds' => [$mediaResult['response']['data']['id']],
+                    ]
+                ];
+            } else {
+                return ['status' => false, 'data' => '发布失败'];
             }
-            return ['status' => true,
-                'data' => [
-                    'responseIds' => [$tweet->data->id],
-                ]];
         } catch (\Exception $e) {
             return ['status' => false, 'data' => $e->getMessage()];
         }
@@ -140,80 +141,244 @@ class TwitterService implements SmmPlatformInterface
     public function postVideo($message, $videoPath, $accessToken = null)
     {
         try {
-            $this->twitterOAuth->setTimeouts(15, 120);
-            $this->twitterOAuth->setApiVersion('1.1');
-            $videoPath = toStoragePath($videoPath);
-            // Verify file exists and is readable
-            if (!file_exists($videoPath) || !is_readable($videoPath)) {
-                throw new Exception("Video file does not exist or is not readable: $videoPath");
+            $this->access_token = $accessToken;
+            $videoPath = toStoragePath($videoPath);//用本地路径上传
+            $mediaResult = $this->uploadAndPost([$videoPath], $message);
+            if (isset($mediaResult['response']['data']['id'])) {
+                return ['status' => true,
+                    'data' => [
+                        'responseIds' => [$mediaResult['response']['data']['id']],
+                    ]
+                ];
+            } else {
+                return ['status' => false, 'data' => '上传视频失败'];
+            }
+        } catch (\Exception $e) {
+            return ['status' => false, 'data' => $e->getMessage()];
+        }
+    }
+
+    // 保持空实现
+    public function getComments($postId) {}
+
+    public function replyToComment($commentId) {}
+
+    public function deleteComment($commentId) {}
+
+    /**
+     * 通用Twitter API请求
+     */
+    private function twitterApiRequest($url, $method = 'POST', $data = [], $headers = []) {
+        try {
+            Log::info('curl 请求:'.$url);
+            $ch = curl_init();
+            $headers = array_merge([
+                'Authorization: Bearer ' . $this->access_token
+            ], $headers);
+
+            Log::info('curl $headers:'.json_encode($headers));
+
+            curl_setopt_array($ch, [
+                CURLOPT_URL => $url,
+                CURLOPT_RETURNTRANSFER => true,
+                CURLOPT_FOLLOWLOCATION => true,
+                CURLOPT_HTTPHEADER => $headers,
+                CURLOPT_TIMEOUT => 50,
+            ]);
+
+            if ($method === 'POST') {
+                curl_setopt($ch, CURLOPT_POST, true);
+
+                // 处理JSON数据
+                curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
             }
 
-            Log::info('twitter上传视频'.$videoPath);
+            $response = curl_exec($ch);
+            $error = curl_error($ch);
+            curl_close($ch);
+
+            Log::info('curl response:'.$response);
 
-            echo '1';
-            // Upload video with chunked upload
-            $uploadedMedia = $this->twitterOAuth->upload('media/upload', [
-                'media' => $videoPath,
-               // 'media_category' => 'tweet_video'
-                'media_type' => 'video/mp4'
-            ],['chunkedUpload'=>true]);
+            if ($error) {
+                throw new \Exception("cURL Error: ".$error);
+            }
 
-           // dd($uploadedMedia);
-           // dd($uploadedMedia);
+            if ($response === false) {
+                throw new \Exception("cURL Error: false response");
+            }
 
-            Log::info('1.1版本,分块上传视频');
+            $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
 
-            $code = $this->twitterOAuth->getLastHttpCode();
+            return ['code'=>$http_code,'response' => json_decode($response, true)];
+        } catch (\Exception $e) {
+            throw new \Exception("catch cURL Error: ".$e->getMessage());
+        }
+    }
 
+    /**
+     * 初始化上传
+     */
+    public function initUpload($fileSize, $mediaType = 'video/mp4',$media_category = 'tweet_video') {
+        return $this->twitterApiRequest($this->media_upload_url, 'POST', [
+            'command' => 'INIT',
+            'media_type' => $mediaType,
+            'total_bytes' => $fileSize,
+            'media_category' => $media_category
+        ]);
+    }
 
-            if (isset($uploadedMedia->error) || $code != 200) {
-                throw new \Exception('媒体上传失败: ' . json_encode($uploadedMedia));
+    /**
+     * 分块上传
+     */
+    public function appendUpload($media_id, $filePath) {
+        $handle = fopen($filePath, 'rb');
+        $segment_index = 0;
+
+        while (!feof($handle)) {
+            $chunk = fread($handle, $this->chunk_size);
+            $result = $this->twitterApiRequest($this->media_upload_url, 'POST', [
+                'command' => 'APPEND',
+                'media_id' => $media_id,
+                'segment_index' => $segment_index,
+                'media' => $chunk
+            ]);
+
+            Log::info('分块上传'.$segment_index.' : '.json_encode($result));
+
+            if ($result['code'] !== 204) {
+                return false;
             }
+            $segment_index++;
+            sleep(2); // 避免速率限制
+        }
+
+        fclose($handle);
+        return true;
+    }
+
+    /**
+     * 完成上传
+     */
+    public function finalizeUpload($media_id) {
+        return $this->twitterApiRequest($this->media_upload_url, 'POST', [
+            'command' => 'FINALIZE',
+            'media_id' => $media_id
+        ]);
+    }
+
+    /**
+     * 检查媒体状态
+     */
+    public function checkStatus($media_id) {
+        $url = $this->media_upload_url . '?' . http_build_query([
+                'command' => 'STATUS',
+                'media_id' => $media_id
+            ]);
+
+        return $this->twitterApiRequest($url, 'GET');
+    }
 
+    /**
+     * 等待媒体处理完成
+     */
+    public function waitForProcessing($media_id, $interval = 10, $max_attempts = 6) {
+        $attempts = 0;
+        do {
+            $status = $this->checkStatus($media_id);
+            Log::info('等待媒体处理完成:'.$attempts.' : '.json_encode($status));
+            if (!isset($status['response']['data'])) {
+                throw new \Exception("waitForProcessing failed: status data not found");
+            }
+            $state = $status['response']['data']['processing_info']['state'] ?? '';
 
-            $mediaId = $uploadedMedia->media_id_string;
+            if ($state === 'succeeded') return true;
+            if ($state === 'failed') return false;
 
+            sleep($interval);
+            $attempts++;
+        } while ($attempts < $max_attempts);
 
-            $limit = 0;
-            do {
-                $status = $this->twitterOAuth->mediaStatus($mediaId);
-                Log::info('检测视频上传状态'.print_r($status,true));
-                sleep(5); // 等待 5 秒
-                $limit++;
-            } while ($status->processing_info->state !== 'succeeded' && $limit <= 3); // 最多重试 3 次
+        throw new \Exception("Media processing timeout");
+    }
 
-            if ($status->processing_info->state === 'succeeded') {
-                //print_r($status->processing_info->state);
-                // 2.0版本发布推文
-                $this->twitterOAuth->setApiVersion('2');
-                $tweet = $this->twitterOAuth->post('tweets', [
-                    'text' => $message,
-                    'media' => ['media_ids' => [$mediaId]]
-                ]);
+    /**
+     * 发布推文
+     */
+    public function postTweet($text, $media_ids) {
+        $data = [
+            'text' => $text,
+            'media' => ['media_ids' => $media_ids]
+        ];
+        $data = json_encode($data);
+        return $this->twitterApiRequest($this->tweet_url, 'POST',$data , ['Content-Type: application/json']);
+    }
 
-                Log::info('2.0版本发布推文');
+    /**
+     * 完整上传流程
+     */
+    public function uploadAndPost($filePaths, $tweetText) {
+        try {
+            $media_ids = [];
+            foreach ($filePaths as $filePath) {
+                $file_info = pathinfo($filePath);
+                $file_extension = $file_info['extension'];
+
+                if ($file_extension == 'mp4' && filesize($filePath) > 20 * 1024 * 1024) {
+                    //不能大于20M
+                    return ['status'=>false,'message' => 'File size too large (max 20M)'];
+                } else if (($file_extension == 'jpg' || $file_extension == 'png') && filesize($filePath) > 5 * 1024 * 1024) {
+                    //不能大于5M
+                    return ['status'=>false,'message' => 'File size too large (max 5M)'];
+                }
+                if ($file_extension == 'mp4') {
+                    $media_category = 'tweet_video';
+                    $mediaType = 'video/mp4';
+                } else if ($file_extension == 'jpg') {
+                    $media_category = 'tweet_image';
+                    $mediaType = 'image/jpeg';
+                } else {
+                    $media_category = 'tweet_image';
+                    $mediaType = 'image/png';
+                }
+                Log::info('Twitter开始发布帖子,文件格式:'.$file_extension);
+                // 1. 初始化上传
+                $init = $this->initUpload(filesize($filePath),$mediaType,$media_category);
+                $media_id = isset($init['response']['data']['id']) ? $init['response']['data']['id'] : null;
+                $media_ids[] = $media_id;
+                if (!$media_id) {
+                    return ['status'=>false,'message' => 'Init upload failed'];
+                }
+                Log::info('初始化上传成功'.$media_id);
 
-                if (isset($tweet->errors)) {
-                    throw new \Exception('推文发布失败: ' . json_encode($tweet->errors));
+                // 2. 分块上传
+                $append = $this->appendUpload($media_id, $filePath);
+                if (!$append) {
+                    return ['status'=>false,'message' => 'Append upload failed'];
                 }
 
-                return ['status' => true,
-                    'data' => [
-                        'responseIds' => [$tweet->data->id],
-                    ]
-                ];
+                // 3. 完成上传
+                $finalize = $this->finalizeUpload($media_id);
+                Log::info('完成上传:'.json_encode($finalize));
+                if (isset($finalize['response']['data']) == false) {
+                    return ['status'=>false,'message' => 'Finalize upload failed'];
+                }
 
-            } else {
-                return ['status' => false, 'data' => '视频上传失败或处理超时。'  ];
+                // 4. mp4 等待处理完成
+                if ($file_extension == 'mp4') {
+                    Log::info('等待处理完成');
+                    if (!$this->waitForProcessing($media_id)) {
+                        throw new \Exception('Media processing failed');
+                    }
+                }
             }
+
+            // 5. 发布推文
+            Log::info('发布推文');
+            $postTweet = $this->postTweet($tweetText, $media_ids);
+            Log::info('发布推文结果:'.json_encode($postTweet));
+            return $postTweet;
         } catch (\Exception $e) {
-            return ['status' => false, 'data' => '视频上传异常: '.$e->getMessage()];
+            return ['status'=>false,'message' => "Upload failed: ".$e->getMessage()];
         }
     }
-
-
-    // 保持空实现
-    public function getComments($postId) {}
-    public function replyToComment($commentId) {}
-    public function deleteComment($commentId) {}
 }

+ 216 - 0
app/Services/Smm/TwitterServiceBak.php

@@ -0,0 +1,216 @@
+<?php
+
+namespace App\Services\Smm;
+
+use Abraham\TwitterOAuth\TwitterOAuth;
+use App\Services\Contracts\SmmPlatformInterface;
+use Carbon\Carbon;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Log;
+
+class TwitterServiceBak implements SmmPlatformInterface
+{
+    protected $configData;
+    protected $twitterOAuth;
+
+    protected $consumerKey;
+    protected $consumerSecret;
+
+    protected $oauthCallbackUrl;
+
+    public function __construct($configData)
+    {
+        $this->configData = $configData;
+
+        $this->consumerKey = env('SSM_X_CONSUMER_KEY');
+        $this->consumerSecret = env('SSM_X_CONSUMER_SECRET');
+        $this->oauthCallbackUrl = env('DIST_SITE_URL') . '/dist/callback/twitter';
+
+        if (isset($configData['accountInfo'])) {
+            //初始化1.1版本的配置信息
+            $access_token = $configData['accountInfo']['access_token'];
+            $backup_field1 = json_decode($configData['accountInfo']['backup_field1'],true);
+            $access_token_secret = $backup_field1['accessTokenSecret'];
+            if (!empty($access_token) && !empty($access_token_secret)) {
+                $this->twitterOAuth = new TwitterOAuth(
+                    $this->consumerKey,
+                    $this->consumerSecret,
+                    $access_token,
+                    $access_token_secret
+                );
+
+            }
+        }
+    }
+
+    public function login()
+    {
+        $connection = new TwitterOAuth($this->consumerKey, $this->consumerSecret);
+        $requestToken = $connection->oauth('oauth/request_token', ['oauth_callback' => $this->oauthCallbackUrl]);
+
+        session(['twitter_oauth_token' => $requestToken['oauth_token']]);
+        session(['twitter_oauth_token_secret' => $requestToken['oauth_token_secret']]);
+
+        $url = $connection->url('oauth/authorize', ['oauth_token' => $requestToken['oauth_token']]);
+
+        return [
+            'status' => true,
+            'data' => ['url' => $url]
+        ];
+    }
+
+    public function loginCallback(Request $request)
+    {
+        try {
+            $oauthToken = $request->get('oauth_token');
+            $oauthVerifier = $request->get('oauth_verifier');
+
+            $requestToken = session('twitter_oauth_token');
+            $requestTokenSecret = session('twitter_oauth_token_secret');
+
+            $connection = new TwitterOAuth(
+                $this->consumerKey,
+                $this->consumerSecret,
+                $requestToken,
+                $requestTokenSecret
+            );
+
+            $accessToken = $connection->oauth('oauth/access_token', [
+                'oauth_verifier' => $oauthVerifier
+            ]);
+            $expiresAt = Carbon::now()->addDays(90)->format('Y-m-d H:i:s');
+            return [
+                'status' => true,
+                'data' => [
+                    'accessToken' => $accessToken['oauth_token'],
+                    'backupField1' => json_encode(['accessTokenSecret'=>$accessToken['oauth_token_secret']]),
+                    'userId' => $accessToken['user_id'],
+                    'userName' => $accessToken['screen_name'],
+                    'accessToken_expiresAt' => $expiresAt,
+                ]
+            ];
+        } catch (\Exception $e) {
+            return ['status' => false, 'data' => $e->getMessage()];
+        }
+    }
+
+
+    public function postImage($message, $imagePaths, $accessToken = null)
+    {
+        Log::info('开始发布图片帖子');
+        try {
+            $this->twitterOAuth->setApiVersion('1.1'); // 强制使用 v1.1 API
+            $this->twitterOAuth->setTimeouts(15, 60);
+            $mediaIds = [];
+            foreach ($imagePaths as $imagePath) {
+                $imagePath = toStoragePath($imagePath);
+                if (!file_exists($imagePath)) {
+                    throw new \Exception("图片不存在: {$imagePath}");
+                }
+                //1.1版本上传媒体文件
+                $uploadedMedia = $this->twitterOAuth->upload('media/upload', [
+                    'media' => $imagePath
+                ]);
+                Log::info('1.1版本上传媒体文件完成');
+                if (isset($uploadedMedia->error)) {
+                    throw new \Exception('媒体上传失败: ' . json_encode($uploadedMedia));
+                }
+                $mediaIds[] = $uploadedMedia->media_id_string;
+            }
+            // 2.0版本发布推文
+            $this->twitterOAuth->setApiVersion('2');
+            $tweet = $this->twitterOAuth->post('tweets', [
+                'text' => $message,
+                'media' => !empty($mediaIds) ? ['media_ids' => $mediaIds] : null
+            ]);
+            Log::info('2.0版本发布推文完成');
+            if (isset($tweet->errors)) {
+                throw new \Exception('推文发布失败: ' . json_encode($tweet->errors));
+            }
+            return ['status' => true,
+                'data' => [
+                    'responseIds' => [$tweet->data->id],
+                ]];
+        } catch (\Exception $e) {
+            return ['status' => false, 'data' => $e->getMessage()];
+        }
+    }
+
+
+    public function postVideo($message, $videoPath, $accessToken = null)
+    {
+        try {
+            ini_set('memory_limit', '512M');
+            $this->twitterOAuth->setTimeouts(15, 120);
+            $this->twitterOAuth->setApiVersion('1.1');
+            $videoPath = toStoragePath($videoPath);
+
+            // Verify file exists and is readable
+            if (!file_exists($videoPath) || !is_readable($videoPath)) {
+                throw new Exception("Video file does not exist or is not readable: $videoPath");
+            }
+
+            Log::info('twitter上传视频'.$videoPath);
+
+
+            // Upload video with chunked upload
+            $uploadedMedia = $this->twitterOAuth->upload('media/upload', [
+                'media' => $videoPath,
+                'media_category' => 'tweet_video',
+            ],['chunkedUpload'=>true]);
+
+
+            Log::info('1.1版本,分块上传视频');
+
+            $code = $this->twitterOAuth->getLastHttpCode();
+
+
+            if (isset($uploadedMedia->error) || $code != 200) {
+                throw new \Exception('媒体上传失败: ' . json_encode($uploadedMedia));
+            }
+
+            $mediaId = $uploadedMedia->media_id_string;
+
+            $limit = 0;
+            do {
+                $status = $this->twitterOAuth->mediaStatus($mediaId);
+                Log::info('检测视频上传状态'.print_r($status,true));
+                sleep(5); // 等待 5 秒
+                $limit++;
+            } while ($status->processing_info->state !== 'succeeded' && $limit <= 3); // 最多重试 3 次
+
+            if ($status->processing_info->state === 'succeeded') {
+                //print_r($status->processing_info->state);
+                // 2.0版本发布推文
+                $this->twitterOAuth->setApiVersion('2');
+                $tweet = $this->twitterOAuth->post('tweets', [
+                    'text' => $message,
+                    'media' => ['media_ids' => [$mediaId]]
+                ]);
+
+                Log::info('2.0版本发布推文');
+
+                if (isset($tweet->errors)) {
+                    throw new \Exception('推文发布失败: ' . json_encode($tweet->errors));
+                }
+
+                return ['status' => true,
+                    'data' => [
+                        'responseIds' => [$tweet->data->id],
+                    ]
+                ];
+
+            } else {
+                return ['status' => false, 'data' => '视频上传失败或处理超时。'  ];
+            }
+        } catch (\Exception $e) {
+            return ['status' => false, 'data' => '视频上传异常: '.$e->getMessage()];
+        }
+    }
+
+
+    // 保持空实现
+    public function getComments($postId) {}
+    public function replyToComment($commentId) {}
+    public function deleteComment($commentId) {}
+}

+ 7 - 0
lang/en/global.php

@@ -238,6 +238,13 @@ return [
         'failed'               => 'Failed',
         'send_post'              => 'Send Post',
         'retrying'              => 'Retrying',
+        'account_management'    => 'Account Management',
+        'local_materials' => 'Posting With Local Content',
+        'select_local_media' => 'Select Local Media',
+        'choose_platforms'     => 'Select Social Media Platforms',
+        'operation_successful' => 'Operation successful',
+        'send_post_description' => 'The system has generated the sending queue. Please check the logs for details.',
+        'continue_publishing'   => 'Continue Publishing',
     ],
     'options' => [
         //

+ 8 - 0
lang/zh_CN/global.php

@@ -118,6 +118,7 @@ return [
         'youtube_category'      => 'Youtube分类',
         'send_post'              => '发送帖子',
 
+
     ],
     'labels' => [
         'list'         => '列表',
@@ -248,6 +249,13 @@ return [
         'send_post'              => '发送帖子',
         'Sending'              => '发送中',
         'retrying'              => '重试中',
+        'account_management'    => '账号管理',
+        'local_materials'   => '本地素材发帖',
+        'select_local_media' => '选择本地媒体',
+        'choose_platforms'     => '选择社媒平台',
+        'operation_successful' => '操作成功',
+        'send_post_description' => '系统已生成发送队列,详情请查看日志。',
+        'continue_publishing'   => '继续发布',
     ],
     'options' => [
         //

+ 0 - 55
public/index.php

@@ -1,55 +0,0 @@
-<?php
-
-use Illuminate\Contracts\Http\Kernel;
-use Illuminate\Http\Request;
-
-define('LARAVEL_START', microtime(true));
-
-/*
-|--------------------------------------------------------------------------
-| Check If The Application Is Under Maintenance
-|--------------------------------------------------------------------------
-|
-| If the application is in maintenance / demo mode via the "down" command
-| we will load this file so that any pre-rendered content can be shown
-| instead of starting the framework, which could cause an exception.
-|
-*/
-
-if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
-    require $maintenance;
-}
-
-/*
-|--------------------------------------------------------------------------
-| Register The Auto Loader
-|--------------------------------------------------------------------------
-|
-| Composer provides a convenient, automatically generated class loader for
-| this application. We just need to utilize it! We'll simply require it
-| into the script here so we don't need to manually load our classes.
-|
-*/
-
-require __DIR__.'/../vendor/autoload.php';
-
-/*
-|--------------------------------------------------------------------------
-| Run The Application
-|--------------------------------------------------------------------------
-|
-| Once we have the application, we can handle the incoming request using
-| the application's HTTP kernel. Then, we will send the response back
-| to this client's browser, allowing them to enjoy our application.
-|
-*/
-
-$app = require_once __DIR__.'/../bootstrap/app.php';
-
-$kernel = $app->make(Kernel::class);
-
-$response = $kernel->handle(
-    $request = Request::capture()
-)->send();
-
-$kernel->terminate($request, $response);

+ 1 - 1
resources/views/distributor/form_custom/completion-page.blade.php

@@ -34,7 +34,7 @@
             {{ $title }}
         </div>
         <div class="st-desc" style="margin-top: 10px">
-            {{ $description }} <a href="/dist/ssm-post">继续发布!</a>
+            {{ $description }} <a href="/dist/ssm-post">{{ $continue_publishing }}!</a>
         </div>
     </div>
 </div>