ソースを参照

Merge pull request #2347 from visuddhinanda/development

Development
visuddhinanda 8 ヶ月 前
コミット
d43cedfb4d

+ 126 - 0
api-v8/app/Console/Commands/TestProjectCopyTask.php

@@ -0,0 +1,126 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Str;
+
+class TestProjectCopyTask extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan test:project.copy.task project-50 dd9bcba8-ad3f-4082-9b52-4f5f8acdbd5f visuddhinanda
+     * @var string
+     */
+    protected $signature = 'test:project.copy.task {project} {task} {studio} {--token=}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '建立project 并复制task';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $appUrl = config('app.url');
+        $projectTitle = $this->argument('project');
+        $taskId = $this->argument('task');
+        $studioName = $this->argument('studio');
+        $token = $this->option('token');
+
+        // 如果 role 选项未提供(为空),提示用户输入
+        if (empty($token)) {
+            $token = $this->ask('Please enter the user token:');
+        }
+
+        $taskCount = $this->ask('Please enter the task count:');
+        $url = $appUrl . '/api/v2/project-tree';
+        $this->info('create project ' . $url);
+        $projects = array();
+        $rootId = Str::uuid();
+        $projects[] = [
+            'id' => $rootId,
+            'title' => $projectTitle,
+            'type' => "instance",
+            'parent_id' => '',
+            'weight' => 0,
+            'res_id' => $rootId,
+        ];
+        for ($i = 0; $i < $taskCount; $i++) {
+            $uid = Str::uuid();
+            $projects[] = [
+                'id' => $uid,
+                'title' => "{$projectTitle}_{$i}",
+                'type' => "instance",
+                'parent_id' => $rootId,
+                'weight' => 0,
+                'res_id' => $uid,
+            ];
+        }
+        $response = Http::withToken($token)
+            ->post($url, [
+                'studio_name' => $studioName,
+                'data' => $projects,
+            ]);
+        if ($response->failed()) {
+            $this->error('project create fail' . $response->json('message'));
+            Log::error('project create fail', ['data' => $response->body()]);
+            return 1;
+        }
+
+        $projectsData = $response->json()['data']['rows'];
+        $this->info('project :' . count($projectsData));
+        //获取task
+        $response = Http::withToken($token)
+            ->get($appUrl . '/api/v2/task/' . $taskId);
+        if ($response->failed()) {
+            $this->error('task read fail' . $response->json('message'));
+            Log::error('task read fail', ['data' => $response->body()]);
+            return 1;
+        }
+
+        //建立task
+        $task = $response->json()['data'];
+        $taskTitle = $task['title'];
+        $this->info('task title:' . $task['title']);
+        $tasks = array();
+        foreach ($projectsData as $key => $project) {
+            if ($project['isLeaf']) {
+                $task['title'] = "{$taskTitle}_{$key}";
+                $tasks[] = [
+                    'project_id' => $project['id'],
+                    'tasks' => [$task]
+                ];
+            }
+        }
+
+        $response = Http::withToken($token)
+            ->post($appUrl . '/api/v2/task-group', [
+                'data' => $tasks,
+            ]);
+        if ($response->failed()) {
+            $this->error('task create fail' . $response->json('message'));
+            Log::error('task create fail', ['data' => $response->body()]);
+            return 1;
+        }
+        return 0;
+    }
+}

+ 76 - 0
api-v8/app/Console/Commands/TestWorkerStartProject.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Http;
+
+class TestWorkerStartProject extends Command
+{
+    /**
+     * The name and signature of the console command.
+     * php artisan test:worker.start.project 0c3d2f69-1098-428b-95db-f1183667c799 restarted
+     * @var string
+     */
+    protected $signature = 'test:worker.start.project {project} {--token=}';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Command description';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+        $appUrl = config('app.url');
+        $projectId = $this->argument('project');
+        $token = $this->option('token');
+        // 如果 role 选项未提供(为空),提示用户输入
+        if (empty($token)) {
+            $token = $this->ask('Please enter the user token:');
+        }
+
+        $status = $this->choice(
+            'Which framework do you prefer?',
+            ['published', 'restarted', 'stop'],
+            0 // 默认选择 Laravel(索引 0)
+        );
+
+        $response = Http::withToken($token)
+            ->get($appUrl . "/api/v2/task?view=project&project_id={$projectId}&status=all&order=order&dir=asc");
+        if ($response->failed()) {
+            $this->error('task read fail' . $response->json('message'));
+            Log::error('task read fail', ['data' => $response->body()]);
+            return 1;
+        }
+        $tasks = $response->json()['data']['rows'];
+        foreach ($tasks as $key => $task) {
+            $this->info("[{$key}]task " . $task['title'] . ' status ' . $task['status']);
+            $response = Http::withToken($token)
+                ->patch($appUrl . "/api/v2/task-status/" . $task['id'], ['status' => $status]);
+            if ($response->failed()) {
+                $this->error('task status fail' . $response->json('message'));
+                Log::error('task status fail', ['data' => $response->body()]);
+            }
+            $this->info("[{$key}]task status changed {$status}");
+        }
+        return 0;
+    }
+}

