From 8ab99143b964d0df0fbe4b0b126526a62e5f05e7 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Mon, 13 Mar 2023 16:30:58 +0800 Subject: [PATCH] Support own OpenAI API Key Support own OpenAI API Key fixed openai check public cache permision when install check public cache permision when install Fixed user id Fixed user id Fixed api wip wip API Key wip fixed openai --- beike/Admin/View/Components/Form/Input.php | 5 +- beike/Installer/config.php | 10 +- beike/Repositories/PluginRepo.php | 38 ++++- .../Openai/Controllers/OpenaiController.php | 64 ++++++- plugins/Openai/Lang/en/common.php | 4 + plugins/Openai/Lang/zh_cn/common.php | 4 + plugins/Openai/Lang/zh_hk/common.php | 4 + plugins/Openai/Libraries/OpenAI/Base.php | 161 ++++++++++++++++++ plugins/Openai/Libraries/OpenAI/Chat.php | 66 +++++++ .../Openai/Libraries/OpenAI/Completion.php | 33 ++++ .../2023_02_27_173221_add_openai_logs.php | 36 ++++ plugins/Openai/Models/OpenaiLog.php | 32 ++++ plugins/Openai/Routes/admin.php | 3 + plugins/Openai/Services/OpenAIService.php | 120 +++++++++++++ plugins/Openai/Views/admin/openai.blade.php | 38 +++-- plugins/Openai/columns.php | 31 ++++ .../views/components/form/input.blade.php | 4 + .../admin/views/pages/plugins/form.blade.php | 1 + 18 files changed, 630 insertions(+), 24 deletions(-) create mode 100644 plugins/Openai/Libraries/OpenAI/Base.php create mode 100644 plugins/Openai/Libraries/OpenAI/Chat.php create mode 100644 plugins/Openai/Libraries/OpenAI/Completion.php create mode 100644 plugins/Openai/Migrations/2023_02_27_173221_add_openai_logs.php create mode 100644 plugins/Openai/Models/OpenaiLog.php create mode 100644 plugins/Openai/Services/OpenAIService.php create mode 100644 plugins/Openai/columns.php diff --git a/beike/Admin/View/Components/Form/Input.php b/beike/Admin/View/Components/Form/Input.php index 09a0c69d..c9f4c3dd 100644 --- a/beike/Admin/View/Components/Form/Input.php +++ b/beike/Admin/View/Components/Form/Input.php @@ -18,13 +18,15 @@ class Input extends Component public string $placeholder; + public string $description; + public string $type; public string $step; public bool $required; - public function __construct(string $name, string $title, string $value, bool $required = false, string $error = '', string $width = '400', string $type = 'text', string $step = '', string $placeholder = '') + public function __construct(string $name, string $title, string $value, bool $required = false, string $error = '', string $width = '400', string $type = 'text', string $step = '', string $placeholder = '', string $description = '') { $this->name = $name; $this->title = $title; @@ -35,6 +37,7 @@ class Input extends Component $this->type = $type; $this->step = $step; $this->required = $required; + $this->description = $description; } public function render() diff --git a/beike/Installer/config.php b/beike/Installer/config.php index 62086026..26c461d9 100644 --- a/beike/Installer/config.php +++ b/beike/Installer/config.php @@ -50,10 +50,12 @@ return [ | */ 'permissions' => [ - 'storage/framework/' => '755', - 'storage/logs/' => '755', - 'bootstrap/cache/' => '755', - '.env' => '755', + '.env' => '755', + 'bootstrap/cache/' => '755', + 'public/cache/' => '755', + 'public/plugin/' => '755', + 'storage/framework/' => '755', + 'storage/logs/' => '755', ], /* diff --git a/beike/Repositories/PluginRepo.php b/beike/Repositories/PluginRepo.php index 6447c4db..0393ce50 100644 --- a/beike/Repositories/PluginRepo.php +++ b/beike/Repositories/PluginRepo.php @@ -79,9 +79,17 @@ class PluginRepo { $migrationPath = "{$bPlugin->getPath()}/Migrations"; if (is_dir($migrationPath)) { - Artisan::call('migrate', [ - '--force' => true, - ]); + $files = glob($migrationPath . '/*'); + asort($files); + + foreach ($files as $file) { + $file = str_replace(base_path(), '', $file); + Artisan::call('migrate', [ + '--force' => true, + '--step' => 1, + '--path' => $file, + ]); + } } } @@ -91,7 +99,8 @@ class PluginRepo */ public static function uninstallPlugin(BPlugin $bPlugin) { - self::removeStaticFiles($bPlugin); + // self::removeStaticFiles($bPlugin); + self::rollbackDatabase($bPlugin); $type = $bPlugin->type; $code = $bPlugin->code; Plugin::query() @@ -114,6 +123,27 @@ class PluginRepo } } + /** + * 数据库回滚 + */ + public static function rollbackDatabase(BPlugin $bPlugin) + { + $migrationPath = "{$bPlugin->getPath()}/Migrations"; + if (is_dir($migrationPath)) { + $files = glob($migrationPath . '/*'); + arsort($files); + + foreach ($files as $file) { + $file = str_replace(base_path(), '', $file); + Artisan::call('migrate:rollback', [ + '--force' => true, + '--step' => 1, + '--path' => $file, + ]); + } + } + } + /** * Get plugin by code * diff --git a/plugins/Openai/Controllers/OpenaiController.php b/plugins/Openai/Controllers/OpenaiController.php index f0dd30ea..32be9c0b 100644 --- a/plugins/Openai/Controllers/OpenaiController.php +++ b/plugins/Openai/Controllers/OpenaiController.php @@ -12,17 +12,79 @@ namespace Plugin\Openai\Controllers; use Beike\Admin\Http\Controllers\Controller; +use Illuminate\Http\Request; +use Plugin\Openai\Services\OpenAIService; class OpenaiController extends Controller { + /** + * OpenAI home page. + * + * @return mixed + */ public function index() { $plugin = app('plugin')->getPlugin('openai'); - $data = [ + + $error = ''; + $baseUrl = config('beike.api_url') . '/api/openai'; + $apiType = plugin_setting('openai.api_type'); + if ($apiType == 'own') { + $apiKey = plugin_setting('openai.api_key'); + if (empty($apiKey)) { + $error = trans('Openai::common.empty_api_key'); + } + $baseUrl = config('app.url') . '/admin/openai'; + } + + $data = [ 'name' => $plugin->getLocaleName(), 'description' => $plugin->getLocaleDescription(), + 'type' => $apiType, + 'base' => $baseUrl, + 'error' => $error, ]; return view('Openai::admin.openai', $data); } + + /** + * Send chat completions with OpenAI API + * + * @param Request $request + * @return array|mixed + * @throws \Throwable + */ + public function completions(Request $request) + { + try { + $result = (new OpenAIService())->requestAI($request->all()); + } catch (\Exception $e) { + $result = [ + 'error' => $e->getMessage(), + ]; + } + + return $result; + } + + /** + * Get histories + * + * @param Request $request + * @return array|mixed + */ + public function histories(Request $request) + { + try { + $perPage = $request->get('per_page', 10); + $result = (new OpenAIService())->getOpenaiLogs($perPage); + } catch (\Exception $e) { + $result = [ + 'error' => $e->getMessage(), + ]; + } + + return $result; + } } diff --git a/plugins/Openai/Lang/en/common.php b/plugins/Openai/Lang/en/common.php index d98aef08..efdcdf17 100644 --- a/plugins/Openai/Lang/en/common.php +++ b/plugins/Openai/Lang/en/common.php @@ -20,4 +20,8 @@ return [ 'qa_q' => 'ask', 'qa_a' => 'answer', 'number_free' => 'The remaining free times of the day', + 'api_type' => 'API Method', + 'own' => 'Own Key', + 'beikeshop' => 'BeikeShop', + 'empty_api_key' => 'API Key is empty, please go to the plugin settings - OpenAI - Edit and fill in the API Key first.', ]; diff --git a/plugins/Openai/Lang/zh_cn/common.php b/plugins/Openai/Lang/zh_cn/common.php index 4af3cf42..7531ee85 100644 --- a/plugins/Openai/Lang/zh_cn/common.php +++ b/plugins/Openai/Lang/zh_cn/common.php @@ -20,4 +20,8 @@ return [ 'qa_q' => '问', 'qa_a' => '答', 'number_free' => '当日剩余免费次数', + 'api_type' => 'API 方式', + 'own' => '自有Key', + 'beikeshop' => 'BeikeShop平台', + 'empty_api_key' => 'API Key 为空, 请先到插件设置 - OpenAI - 编辑 填写API Key', ]; diff --git a/plugins/Openai/Lang/zh_hk/common.php b/plugins/Openai/Lang/zh_hk/common.php index ebeb3b0a..cee4b66b 100644 --- a/plugins/Openai/Lang/zh_hk/common.php +++ b/plugins/Openai/Lang/zh_hk/common.php @@ -20,4 +20,8 @@ return [ 'qa_q' => '問', 'qa_a' => '答', 'number_free' => '當日剩餘免費次數', + 'api_type' => 'API 方式', + 'own' => '自有 Key', + 'beikeshop' => 'BeikeShop 平台', + 'empty_api_key' => 'API Key 為空,請先到插件設置 - OpenAI - 編輯 填寫 API Key', ]; diff --git a/plugins/Openai/Libraries/OpenAI/Base.php b/plugins/Openai/Libraries/OpenAI/Base.php new file mode 100644 index 00000000..43a8cbe6 --- /dev/null +++ b/plugins/Openai/Libraries/OpenAI/Base.php @@ -0,0 +1,161 @@ + + * @created 2023-02-22 20:31:42 + * @modified 2023-02-22 20:31:42 + */ + +namespace Plugin\Openai\Libraries\OpenAI; + +use Exception; + +class Base +{ + /** + * @var string|bool|mixed + */ + private string $apiKey = ''; + + /** + * @var int + */ + protected int $maxTokens = 1000; + + /** + * @var float + */ + protected float $temperature = 0.5; + + /** + * @var int + */ + protected int $number = 1; + + /** + * @var string + */ + protected string $prompt; + + /** + * OpenAI constructor. + * @param string|null $apiKey + */ + public function __construct(?string $apiKey = '') + { + if ($apiKey) { + $this->apiKey = $apiKey; + } + if (empty($this->apiKey)) { + $this->apiKey = env('OPENAI_API_KEY'); + } + } + + /** + * Get OpenAI instance. + * + * @param string|null $apiKey + * @return Base + */ + public static function getInstance(?string $apiKey = ''): static + { + return new self($apiKey); + } + + /** + * 设置 max_tokens的值 + * 一般来说,max_tokens值越大,模型的表现就越好, + * 但是需要考虑到计算资源的限制,max_tokens值不宜过大。 + * 一般来说,max_tokens值可以设置在比较合理的范围内,比如500到1000之间。 + * + * @param int $maxTokens + * @return $this + */ + public function setMaxTokens(int $maxTokens): static + { + $this->maxTokens = $maxTokens; + + return $this; + } + + /** + * 设置temperature参数值, 参数的范围是0.0到2.0之间。 + * 在较低的温度下,模型会生成更加安全的文本, + * 而在较高的温度下,模型会生成更加创新的文本。 + * + * @param float $temperature + * @return $this + */ + public function setTemperature(float $temperature): static + { + $this->temperature = $temperature; + + return $this; + } + + /** + * 设置 n 参数值 + * 指定了返回结果的数量。n参数越大,返回的结果数量也越多。 + * + * @param int $number + * @return $this + */ + public function setNumber(int $number): static + { + $this->number = $number; + + return $this; + } + + /** + * 设置 prompt 参数值 + * 用于指定一段文本,用于提供给模型参考,以便于生成更加相关的文本 + * + * @param string $prompt + * @return $this + * @throws Exception + */ + public function setPrompt(string $prompt): static + { + $this->prompt = trim($prompt); + if (empty($this->prompt)) { + throw new Exception('prompt 不能为空!'); + } + + return $this; + } + + /** + * 发送请求到 OpenAI + * + * @param $url + * @param $data + * @return mixed + * @throws Exception + */ + protected function request($url, $data): mixed + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $this->apiKey, + ]); + + $response = curl_exec($ch); + if ($response === false) { + throw new \Exception(curl_error($ch)); + } + curl_close($ch); + + return json_decode($response, true); + } +} diff --git a/plugins/Openai/Libraries/OpenAI/Chat.php b/plugins/Openai/Libraries/OpenAI/Chat.php new file mode 100644 index 00000000..336af1f2 --- /dev/null +++ b/plugins/Openai/Libraries/OpenAI/Chat.php @@ -0,0 +1,66 @@ + + * @created 2023-03-02 14:30:10 + * @modified 2023-03-02 14:30:10 + */ + +namespace Plugin\Openai\Libraries\OpenAI; + +class Chat extends Base +{ + /** + * @var array 聊天上下文 + */ + private array $messages; + + /** + * @param string|null $apiKey + * @return static + */ + public static function getInstance(?string $apiKey = ''): static + { + return new self($apiKey); + } + + /** + * https://platform.openai.com/docs/guides/chat/introduction + * + * @param $messages + * @param $prompt + * @return Chat + */ + public function setMessages($messages, $prompt): self + { + $messages[] = ['role' => 'user', 'content' => $prompt]; + $this->messages = $messages; + + return $this; + } + + /** + * 发送请求到 OpenAI + * + * @return mixed + * @throws \Exception + */ + public function create(): mixed + { + $model = 'gpt-3.5-turbo'; + $url = 'https://api.openai.com/v1/chat/completions'; + $data = [ + 'messages' => $this->messages, + 'max_tokens' => $this->maxTokens, + 'temperature' => $this->temperature, + 'n' => $this->number, + 'stop' => '\n', + 'model' => $model, + ]; + + return $this->request($url, $data); + } +} diff --git a/plugins/Openai/Libraries/OpenAI/Completion.php b/plugins/Openai/Libraries/OpenAI/Completion.php new file mode 100644 index 00000000..a30f822a --- /dev/null +++ b/plugins/Openai/Libraries/OpenAI/Completion.php @@ -0,0 +1,33 @@ + + * @created 2023-03-02 14:37:15 + * @modified 2023-03-02 14:37:15 + */ + +namespace Plugin\Openai\Libraries\OpenAI; + +class Completion extends Base +{ + /** + * @throws \Exception + */ + public function create() + { + $model = 'text-davinci-003'; + $url = 'https://api.openai.com/v1/completions'; + $data = [ + 'prompt' => $this->prompt, + 'max_tokens' => $this->maxTokens, + 'temperature' => $this->temperature, + 'n' => $this->number, + 'stop' => '\n', + 'model' => $model, + ]; + $this->request($url, $data); + } +} diff --git a/plugins/Openai/Migrations/2023_02_27_173221_add_openai_logs.php b/plugins/Openai/Migrations/2023_02_27_173221_add_openai_logs.php new file mode 100644 index 00000000..5b6f706a --- /dev/null +++ b/plugins/Openai/Migrations/2023_02_27_173221_add_openai_logs.php @@ -0,0 +1,36 @@ +id(); + $table->integer('user_id')->index('user_id'); + $table->text('question'); + $table->text('answer'); + $table->string('request_ip'); + $table->text('user_agent'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('openai_logs'); + } +}; diff --git a/plugins/Openai/Models/OpenaiLog.php b/plugins/Openai/Models/OpenaiLog.php new file mode 100644 index 00000000..e6b8114f --- /dev/null +++ b/plugins/Openai/Models/OpenaiLog.php @@ -0,0 +1,32 @@ + + * @created 2023-03-13 16:48:17 + * @modified 2023-03-13 16:48:17 + */ + +namespace Plugin\Openai\Models; + +use Illuminate\Database\Eloquent\Model; + +class OpenaiLog extends Model +{ + public $timestamps = true; + + protected $table = 'openai_logs'; + + protected $fillable = [ + 'user_id', 'question', 'answer', 'request_ip', 'user_agent', + ]; + + protected $appends = ['created_format']; + + public function getCreatedFormatAttribute() + { + return $this->created_at->format('Y-m-d H:i:s'); + } +} diff --git a/plugins/Openai/Routes/admin.php b/plugins/Openai/Routes/admin.php index f6b3d62f..184a3ed2 100644 --- a/plugins/Openai/Routes/admin.php +++ b/plugins/Openai/Routes/admin.php @@ -13,3 +13,6 @@ use Illuminate\Support\Facades\Route; use Plugin\Openai\Controllers\OpenaiController; Route::middleware('can:openai_index')->get('/openai', [OpenaiController::class, 'index'])->name('openai.index'); + +Route::middleware('can:openai_index')->get('/openai/histories', [OpenaiController::class, 'histories'])->name('openai.histories'); +Route::middleware('can:openai_index')->post('/openai/completions', [OpenaiController::class, 'completions'])->name('openai.completions'); diff --git a/plugins/Openai/Services/OpenAIService.php b/plugins/Openai/Services/OpenAIService.php new file mode 100644 index 00000000..8eff9252 --- /dev/null +++ b/plugins/Openai/Services/OpenAIService.php @@ -0,0 +1,120 @@ + + * @created 2023-03-13 16:42:52 + * @modified 2023-03-13 16:42:52 + */ + +namespace Plugin\Openai\Services; + +use Plugin\Openai\Libraries\OpenAI\Chat; +use Plugin\Openai\Models\OpenaiLog; + +class OpenAIService +{ + /** + * 发起 OpenAI 请求 + * + * @param $data + * @return mixed + * @throws \Throwable + */ + public function requestAI($data) + { + $prompt = $data['prompt'] ?? ''; + $apiKey = plugin_setting('openai.api_key'); + + $openAI = Chat::getInstance($apiKey); + $messages = $this->getChatMessages(); + $response = $openAI->setMessages($messages, $prompt)->create(); + + $result['prompt'] = $prompt; + + $error = trim($response['error']['message'] ?? ''); + if ($error) { + $result['error'] = $error; + } else { + $content = trim($response['choices'][0]['message']['content'] ?? ''); + + $response['choices'][0]['text'] = $content; + + $result['response'] = $response; + $newLog = $this->createOpenaiLog($prompt, $content); + $result['created_format'] = $newLog->created_format; + } + + return $result; + } + + /** + * @param $question + * @param $answer + * @return OpenaiLog + * @throws \Throwable + */ + private function createOpenaiLog($question, $answer): OpenaiLog + { + $user = current_user(); + $newOpenaiLog = new OpenaiLog([ + 'user_id' => $user->id ?? 0, + 'question' => trim($question), + 'answer' => trim($answer), + 'request_ip' => request()->getClientIp(), + 'user_agent' => request()->userAgent(), + ]); + $newOpenaiLog->saveOrFail(); + + return $newOpenaiLog; + } + + /** + * 获取聊天记录 + * + * @param int $perPage + * @return mixed + */ + public function getOpenaiLogs(int $perPage = 10) + { + $user = current_user(); + + return OpenaiLog::query() + ->select(['user_id', 'question', 'answer', 'created_at']) + ->where('user_id', $user->id) + ->orderByDesc('created_at') + ->paginate($perPage); + } + + /** + * https://platform.openai.com/docs/guides/chat/introduction + * + * messages=[ + * {"role": "system", "content": "You are a helpful assistant."}, + * {"role": "user", "content": "Who won the world series in 2020?"}, + * {"role": "assistant", "content": "The Los Angeles Dodgers won the World Series in 2020."}, + * {"role": "user", "content": "Where was it played?"} + * ] + * + * @return array + */ + public function getChatMessages() + { + $logs = OpenaiLog::query() + ->select(['user_id', 'question', 'answer', 'created_at']) + ->limit(5) + ->get(); + + $messages[] = [ + 'role' => 'system', 'content' => 'You are a helpful assistant.', + ]; + foreach ($logs as $log) { + $messages[] = ['role' => 'user', 'content' => $log->question]; + $messages[] = ['role' => 'assistant', 'content' => $log->answer]; + } + + return $messages; + } +} diff --git a/plugins/Openai/Views/admin/openai.blade.php b/plugins/Openai/Views/admin/openai.blade.php index 3c9b6a82..17480c6c 100644 --- a/plugins/Openai/Views/admin/openai.blade.php +++ b/plugins/Openai/Views/admin/openai.blade.php @@ -9,24 +9,32 @@ @endpush @section('content') -
-
-
-
{{ __('Openai::common.no_question') }}
-
- +
+
+
+
{{ __('Openai::common.no_question') }}
+
-
-
{{ __('Openai::common.number_free') }}: - {{ __('Openai::common.loading') }}
+ @if ($type != 'own') +
{{ __('Openai::common.number_free') }}: + {{ __('Openai::common.loading') }} +
+ @endif + @if ($error) +
+ + {{ $error }} +
+ @endif
{{ $description }}
@@ -71,7 +79,9 @@ $('#answer').height($(window).height() - 260); $(document).ready(function() { - loadQuantities(); + @if ($type != 'own') + loadQuantities(); + @endif loadHistories(1 , function() { // 获取 answer .answer-list 内容高度 let height = 0; @@ -105,7 +115,7 @@ let html = ''; $.ajax({ - url: `${config.api_url}/api/openai/completions`, + url: `{{ $base }}/completions`, type: 'POST', headers: { 'token': '{{ system_setting('base.developer_token') ?? '' }}' @@ -149,7 +159,7 @@ function loadQuantities() { $.ajax({ - url: `${config.api_url}/api/openai/quantities?domain=${config.app_url}`, + url: `{{ $base }}/quantities?domain=${config.app_url}`, headers: { 'token': '{{ system_setting('base.developer_token') ?? '' }}' }, @@ -161,7 +171,7 @@ function loadHistories(page = 1, callback = null) { $.ajax({ - url: `${config.api_url}/api/openai/histories?domain=${config.app_url}&page=${page}`, + url: `{{ $base }}/histories?domain=${config.app_url}&page=${page}`, headers: { 'token': '{{ system_setting('base.developer_token') ?? '' }}' }, diff --git a/plugins/Openai/columns.php b/plugins/Openai/columns.php new file mode 100644 index 00000000..6f076006 --- /dev/null +++ b/plugins/Openai/columns.php @@ -0,0 +1,31 @@ + + * @created 2023-03-13 16:08:41 + * @modified 2023-03-13 16:08:41 + */ + +return [ + [ + 'name' => 'api_type', + 'label_key' => 'common.api_type', + 'type' => 'select', + 'options' => [ + ['value' => 'own', 'label_key' => 'common.own'], + ['value' => 'beikeshop', 'label_key' => 'common.beikeshop'], + ], + 'required' => true, + 'description' => '如果选择 BeikeShop 平台, 则 API Key 可以留空', + ], + [ + 'name' => 'api_key', + 'label' => 'API Key', + 'type' => 'string', + 'required' => false, + 'description' => '获取 API Key', + ], +]; diff --git a/resources/beike/admin/views/components/form/input.blade.php b/resources/beike/admin/views/components/form/input.blade.php index eddb6322..3b27ef3f 100644 --- a/resources/beike/admin/views/components/form/input.blade.php +++ b/resources/beike/admin/views/components/form/input.blade.php @@ -2,6 +2,10 @@ + @if ($description) +
{!! $description !!}
+ @endif + @if ($error) {{ $error }} diff --git a/resources/beike/admin/views/pages/plugins/form.blade.php b/resources/beike/admin/views/pages/plugins/form.blade.php index d6dcf461..34f7d931 100644 --- a/resources/beike/admin/views/pages/plugins/form.blade.php +++ b/resources/beike/admin/views/pages/plugins/form.blade.php @@ -22,6 +22,7 @@ :name="$column['name']" :title="$column['label']" :placeholder="$column['placeholder'] ?? ''" + :description="$column['description'] ?? ''" :error="$errors->first($column['name'])" :required="$column['required'] ? true : false" :value="old($column['name'], $column['value'] ?? '')" />