diff --git a/app/Client/TmsService.php b/app/Client/TmsService.php new file mode 100644 index 0000000..fc61031 --- /dev/null +++ b/app/Client/TmsService.php @@ -0,0 +1,66 @@ +endpoint = $endpoint; + $this->region = $region; + // 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey + // 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取 + $this->credential = new Credential($secretId, $secretKey); + } + + public function checkText(string $content): bool | Exception + { + try { + // 实例化一个http选项,可选的,没有特殊需求可以跳过 + $httpProfile = new HttpProfile(); + $httpProfile->setEndpoint($this->endpoint); + + // 实例化一个client选项,可选的,没有特殊需求可以跳过 + $clientProfile = new ClientProfile(); + $clientProfile->setHttpProfile($httpProfile); + // 实例化要请求产品的client对象,clientProfile是可选的 + $client = new TmsClient($this->credential, $this->region, $clientProfile); + + // 实例化一个请求对象,每个接口都会对应一个request对象 + $req = new TextModerationRequest(); + + $params = [ + "Content" => base64_encode($content), + ]; + $req->fromJsonString(json_encode($params)); + + // 返回的resp是一个TextModerationResponse的实例,与请求对象对应 + $resp = $client->TextModeration($req); + // 广告予以放行(适用于广告文案) + if ($resp->Suggestion === 'Block' && $resp->Label !== 'Ad') { + return false; + } + // 政治话题红线最高,即使疑似也予以拦截 + if ($resp->Suggestion === 'Review' && $resp->Label === 'Polity') { + return false; + } + return true; + } catch (TencentCloudSDKException $e) { + throw $e; + } + } +} diff --git a/app/Facades/TmsFacade.php b/app/Facades/TmsFacade.php new file mode 100644 index 0000000..cfc5bf0 --- /dev/null +++ b/app/Facades/TmsFacade.php @@ -0,0 +1,16 @@ + 'system', 'content' => 'You are GeekChat - A chatbot that can understand text, voice, draw image and translate. Answer as concisely as possible. Using Simplified Chinese as the first language.']; + private $preset = ['role' => 'system', 'content' => 'You are GeekChat - A ChatGPT clone. Answer as concisely as possible. Using Simplified Chinese as the first language.']; public function index() { @@ -37,8 +38,12 @@ public function chat(Request $request): JsonResponse $request->validate([ 'prompt' => 'required|string', 'regen' => ['required', 'in:true,false'], - 'api_key' => 'sometimes|string', + 'api_key' => 'required|string', ]); + $passed = TmsFacade::checkText($request->input('prompt')); + if (!$passed) { + return response()->json(['message' => CONTENT_ERROR], 400); + } $regen = $request->boolean('regen'); $messages = $request->session()->get('messages', [$this->preset]); if ($regen && count($messages) == 1) { @@ -68,8 +73,12 @@ public function translate(Request $request): JsonResponse $request->validate([ 'prompt' => 'required|string', 'regen' => ['required', 'in:true,false'], - 'api_key' => 'sometimes|string', + 'api_key' => 'required|string', ]); + $passed = TmsFacade::checkText($request->input('prompt')); + if (!$passed) { + return response()->json(['message' => CONTENT_ERROR], 400); + } $regen = $request->boolean('regen'); $messages = $request->session()->get('messages', [$this->preset]); if ($regen && count($messages) == 1) { @@ -100,6 +109,11 @@ public function stream(Request $request) if ($request->session()->get('chat_id') != $request->input('chat_id')) { abort(400); } + $apiKey = $request->input('api_key'); + if (empty($apiKey)) { + abort(403); + } + $apiKey = base64_decode($apiKey); $messages = $request->session()->get('messages'); @@ -111,22 +125,18 @@ public function stream(Request $request) // 实时将流式响应数据发送到客户端 $respData = ''; - $apiKey = $request->input('api_key'); - if ($apiKey) { - $apiKey = base64_decode($apiKey); - } header('Access-Control-Allow-Origin: *'); header('Content-type: text/event-stream'); header('Cache-Control: no-cache'); header('X-Accel-Buffering: no'); - OpenAI::withToken($apiKey)->chat($params, function ($ch, $data) use ($apiKey, &$respData) { + OpenAI::withToken($apiKey)->chat($params, function ($ch, $data) use (&$respData) { $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if ($httpCode >= 400) { echo "data: [ERROR] $httpCode"; - if (($httpCode == 400 || $httpCode == 401) && empty($apiKey)) { + /* if (($httpCode == 400 || $httpCode == 401) && empty($apiKey)) { // app key 耗尽自动切换到下一个免费的 key Artisan::call('app:update-open-ai-key'); - } + }*/ } else { $respData .= $data; echo $data;; @@ -168,16 +178,15 @@ public function audio(Request $request): JsonResponse ->min(1) // 最小不低于 1 KB ->max(10 * 1024), // 最大不超过 10 MB ], - 'api_key' => 'sometimes|string', + 'api_key' => 'required|string', ]); - // 保存到本地 $fileName = Str::uuid() . '.wav'; $dir = 'audios' . date('/Y/m/d', time()); $path = $request->audio->storeAs($dir, $fileName, 'local'); $messages = $request->session()->get('messages', [ - ['role' => 'system', 'content' => 'You are GeekChat - A chatbot that can understand text, voice, draw image and translate. Answer as concisely as possible. Make Mandarin Chinese the primary language'] + ['role' => 'system', 'content' => 'You are GeekChat - A ChatGPT clone. Answer as concisely as possible. Make Mandarin Chinese the primary language'] ]); // $path = 'audios/2023/03/09/test.wav';(测试用) // 调用 speech to text API 将语音转化为文字 @@ -199,7 +208,10 @@ public function audio(Request $request): JsonResponse } return response()->json(['message' => ['role' => 'assistant', 'content' => '对不起,我没有听清你说的话,请再试一次']]); } - + $passed = TmsFacade::checkText($result->text); + if (!$passed) { + return response()->json(['message' => CONTENT_ERROR], 400); + } // 接下来的流程和 ChatGPT 一样 $userMessage = ['role' => 'user', 'content' => $result->text]; $messages[] = $userMessage; @@ -214,8 +226,12 @@ public function image(Request $request): JsonResponse $request->validate([ 'prompt' => 'required|string', 'regen' => ['required', 'in:true,false'], - 'api_key' => 'sometimes|string', + 'api_key' => 'required|string', ]); + $passed = TmsFacade::checkText($request->input('prompt')); + if (!$passed) { + return response()->json(['message' => CONTENT_ERROR], 400); + } $regen = $request->boolean('regen'); $messages = $request->session()->get('messages', [$this->preset]); if ($regen && count($messages) == 1) { @@ -229,18 +245,14 @@ public function image(Request $request): JsonResponse array_pop($messages); } $apiKey = $request->input('api_key'); - $size = '256x256'; - if (!empty($apiKey)) { - $size = '1024x1024'; - } $response = OpenAI::withToken($apiKey)->image([ "prompt" => $prompt, "n" => 1, - "size" => $size, + "size" => '256x256', "response_format" => "url", ]); $result = json_decode($response); - $image = '画图失败,如果你设置了自己的key,请确保它是有效的'; + $image = '画图失败,请确保 API KEY 是有效的'; if (isset($result->data[0]->url)) { $image = '![](' . $result->data[0]->url . ')'; } @@ -266,12 +278,7 @@ public function valid(Request $request): JsonResponse 'api_key' => 'required|string' ]); $apiKey = $request->input('api_key'); - if (empty($apiKey)) { - return response()->json(['valid' => false, 'error' => '无效的 API KEY']); - } - $response = Http::withToken($apiKey)->timeout(15) - ->get(config('openai.base_uri') . '/dashboard/billing/credit_grants'); - if ($response->failed()) { + if (empty($apiKey) || strlen($apiKey) != 51 || !Str::startsWith($apiKey, 'sk-')) { return response()->json(['valid' => false, 'error' => '无效的 API KEY']); } return response()->json(['valid' => true]); diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 0d71900..2cc232d 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -47,12 +47,12 @@ protected function configureRateLimiting(): void RateLimiter::for('chat', function (Request $request) { return $request->input('api_key') ? Limit::none() - : Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); + : Limit::perMinute(600)->by($request->user()?->id ?: $request->ip()); }); RateLimiter::for('audio', function (Request $request) { return $request->input('api_key') ? Limit::none() - : Limit::perMinute(30)->by($request->user()?->id ?: $request->ip()); + : Limit::perMinute(300)->by($request->user()?->id ?: $request->ip()); }); RateLimiter::for('image', function (Request $request) { return $request->input('api_key') diff --git a/app/Providers/TmsServiceProvider.php b/app/Providers/TmsServiceProvider.php new file mode 100644 index 0000000..8d20126 --- /dev/null +++ b/app/Providers/TmsServiceProvider.php @@ -0,0 +1,34 @@ +app->singleton(TmsService::class, static function (): TmsService { + $secretId = config('tencent.secret_id'); + $secretKey = config('tencent.secret_key'); + $tmsEndpoint = config('tencent.tms_endpoint'); + $region = config('tencent.region'); + + return new TmsService($secretId, $secretKey, $tmsEndpoint, $region); + }); + + $this->app->alias(TmsService::class, 'tms'); + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + // + } +} diff --git a/composer.json b/composer.json index 23ec65d..f104d25 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "orhanerday/open-ai": "^4.7", "spatie/laravel-responsecache": "^7.4", "spiral/roadrunner": "^2.8.2", + "tencentcloud/tms": "^3.0", "tightenco/ziggy": "^1.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index ff9ec7e..fdd80fb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4110333a8701670b1f8d0a5f5b597cf5", + "content-hash": "2d44e4edb81b503b49b81904515fe5b5", "packages": [ { "name": "brick/math", @@ -6588,6 +6588,93 @@ ], "time": "2023-02-16T09:57:23+00:00" }, + { + "name": "tencentcloud/common", + "version": "3.0.854", + "source": { + "type": "git", + "url": "https://github.com/tencentcloud-sdk-php/common.git", + "reference": "03c5b2892a3a9ef8deab0d133895c8fc0e51c64a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tencentcloud-sdk-php/common/zipball/03c5b2892a3a9ef8deab0d133895c8fc0e51c64a", + "reference": "03c5b2892a3a9ef8deab0d133895c8fc0e51c64a", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.3||^7.0", + "php": ">=5.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "TencentCloud\\": "./src/TencentCloud" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "tencentcloudapi", + "email": "tencentcloudapi@tencent.com", + "homepage": "https://cloud.tencent.com/document/sdk/PHP", + "role": "Developer" + } + ], + "description": "TencentCloudApi php sdk", + "homepage": "https://github.com/tencentcloud-sdk-php/common", + "support": { + "issues": "https://github.com/tencentcloud-sdk-php/common/issues", + "source": "https://github.com/tencentcloud-sdk-php/common/tree/3.0.854" + }, + "time": "2023-03-28T00:16:48+00:00" + }, + { + "name": "tencentcloud/tms", + "version": "3.0.854", + "source": { + "type": "git", + "url": "https://github.com/tencentcloud-sdk-php/tms.git", + "reference": "2d23dc83283cb6fe6cee9bc385a8b1879c536909" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tencentcloud-sdk-php/tms/zipball/2d23dc83283cb6fe6cee9bc385a8b1879c536909", + "reference": "2d23dc83283cb6fe6cee9bc385a8b1879c536909", + "shasum": "" + }, + "require": { + "tencentcloud/common": "3.0.854" + }, + "type": "library", + "autoload": { + "psr-4": { + "TencentCloud\\": "./src/TencentCloud" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "tencentcloudapi", + "email": "tencentcloudapi@tencent.com", + "homepage": "https://github.com/tencentcloud-sdk-php/tms", + "role": "Developer" + } + ], + "description": "TencentCloudApi php sdk tms", + "homepage": "https://github.com/tencentcloud-sdk-php/tms", + "support": { + "issues": "https://github.com/tencentcloud-sdk-php/tms/issues", + "source": "https://github.com/tencentcloud-sdk-php/tms/tree/3.0.854" + }, + "time": "2023-03-28T00:43:58+00:00" + }, { "name": "tightenco/ziggy", "version": "v1.5.0", diff --git a/config/app.php b/config/app.php index dc0ffa7..2706735 100644 --- a/config/app.php +++ b/config/app.php @@ -198,7 +198,7 @@ // App\Providers\BroadcastServiceProvider::class, \App\Providers\EventServiceProvider::class, \App\Providers\RouteServiceProvider::class, - + \App\Providers\TmsServiceProvider::class, ], /* diff --git a/config/tencent.php b/config/tencent.php new file mode 100644 index 0000000..4db68d9 --- /dev/null +++ b/config/tencent.php @@ -0,0 +1,7 @@ + env('TENCENT_SECRET_ID'), + 'secret_key' => env('TENCENT_SECRET_KEY'), + 'tms_endpoint' => env('TENCENT_TMS_ENDPOINT'), + 'region' => env('TENCENT_REGION'), +]; diff --git a/public/index.php b/public/index.php index 56f6d8d..31498af 100644 --- a/public/index.php +++ b/public/index.php @@ -5,6 +5,7 @@ define('LARAVEL_START', microtime(true)); define('SYSTEM_ERROR', '服务端出现异常,[请联系管理员](https://image.gstatics.cn/wechat.webp)。'); +define('CONTENT_ERROR', '内容不合规,请勿将本工具用于正常工作、学习、生活之外的无关场景'); /* diff --git a/resources/js/Pages/Chat.vue b/resources/js/Pages/Chat.vue index 9344851..ca3c934 100644 --- a/resources/js/Pages/Chat.vue +++ b/resources/js/Pages/Chat.vue @@ -21,6 +21,10 @@ const form = useForm({ }) const chat = () => { + if (apiKey.value == '') { + alert('请先输入 API KEY') + return + } if (isTyping.value) { return; } @@ -34,6 +38,10 @@ const reset = () => { } const audio = (blob) => { + if (apiKey.value == '') { + alert('请先输入 API KEY') + return + } if (isTyping.value) { return; } @@ -52,6 +60,10 @@ const image = () => { alert('请在输入框输入图片描述信息') return } + if (apiKey.value == '') { + alert('请先输入 API KEY') + return + } if (isTyping.value) { return; } @@ -64,6 +76,10 @@ const translate = () => { alert('请在输入框输入待翻译内容') return } + if (apiKey.value == '') { + alert('请先输入 API KEY') + return + } if (isTyping.value) { return; } @@ -72,7 +88,7 @@ const translate = () => { } const enterApiKey = () => { - const api_key = prompt("输入你的 OpenAI API KEY(使用自己的 KEY 可以不受共享通道频率限制,还可以绘制大尺寸图片):"); + const api_key = prompt("输入你的 OpenAI API KEY:"); store.dispatch('validAndSetApiKey', api_key) } @@ -102,7 +118,7 @@ const regenerate = () => {