Skip to content

Commit dd18af8

Browse files
committed
支持自定义key
1 parent fb27cc3 commit dd18af8

File tree

11 files changed

+210
-41
lines changed

11 files changed

+210
-41
lines changed

app/Client/OpenAI.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace App\Client;
4+
5+
use Orhanerday\OpenAi\OpenAi as BaseOpenAi;
6+
7+
class OpenAI extends BaseOpenAi
8+
{
9+
public function __construct($apiKey)
10+
{
11+
parent::__construct($apiKey);
12+
}
13+
14+
public function withToken($token = '')
15+
{
16+
if ($token) {
17+
$this->setHeader([1 => 'Authorization: Bearer ' . $token]);
18+
return $this;
19+
} else {
20+
return $this;
21+
}
22+
}
23+
}

app/Facades/OpenAI.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Illuminate\Support\Facades\Facade;
88

99
/**
10+
* @method static \App\Client\OpenAI withToken($token = '')
1011
* @method static string chat()
1112
* @method static string completion()
1213
* @method static string embeddings()

app/Http/Controllers/ChatController.php

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Illuminate\Support\Str;
99
use Illuminate\Http\JsonResponse;
1010
use Illuminate\Support\Facades\Artisan;
11+
use Illuminate\Support\Facades\Http;
1112
use Illuminate\Support\Facades\Log;
1213
use Illuminate\Support\Facades\Storage;
1314
use Illuminate\Validation\Rules\File;
@@ -34,7 +35,8 @@ public function messages(): JsonResponse
3435
public function chat(Request $request): JsonResponse
3536
{
3637
$request->validate([
37-
'prompt' => 'required|string'
38+
'prompt' => 'required|string',
39+
'api_key' => 'sometimes|string',
3840
]);
3941
$messages = $request->session()->get('messages', [$this->preset]);
4042
$userMessage = ['role' => 'user', 'content' => $request->input('prompt')];
@@ -51,7 +53,8 @@ public function chat(Request $request): JsonResponse
5153
public function translate(Request $request): JsonResponse
5254
{
5355
$request->validate([
54-
'prompt' => 'required|string'
56+
'prompt' => 'required|string',
57+
'api_key' => 'sometimes|string',
5558
]);
5659
$messages = $request->session()->get('messages', [$this->preset]);
5760
// 判断内容是中文还是英文
@@ -85,25 +88,29 @@ public function stream(Request $request)
8588

8689
// 实时将流式响应数据发送到客户端
8790
$respData = '';
91+
$apiKey = $request->input('api_key');
92+
if ($apiKey) {
93+
$apiKey = base64_decode($apiKey);
94+
}
8895
header('Access-Control-Allow-Origin: *');
8996
header('Content-type: text/event-stream');
9097
header('Cache-Control: no-cache');
9198
header('X-Accel-Buffering: no');
92-
OpenAI::chat($params, function ($ch, $data) use (&$respData) {
99+
OpenAI::withToken($apiKey)->chat($params, function ($ch, $data) use ($apiKey, &$respData) {
93100
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
94101
if ($httpCode >= 400) {
95102
echo "data: [ERROR] $httpCode";
96-
if ($httpCode == 429) {
103+
if (($httpCode == 400 || $httpCode == 401) && empty($apiKey)) {
97104
// app key 耗尽自动切换到下一个免费的 key
98105
Artisan::call('app:update-open-ai-key');
99106
}
100107
} else {
101108
$respData .= $data;
102109
echo $data;;
103-
ob_flush();
104-
flush();
105-
return strlen($data);
106110
}
111+
ob_flush();
112+
flush();
113+
return strlen($data);
107114
});
108115

109116
// 将响应数据存储到当前会话中以便刷新页面后可以看到聊天记录
@@ -137,7 +144,8 @@ public function audio(Request $request): JsonResponse
137144
File::types(['mp3', 'mp4', 'mpeg', 'mpga', 'm4a', 'wav', 'webm'])
138145
->min(1) // 最小不低于 1 KB
139146
->max(10 * 1024), // 最大不超过 10 MB
140-
]
147+
],
148+
'api_key' => 'sometimes|string',
141149
]);
142150

143151
// 保存到本地
@@ -148,24 +156,25 @@ public function audio(Request $request): JsonResponse
148156
$messages = $request->session()->get('messages', [
149157
['role' => 'system', 'content' => 'You are GeekChat - A ChatGPT clone. Answer as concisely as possible. Make Mandarin Chinese the primary language']
150158
]);
151-
152159
// $path = 'audios/2023/03/09/test.wav';(测试用)
153160
// 调用 speech to text API 将语音转化为文字
154161
try {
155-
$response = OpenAI::transcribe([
162+
$response = OpenAI::withToken($request->input('api_key'))->transcribe([
156163
'model' => 'whisper-1',
157164
'file' => curl_file_create(Storage::disk('local')->path($path)),
158165
'response_format' => 'verbose_json',
159166
]);
160167
} catch (Exception $exception) {
161-
return $this->toJsonResponse($request, SYSTEM_ERROR, $messages);
168+
return response()->json(['message' => ['role' => 'assistant', 'content' => SYSTEM_ERROR]]);
162169
} finally {
163170
Storage::disk('local')->delete($path); // 处理完毕删除音频文件
164171
}
165-
166172
$result = json_decode($response);
167173
if (empty($result->text)) {
168-
return $this->toJsonResponse($request, '对不起,我没有听清你说的话,请再试一次', $messages);
174+
if ($request->input('api_key')) {
175+
return response()->json(['message' => ['role' => 'assistant', 'content' => '识别语音失败,请确保你的KEY有效']]);
176+
}
177+
return response()->json(['message' => ['role' => 'assistant', 'content' => '对不起,我没有听清你说的话,请再试一次']]);
169178
}
170179

171180
// 接下来的流程和 ChatGPT 一样
@@ -180,20 +189,26 @@ public function audio(Request $request): JsonResponse
180189
public function image(Request $request): JsonResponse
181190
{
182191
$request->validate([
183-
'prompt' => 'required|string'
192+
'prompt' => 'required|string',
193+
'api_key' => 'sometimes|string',
184194
]);
185195
$messages = $request->session()->get('messages', [$this->preset]);
186196
$prompt = $request->input('prompt');
187197
$userMsg = ['role' => 'user', 'content' => $prompt];
188198
$messages[] = $userMsg;
189-
$response = OpenAI::image([
199+
$apiKey = $request->input('api_key');
200+
$size = '256x256';
201+
if (!empty($apiKey)) {
202+
$size = '1024x1024';
203+
}
204+
$response = OpenAI::withToken($apiKey)->image([
190205
"prompt" => $prompt,
191206
"n" => 1,
192-
"size" => "256x256",
207+
"size" => $size,
193208
"response_format" => "url",
194209
]);
195210
$result = json_decode($response);
196-
$image = '';
211+
$image = '画图失败,如果你设置了自己的key,请确保它是有效的';
197212
if (isset($result->data[0]->url)) {
198213
$image = '![](' . $result->data[0]->url . ')';
199214
}
@@ -212,4 +227,21 @@ public function reset(Request $request): JsonResponse
212227

213228
return response()->json(['status' => 'ok']);
214229
}
230+
231+
public function valid(Request $request): JsonResponse
232+
{
233+
$request->validate([
234+
'api_key' => 'required|string'
235+
]);
236+
$apiKey = $request->input('api_key');
237+
if (empty($apiKey)) {
238+
return response()->json(['valid' => false, 'error' => '无效的 API KEY']);
239+
}
240+
$response = Http::withToken($apiKey)->timeout(15)
241+
->get(config('openai.base_uri') . '/dashboard/billing/credit_grants');
242+
if ($response->failed()) {
243+
return response()->json(['valid' => false, 'error' => '无效的 API KEY']);
244+
}
245+
return response()->json(['valid' => true]);
246+
}
215247
}

app/Providers/AiServiceProvider.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
namespace App\Providers;
66

7+
use App\Client\OpenAI;
78
use Illuminate\Support\ServiceProvider;
89
use App\Exceptions\OpenAI\ApiKeyIsMissing;
9-
use Orhanerday\OpenAi\OpenAi;
1010

1111
/**
1212
* @internal
@@ -18,7 +18,7 @@ class AiServiceProvider extends ServiceProvider
1818
*/
1919
public function register(): void
2020
{
21-
$this->app->singleton(OpenAi::class, static function (): OpenAi {
21+
$this->app->singleton(OpenAI::class, static function (): OpenAI {
2222
$apiKey = config('openai.api_key');
2323
$baseUri = config('openai.base_uri');
2424
$httpProxy = config('openai.http_proxy');
@@ -28,7 +28,7 @@ public function register(): void
2828
throw ApiKeyIsMissing::create();
2929
}
3030

31-
$openAi = new OpenAi($apiKey);
31+
$openAi = new OpenAI($apiKey);
3232
$openAi->setTimeout(60); // 超时时间默认是60s
3333
if ($organization !== null) {
3434
$openAi->setORG($organization);

app/Providers/RouteServiceProvider.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,19 @@ protected function configureRateLimiting(): void
4545
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
4646
});
4747
RateLimiter::for('chat', function (Request $request) {
48-
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
48+
return $request->input('api_key') || $this->app->environment('development')
49+
? Limit::none()
50+
: Limit::perMinute(30)->by($request->user()?->id ?: $request->ip());
4951
});
5052
RateLimiter::for('audio', function (Request $request) {
51-
return Limit::perMinute(30)->by($request->user()?->id ?: $request->ip());
53+
return $request->input('api_key') || $this->app->environment('development')
54+
? Limit::none()
55+
: Limit::perMinute(15)->by($request->user()?->id ?: $request->ip());
5256
});
5357
RateLimiter::for('image', function (Request $request) {
54-
return Limit::perMinute(20)->by($request->user()?->id ?: $request->ip());
58+
return $request->input('api_key') || $this->app->environment('development')
59+
? Limit::none()
60+
: Limit::perHour(5)->by($request->user()?->id ?: $request->ip());
5561
});
5662
}
5763
}

resources/js/Components/AudioWidget.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ export default {
3737
computed: {
3838
buttonClass() {
3939
if (this.isTyping) {
40-
return "flex items-center justify-center px-4 py-2 border border-green-600 bg-green-500 hover:bg-green-600 text-white rounded-md text-sm md:text-base cursor-pointer opacity-25";
40+
return "flex items-center justify-center px-4 py-2 border border-blue-600 bg-blue-500 hover:bg-blue-600 text-white rounded-md text-sm md:text-base cursor-pointer opacity-25";
4141
} else {
42-
return "flex items-center justify-center px-4 py-2 border border-green-600 bg-green-500 hover:bg-green-600 text-white rounded-md text-sm md:text-base cursor-pointer";
42+
return "flex items-center justify-center px-4 py-2 border border-blue-600 bg-blue-500 hover:bg-blue-600 text-white rounded-md text-sm md:text-base cursor-pointer";
4343
}
4444
}
4545
},

resources/js/Pages/Chat.vue

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ onMounted(() => {
1212
1313
const messages = computed(() => store.state.messages.filter(message => message != undefined))
1414
const isTyping = computed(() => store.state.isTyping)
15+
const apiKey = computed(() => store.state.apiKey ? '*'.repeat(4) + store.state.apiKey.substr(length - 4, 4) : '')
1516
1617
const form = useForm({
17-
prompt: null,
18+
prompt: null
1819
})
1920
2021
const chat = () => {
@@ -68,6 +69,11 @@ const translate = () => {
6869
form.reset()
6970
}
7071
72+
const enterApiKey = () => {
73+
const api_key = prompt("输入你的 OpenAI API Key(使用自己的 Key 可以专享独立的调用通道,不受共享通道频率影响,并且可以绘制更精准的大尺寸图片):");
74+
store.dispatch('validAndSetApiKey', api_key)
75+
}
76+
7177
</script>
7278

7379
<template>
@@ -81,13 +87,35 @@ const translate = () => {
8187
src="https://image.gstatics.cn/icon/geekchat.png" alt="GeekChat"
8288
class="rounded-lg w-12 h-12">
8389
<div class="font-semibold text-4xl sm:text-5xl">Geek<span class="text-blue-500">Chat</span>
84-
</div><span
85-
class="bg-gradient-to-r from-purple-400 to-pink-500 px-3 py-1 text-xs font-semibold text-white text-center rounded-full inline-block ">Beta</span>
90+
</div>
8691
</div>
8792
<div class="text-center my-4 font-light text-base sm:text-xl my-2 sm:my-5">支持文字、语音、翻译、画图的聊天机器人
8893
</div>
8994
</div>
9095
</div>
96+
<div class="mt-5">
97+
<div class="text-sm text-center">
98+
<div>
99+
<button v-if="apiKey" @click="enterApiKey"
100+
class="text-blue-500 hover:underline font-semibold inline-flex space-x-2 disabled:text-gray-500"><svg
101+
stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512"
102+
class="w-5 h-5" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
103+
<path
104+
d="M218.1 167.17c0 13 0 25.6 4.1 37.4-43.1 50.6-156.9 184.3-167.5 194.5a20.17 20.17 0 00-6.7 15c0 8.5 5.2 16.7 9.6 21.3 6.6 6.9 34.8 33 40 28 15.4-15 18.5-19 24.8-25.2 9.5-9.3-1-28.3 2.3-36s6.8-9.2 12.5-10.4 15.8 2.9 23.7 3c8.3.1 12.8-3.4 19-9.2 5-4.6 8.6-8.9 8.7-15.6.2-9-12.8-20.9-3.1-30.4s23.7 6.2 34 5 22.8-15.5 24.1-21.6-11.7-21.8-9.7-30.7c.7-3 6.8-10 11.4-11s25 6.9 29.6 5.9c5.6-1.2 12.1-7.1 17.4-10.4 15.5 6.7 29.6 9.4 47.7 9.4 68.5 0 124-53.4 124-119.2S408.5 48 340 48s-121.9 53.37-121.9 119.17zM400 144a32 32 0 11-32-32 32 32 0 0132 32z">
105+
</path>
106+
</svg><span>修改 API Key ({{ apiKey }})</span></button>
107+
<button v-else @click="enterApiKey"
108+
class="inline-flex items-center justify-center rounded-full px-4 py-3 text-sm shadow-md bg-blue-300 text-white hover:bg-blue-500 transition-all active:bg-blue-600 group font-semibold text-sm disabled:bg-gray-400 space-x-2">
109+
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512"
110+
class="w-5 h-5" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
111+
<path
112+
d="M218.1 167.17c0 13 0 25.6 4.1 37.4-43.1 50.6-156.9 184.3-167.5 194.5a20.17 20.17 0 00-6.7 15c0 8.5 5.2 16.7 9.6 21.3 6.6 6.9 34.8 33 40 28 15.4-15 18.5-19 24.8-25.2 9.5-9.3-1-28.3 2.3-36s6.8-9.2 12.5-10.4 15.8 2.9 23.7 3c8.3.1 12.8-3.4 19-9.2 5-4.6 8.6-8.9 8.7-15.6.2-9-12.8-20.9-3.1-30.4s23.7 6.2 34 5 22.8-15.5 24.1-21.6-11.7-21.8-9.7-30.7c.7-3 6.8-10 11.4-11s25 6.9 29.6 5.9c5.6-1.2 12.1-7.1 17.4-10.4 15.5 6.7 29.6 9.4 47.7 9.4 68.5 0 124-53.4 124-119.2S408.5 48 340 48s-121.9 53.37-121.9 119.17zM400 144a32 32 0 11-32-32 32 32 0 0132 32z">
113+
</path>
114+
</svg><span>输入 API Key(可选)</span>
115+
</button>
116+
</div>
117+
</div>
118+
</div>
91119
<div class="px-4 rounded-lg mb-4" v-for="(message, index) in messages" :key="index">
92120
<div v-if="message && message.role == 'user'"
93121
class="pl-12 relative response-block scroll-mt-32 min-h-[40px]">
@@ -138,7 +166,7 @@ const translate = () => {
138166
class="block w-full rounded-md border-0 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:py-1.5 sm:text-sm sm:leading-6 resize-y"></textarea>
139167
<div class="flex space-x-2">
140168
<button
141-
:class="{ 'flex items-center justify-center px-4 py-2 border border-green-600 bg-green-500 hover:bg-green-600 text-white rounded-md text-sm md:text-base': true, 'opacity-25': isTyping }"
169+
:class="{ 'flex items-center justify-center px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md text-sm md:text-base': true, 'opacity-25': isTyping }"
142170
title="发送消息" type="submit" :disabled="isTyping">
143171
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
144172
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
@@ -148,7 +176,7 @@ const translate = () => {
148176
</button>
149177
<audio-widget @audio-upload="audio" @audio-failed="audioFailed" :is-typing="isTyping" />
150178
<button
151-
:class="{ 'flex items-center justify-center px-4 py-2 border border-green-600 bg-green-500 hover:bg-green-600 text-white rounded-md text-sm md:text-base': true, 'opacity-25': isTyping }"
179+
:class="{ 'flex items-center justify-center px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md text-sm md:text-base': true, 'opacity-25': isTyping }"
152180
@click="translate" title="中英互译" type="button" :disabled="isTyping">
153181
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
154182
width="24" height="24" viewBox="0 0 24 24">
@@ -161,7 +189,7 @@ const translate = () => {
161189
</svg>
162190
</button>
163191
<button
164-
:class="{ 'flex items-center justify-center px-4 py-2 border border-green-600 bg-green-500 hover:bg-green-600 text-white rounded-md text-sm md:text-base': true, 'opacity-25': isTyping }"
192+
:class="{ 'flex items-center justify-center px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-md text-sm md:text-base': true, 'opacity-25': isTyping }"
165193
@click="image" title="AI绘图" type="button" :disabled="isTyping">
166194
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
167195
width="24" height="24" viewBox="0 0 24 24">
@@ -171,7 +199,7 @@ const translate = () => {
171199
</svg>
172200
</button>
173201
<button
174-
:class="{ 'flex items-center justify-center px-4 py-2 border border-gray-500 bg-gray-400 hover:bg-gray-500 text-white rounded-md text-sm md:text-base': true, 'opacity-25': isTyping }"
202+
:class="{ 'flex items-center justify-center px-4 py-2 bg-gray-400 hover:bg-gray-500 text-white rounded-md text-sm md:text-base': true, 'opacity-25': isTyping }"
175203
@click="reset" title="清空消息" type="button" :disabled="isTyping">
176204
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
177205
stroke-width="1.5" stroke="currentColor" class="w-6 h-6">

0 commit comments

Comments
 (0)