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') -