+ 2 - 1
api-v8/app/Http/Api/StudioApi.php

@@ -52,7 +52,8 @@ class StudioApi
             }
             if ($userInfo->avatar) {
                 $img = str_replace('.jpg', '_s.jpg', $userInfo->avatar);
-                if (App::environment('local')) {
+
+                if (App::environment(['local', 'testing'])) {
                     $data['avatar'] = Storage::url($img);
                 } else {
                     $data['avatar'] = Storage::temporaryUrl($img, now()->addDays(6));

+ 1 - 1
api-v8/app/Http/Api/UserApi.php

@@ -90,7 +90,7 @@ class UserApi
         }
         if ($user->avatar) {
             $img = str_replace('.jpg', '_s.jpg', $user->avatar);
-            if (App::environment('local')) {
+            if (App::environment(['local', 'testing'])) {
                 $data['avatar'] = Storage::url($img);
             } else {
                 $data['avatar'] = Storage::temporaryUrl($img, now()->addDays(6));

+ 248 - 0
api-v8/app/Http/Controllers/MockOpenAIController.php

@@ -0,0 +1,248 @@
+<?php
+
+namespace App\Http\Controllers;
+
+use Illuminate\Http\Request;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Support\Str;
+
+class MockOpenAIController extends Controller
+{
+    /**
+     * 模拟 Chat Completions API
+     */
+    public function chatCompletions(Request $request): JsonResponse
+    {
+        $delay = $request->query('delay', true);
+        if ($delay === true) {
+            // 随机延迟
+            $this->randomDelay();
+        }
+        $error = $request->query('error', true);
+        // 随机返回错误
+        if ($error === true) {
+            if ($errorResponse = $this->randomError()) {
+                return $errorResponse;
+            }
+        }
+
+        $model = $request->input('model', 'gpt-3.5-turbo');
+        $messages = $request->input('messages', []);
+
+        return response()->json([
+            'id' => 'chatcmpl-' . Str::random(29),
+            'object' => 'chat.completion',
+            'created' => time(),
+            'model' => $model,
+            'choices' => [
+                [
+                    'index' => 0,
+                    'message' => [
+                        'role' => 'assistant',
+                        'content' => $this->generateMockResponse($messages)
+                    ],
+                    'finish_reason' => 'stop'
+                ]
+            ],
+            'usage' => [
+                'prompt_tokens' => rand(10, 100),
+                'completion_tokens' => rand(20, 200),
+                'total_tokens' => rand(30, 300)
+            ]
+        ]);
+    }
+
+    /**
+     * 模拟 Completions API
+     */
+    public function completions(Request $request): JsonResponse
+    {
+        $delay = $request->query('delay', true);
+        if ($delay === true) {
+            // 随机延迟
+            $this->randomDelay();
+        }
+        $error = $request->query('error', true);
+        // 随机返回错误
+        if ($error === true) {
+            if ($errorResponse = $this->randomError()) {
+                return $errorResponse;
+            }
+        }
+
+        $model = $request->input('model', 'text-davinci-003');
+        $prompt = $request->input('prompt', '');
+
+        return response()->json([
+            'id' => 'cmpl-' . Str::random(29),
+            'object' => 'text_completion',
+            'created' => time(),
+            'model' => $model,
+            'choices' => [
+                [
+                    'text' => $this->generateMockTextResponse($prompt),
+                    'index' => 0,
+                    'logprobs' => null,
+                    'finish_reason' => 'stop'
+                ]
+            ],
+            'usage' => [
+                'prompt_tokens' => rand(10, 100),
+                'completion_tokens' => rand(20, 200),
+                'total_tokens' => rand(30, 300)
+            ]
+        ]);
+    }
+
+    /**
+     * 模拟 Models API
+     */
+    public function models(Request $request): JsonResponse
+    {
+        // 随机延迟
+        $this->randomDelay();
+
+        // 随机返回错误
+        if ($errorResponse = $this->randomError()) {
+            return $errorResponse;
+        }
+
+        return response()->json([
+            'object' => 'list',
+            'data' => [
+                [
+                    'id' => 'gpt-4',
+                    'object' => 'model',
+                    'created' => 1687882411,
+                    'owned_by' => 'openai'
+                ],
+                [
+                    'id' => 'gpt-3.5-turbo',
+                    'object' => 'model',
+                    'created' => 1677610602,
+                    'owned_by' => 'openai'
+                ],
+                [
+                    'id' => 'text-davinci-003',
+                    'object' => 'model',
+                    'created' => 1669599635,
+                    'owned_by' => 'openai-internal'
+                ]
+            ]
+        ]);
+    }
+
+    /**
+     * 随机延迟
+     */
+    private function randomDelay(): void
+    {
+        // 90% 概率 1-3秒延迟
+        // 10% 概率 60-100秒延迟
+        if (rand(1, 100) <= 10) {
+            sleep(rand(60, 100));
+        } else {
+            sleep(rand(1, 3));
+        }
+    }
+
+    /**
+     * 随机返回错误响应
+     */
+    private function randomError(): ?JsonResponse
+    {
+        // 20% 概率返回错误
+        if (rand(1, 100) <= 20) {
+            $errorType = rand(1, 3);
+
+            switch ($errorType) {
+                case 1:
+                    return $this->badRequestError();
+                case 2:
+                    return $this->internalServerError();
+                case 3:
+                    return $this->rateLimitError();
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * 400 错误响应
+     */
+    private function badRequestError(): JsonResponse
+    {
+        return response()->json([
+            'error' => [
+                'message' => 'Invalid request: missing required parameter',
+                'type' => 'invalid_request_error',
+                'param' => null,
+                'code' => null
+            ]
+        ], 400);
+    }
+
+    /**
+     * 500 错误响应
+     */
+    private function internalServerError(): JsonResponse
+    {
+        return response()->json([
+            'error' => [
+                'message' => 'The server had an error while processing your request. Sorry about that!',
+                'type' => 'server_error',
+                'param' => null,
+                'code' => null
+            ]
+        ], 500);
+    }
+
+    /**
+     * 429 限流错误响应
+     */
+    private function rateLimitError(): JsonResponse
+    {
+        return response()->json([
+            'error' => [
+                'message' => 'Rate limit reached for requests',
+                'type' => 'requests',
+                'param' => null,
+                'code' => 'rate_limit_exceeded'
+            ]
+        ], 429);
+    }
+
+    /**
+     * 生成模拟聊天响应
+     */
+    private function generateMockResponse(array $messages): string
+    {
+        $responses = [
+            "这是一个模拟的AI响应。我正在模拟OpenAI的API服务器。",
+            "感谢您的问题!这是一个测试响应,用于模拟真实的AI助手。",
+            "我是一个模拟的AI助手。您的请求已被处理,这是模拟生成的回复。",
+            "模拟Hello! This is a mock response from the simulated OpenAI API server.",
+            "模拟Thank you for your message. This is a simulated response for testing purposes.",
+            "模拟I understand your question. This is a mock reply generated by the test API server.",
+        ];
+
+        return $responses[array_rand($responses)] . " (响应时间: " . date('Y-m-d H:i:s') . ")";
+    }
+
+    /**
+     * 生成模拟文本补全响应
+     */
+    private function generateMockTextResponse(string $prompt): string
+    {
+        $responses = [
+            " 这是对您提示的模拟补全回复。",
+            " Mock completion response for your prompt.",
+            " 模拟的文本补全结果,用于测试目的。",
+            " This is a simulated text completion.",
+            " 基于您的输入生成的模拟响应。",
+        ];
+
+        return $responses[array_rand($responses)];
+    }
+}

+ 2 - 1
api-v8/app/Services/AiTranslateService.php

@@ -273,7 +273,8 @@ class AiTranslateService
                 ["role" => "system", "content" => $message->model->system_prompt ?? ''],
                 ["role" => "user", "content" => $message->prompt],
             ],
-            "temperature" => 0.7,
+            "temperature" => 0.3,  # 低随机性,确保准确
+            "top_k" => 20,         # 限制候选词范围
             "stream" => false
         ];
         if ($this->openaiProxy) {

+ 5 - 0
api-v8/routes/api.php

@@ -116,6 +116,7 @@ use App\Http\Controllers\AiAssistantController;
 use App\Http\Controllers\ModelLogController;
 use App\Http\Controllers\SentenceAttachmentController;
 use App\Http\Controllers\EmailCertificationController;
+use App\Http\Controllers\MockOpenAIController;
 
 
 
@@ -290,4 +291,8 @@ Route::group(['prefix' => 'v2'], function () {
     Route::apiResource('sentence-attachment', SentenceAttachmentController::class);
     Route::apiResource('email-certification', EmailCertificationController::class);
     Route::apiResource('sentence-info', SentenceInfoController::class);
+
+    Route::post('mock/openai/chat/completions', [MockOpenAIController::class, 'chatCompletions']);
+    Route::post('mock/openai/completions', [MockOpenAIController::class, 'completions']);
+    Route::get('mock/openai/models', [MockOpenAIController::class, 'models']);
 });

+ 275 - 0
api-v8/tests/Feature/MockOpenAIApiTest.php

@@ -0,0 +1,275 @@
+<?php
+
+namespace Tests\Feature;
+
+use Tests\TestCase;
+
+class MockOpenAIApiTest extends TestCase
+{
+
+    protected string $baseUrl = '/api/v2/mock/openai';
+    protected string $validApiKey = 'Bearer test-api-key-12345';
+    protected string $invalidApiKey = 'Bearer invalid-key';
+
+
+    /**
+     * 测试聊天完成 API - 成功响应
+     */
+    public function test_chat_completions_success_response()
+    {
+        $response = $this->postJson($this->baseUrl . '/chat/completions', [
+            'model' => 'gpt-3.5-turbo',
+            'messages' => [
+                [
+                    'role' => 'user',
+                    'content' => 'Hello, this is a test message'
+                ]
+            ]
+        ], [
+            'Authorization' => $this->validApiKey
+        ]);
+
+        // 由于有随机错误,我们需要处理可能的错误响应
+        if ($response->status() === 200) {
+            $response->assertStatus(200)
+                ->assertJsonStructure([
+                    'id',
+                    'object',
+                    'created',
+                    'model',
+                    'choices' => [
+                        '*' => [
+                            'index',
+                            'message' => [
+                                'role',
+                                'content'
+                            ],
+                            'finish_reason'
+                        ]
+                    ],
+                    'usage' => [
+                        'prompt_tokens',
+                        'completion_tokens',
+                        'total_tokens'
+                    ]
+                ]);
+
+            $responseData = $response->json();
+            $this->assertEquals('chat.completion', $responseData['object']);
+            $this->assertEquals('gpt-3.5-turbo', $responseData['model']);
+            $this->assertEquals('assistant', $responseData['choices'][0]['message']['role']);
+            $this->assertStringContainsString('模拟', $responseData['choices'][0]['message']['content']);
+        } else {
+            // 如果是错误响应,验证错误格式
+            $this->assertContains($response->status(), [400, 429, 500]);
+            $response->assertJsonStructure([
+                'error' => [
+                    'message',
+                    'type'
+                ]
+            ]);
+        }
+    }
+
+    /**
+     * 测试文本完成 API - 成功响应
+     */
+    public function test_completions_success_response()
+    {
+        $response = $this->postJson($this->baseUrl . '/completions', [
+            'model' => 'text-davinci-003',
+            'prompt' => 'Once upon a time'
+        ], [
+            'Authorization' => $this->validApiKey
+        ]);
+
+        if ($response->status() === 200) {
+            $response->assertStatus(200)
+                ->assertJsonStructure([
+                    'id',
+                    'object',
+                    'created',
+                    'model',
+                    'choices' => [
+                        '*' => [
+                            'text',
+                            'index',
+                            'logprobs',
+                            'finish_reason'
+                        ]
+                    ],
+                    'usage'
+                ]);
+
+            $responseData = $response->json();
+            $this->assertEquals('text_completion', $responseData['object']);
+            $this->assertEquals('text-davinci-003', $responseData['model']);
+        } else {
+            $this->assertContains($response->status(), [400, 429, 500]);
+        }
+    }
+
+    /**
+     * 测试模型列表 API
+     */
+    public function test_models_list_response()
+    {
+        $response = $this->getJson($this->baseUrl . '/models', [
+            'Authorization' => $this->validApiKey
+        ]);
+
+        if ($response->status() === 200) {
+            $response->assertStatus(200)
+                ->assertJsonStructure([
+                    'object',
+                    'data' => [
+                        '*' => [
+                            'id',
+                            'object',
+                            'created',
+                            'owned_by'
+                        ]
+                    ]
+                ]);
+
+            $responseData = $response->json();
+            $this->assertEquals('list', $responseData['object']);
+            $this->assertGreaterThan(0, count($responseData['data']));
+
+            // 验证包含预期的模型
+            $modelIds = collect($responseData['data'])->pluck('id')->toArray();
+            $this->assertContains('gpt-4', $modelIds);
+            $this->assertContains('gpt-3.5-turbo', $modelIds);
+        } else {
+            $this->assertContains($response->status(), [400, 429, 500]);
+        }
+    }
+
+    /**
+     * 测试错误响应格式
+     */
+    public function test_error_response_formats()
+    {
+        // 多次请求以增加遇到错误的概率
+        for ($i = 0; $i < 10; $i++) {
+            $response = $this->postJson($this->baseUrl . '/chat/completions', [
+                'model' => 'gpt-3.5-turbo',
+                'messages' => [
+                    ['role' => 'user', 'content' => "Test message $i"]
+                ]
+            ], [
+                'Authorization' => $this->validApiKey
+            ]);
+
+            if (in_array($response->status(), [400, 429, 500])) {
+                $response->assertJsonStructure([
+                    'error' => [
+                        'message',
+                        'type'
+                    ]
+                ]);
+
+                $errorData = $response->json()['error'];
+                $this->assertNotEmpty($errorData['message']);
+                $this->assertNotEmpty($errorData['type']);
+
+                // 验证特定错误类型
+                switch ($response->status()) {
+                    case 400:
+                        $this->assertEquals('invalid_request_error', $errorData['type']);
+                        break;
+                    case 429:
+                        $this->assertEquals('requests', $errorData['type']);
+                        break;
+                    case 500:
+                        $this->assertEquals('server_error', $errorData['type']);
+                        break;
+                }
+
+                // 找到一个错误响应就足够了
+                break;
+            }
+        }
+    }
+
+    /**
+     * 测试响应时间(延迟)
+     */
+    public function test_response_delay()
+    {
+        $startTime = microtime(true);
+
+        $response = $this->postJson($this->baseUrl . '/chat/completions', [
+            'model' => 'gpt-3.5-turbo',
+            'messages' => [
+                ['role' => 'user', 'content' => 'Test delay']
+            ]
+        ], [
+            'Authorization' => $this->validApiKey
+        ]);
+
+        $endTime = microtime(true);
+        $duration = $endTime - $startTime;
+
+        // 验证至少有1秒延迟
+        $this->assertGreaterThanOrEqual(1, $duration);
+
+        // 记录响应时间用于调试
+        echo "\nResponse time: " . number_format($duration, 2) . " seconds\n";
+    }
+
+    /**
+     * 测试请求参数验证
+     */
+    public function test_request_parameters()
+    {
+        // 测试自定义模型参数
+        $response = $this->postJson($this->baseUrl . '/chat/completions', [
+            'model' => 'gpt-4',
+            'messages' => [
+                ['role' => 'user', 'content' => 'Hello with GPT-4']
+            ],
+            'max_tokens' => 100,
+            'temperature' => 0.7
+        ], [
+            'Authorization' => $this->validApiKey
+        ]);
+
+        if ($response->status() === 200) {
+            $responseData = $response->json();
+            $this->assertEquals('gpt-4', $responseData['model']);
+        }
+    }
+
+    /**
+     * 测试并发请求
+     */
+    public function test_concurrent_requests()
+    {
+        $promises = [];
+        $responses = [];
+
+        // 发送5个并发请求
+        for ($i = 0; $i < 5; $i++) {
+            $responses[] = $this->postJson($this->baseUrl . '/chat/completions', [
+                'model' => 'gpt-3.5-turbo',
+                'messages' => [
+                    ['role' => 'user', 'content' => "Concurrent test $i"]
+                ]
+            ], [
+                'Authorization' => $this->validApiKey
+            ]);
+        }
+
+        // 验证所有响应
+        foreach ($responses as $index => $response) {
+            $this->assertContains($response->status(), [200, 400, 429, 500]);
+
+            if ($response->status() === 200) {
+                $response->assertJsonStructure(['id', 'object', 'choices']);
+            } else {
+                $response->assertJsonStructure(['error']);
+            }
+        }
+    }
+}