feat: add Integration Credentials CRUD and Deploy UI
Integration Credentials: - Add IntegrationCredential model with encrypted payload - Add full CRUD: ManageIntegrationCredentialsAction, 4 Tasks, Controller, Requests - Add migration for integration_credentials table - Add Vue pages: Index, Create, Edit - AI tasks now read API keys from DB instead of .env - Add sidebar menu item Deploy UI: - Add DeployTask with full deploy lifecycle management - Extend DeployController with index, log, history endpoints - Add Deploy/Index.vue with live polling and deploy log viewer - Add sidebar menu item and QuickAction for Sveden UI: - Add 'key' icon to SidebarNavItem - Change deploy button color to amber in Main.vue
This commit is contained in:
+47
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Actions\IntegrationCredentials;
|
||||
|
||||
use App\Containers\Dashboard\Models\IntegrationCredential;
|
||||
use App\Containers\Dashboard\Tasks\IntegrationCredentials\DeleteIntegrationCredentialTask;
|
||||
use App\Containers\Dashboard\Tasks\IntegrationCredentials\GetIntegrationCredentialTask;
|
||||
use App\Containers\Dashboard\Tasks\IntegrationCredentials\ListIntegrationCredentialsTask;
|
||||
use App\Containers\Dashboard\Tasks\IntegrationCredentials\SaveIntegrationCredentialTask;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class ManageIntegrationCredentialsAction
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ListIntegrationCredentialsTask $listTask,
|
||||
private readonly SaveIntegrationCredentialTask $saveTask,
|
||||
private readonly DeleteIntegrationCredentialTask $deleteTask,
|
||||
) {}
|
||||
|
||||
public function list(): Collection
|
||||
{
|
||||
return $this->listTask->run();
|
||||
}
|
||||
|
||||
public function create(array $data): IntegrationCredential
|
||||
{
|
||||
return $this->saveTask->run(
|
||||
$data['provider'],
|
||||
$data['payload'],
|
||||
$data['is_active'] ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
public function update(IntegrationCredential $credential, array $data): IntegrationCredential
|
||||
{
|
||||
return $this->saveTask->run(
|
||||
$data['provider'],
|
||||
$data['payload'],
|
||||
$data['is_active'] ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
public function delete(IntegrationCredential $credential): bool
|
||||
{
|
||||
return $this->deleteTask->run($credential);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class IntegrationCredential extends Model
|
||||
{
|
||||
protected $guarded = false;
|
||||
|
||||
protected $casts = [
|
||||
'payload' => 'encrypted:array',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
@@ -2,11 +2,16 @@
|
||||
|
||||
namespace App\Containers\Dashboard\Tasks\AI;
|
||||
|
||||
use App\Containers\Dashboard\Tasks\IntegrationCredentials\GetIntegrationCredentialTask;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CallAiServiceForFileSelectionTask
|
||||
{
|
||||
public function __construct(
|
||||
private readonly GetIntegrationCredentialTask $getCredentialTask,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Отправляет фрагменты файлов в AI для определения основного файла
|
||||
*
|
||||
@@ -19,6 +24,15 @@ class CallAiServiceForFileSelectionTask
|
||||
'fragments_count' => count($fragments),
|
||||
]);
|
||||
|
||||
$credential = $this->getCredentialTask->run('qwen');
|
||||
if (!$credential) {
|
||||
Log::error('[CallAiServiceForFileSelectionTask] API ключ не настроен', [
|
||||
'provider' => 'qwen',
|
||||
]);
|
||||
return null;
|
||||
}
|
||||
$apiKey = $credential->payload['api_key'] ?? '';
|
||||
|
||||
$prompt = $this->buildPrompt($fragments);
|
||||
|
||||
Log::info('[CallAiServiceForFileSelectionTask] Отправка запроса к AI');
|
||||
@@ -26,7 +40,7 @@ class CallAiServiceForFileSelectionTask
|
||||
try {
|
||||
$response = Http::timeout(30) // Таймаут 30 секунд
|
||||
->withHeaders([
|
||||
'Authorization' => 'Bearer ' . env('QWEN_API_KEY'),
|
||||
'Authorization' => 'Bearer ' . $apiKey,
|
||||
'Content-Type' => 'application/json',
|
||||
])->post('https://routerai.ru/api/v1/chat/completions', [
|
||||
'model' => 'qwen/qwen3.5-flash-02-23',
|
||||
|
||||
@@ -2,12 +2,17 @@
|
||||
|
||||
namespace App\Containers\Dashboard\Tasks\AI;
|
||||
|
||||
use App\Containers\Dashboard\Tasks\IntegrationCredentials\GetIntegrationCredentialTask;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class CallAiServiceTask
|
||||
{
|
||||
public function __construct(
|
||||
private readonly GetIntegrationCredentialTask $getCredentialTask,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Отправляет текст в AI сервис для извлечения структурированных данных
|
||||
*
|
||||
@@ -23,14 +28,14 @@ class CallAiServiceTask
|
||||
'categories_count' => $categories->count(),
|
||||
]);
|
||||
|
||||
// Проверяем наличие API ключа
|
||||
$apiKey = env('QWEN_API_KEY');
|
||||
if (empty($apiKey)) {
|
||||
$credential = $this->getCredentialTask->run('qwen');
|
||||
if (!$credential) {
|
||||
Log::error('[CallAiServiceTask] API ключ не настроен', [
|
||||
'env_key' => 'QWEN_API_KEY',
|
||||
'provider' => 'qwen',
|
||||
]);
|
||||
throw new \RuntimeException('AI API ключ не настроен в окружении');
|
||||
throw new \RuntimeException('AI API ключ не настроен');
|
||||
}
|
||||
$apiKey = $credential->payload['api_key'] ?? '';
|
||||
|
||||
$categoryList = collect($categories)
|
||||
->map(fn($cat) => "{$cat->id}: {$cat->title}")
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Tasks\IntegrationCredentials;
|
||||
|
||||
use App\Containers\Dashboard\Models\IntegrationCredential;
|
||||
use App\Ship\Enums\CacheKeys;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class DeleteIntegrationCredentialTask
|
||||
{
|
||||
public function run(IntegrationCredential $credential): bool
|
||||
{
|
||||
$provider = $credential->provider;
|
||||
$deleted = $credential->delete();
|
||||
|
||||
Cache::forget(CacheKeys::INTEGRATION_CREDENTIAL_PREFIX->value . $provider);
|
||||
|
||||
return $deleted;
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Tasks\IntegrationCredentials;
|
||||
|
||||
use App\Containers\Dashboard\Models\IntegrationCredential;
|
||||
use App\Ship\Enums\CacheKeys;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class GetIntegrationCredentialTask
|
||||
{
|
||||
public function run(string $provider): ?IntegrationCredential
|
||||
{
|
||||
$cacheKey = CacheKeys::INTEGRATION_CREDENTIAL_PREFIX->value . $provider;
|
||||
|
||||
return Cache::remember($cacheKey, now()->addHour(), function () use ($provider) {
|
||||
return IntegrationCredential::where('provider', $provider)
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
});
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Tasks\IntegrationCredentials;
|
||||
|
||||
use App\Containers\Dashboard\Models\IntegrationCredential;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class ListIntegrationCredentialsTask
|
||||
{
|
||||
public function run(): Collection
|
||||
{
|
||||
return IntegrationCredential::orderByDesc('id')->get();
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\Tasks\IntegrationCredentials;
|
||||
|
||||
use App\Containers\Dashboard\Models\IntegrationCredential;
|
||||
use App\Ship\Enums\CacheKeys;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class SaveIntegrationCredentialTask
|
||||
{
|
||||
public function run(string $provider, array $payload, bool $isActive = true): IntegrationCredential
|
||||
{
|
||||
$credential = IntegrationCredential::updateOrCreate(
|
||||
['provider' => $provider],
|
||||
['payload' => $payload, 'is_active' => $isActive]
|
||||
);
|
||||
|
||||
Cache::forget(CacheKeys::INTEGRATION_CREDENTIAL_PREFIX->value . $provider);
|
||||
|
||||
return $credential;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use App\Containers\Dashboard\Actions\Posts\DeploySiteAction;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class DeployController extends Controller
|
||||
{
|
||||
@@ -13,9 +14,17 @@ class DeployController extends Controller
|
||||
private readonly DeploySiteAction $deploySiteAction,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Запускает деплой (создаёт файл-триггер)
|
||||
*/
|
||||
public function index(): \Inertia\Response
|
||||
{
|
||||
$history = $this->deploySiteAction->getHistory();
|
||||
$status = $this->deploySiteAction->getStatus();
|
||||
|
||||
return Inertia::render('Dashboard/Deploy/Index', [
|
||||
'history' => $history['history'],
|
||||
'status' => $status,
|
||||
]);
|
||||
}
|
||||
|
||||
public function deploy(Request $request): JsonResponse
|
||||
{
|
||||
if (app()->environment() !== 'production') {
|
||||
@@ -37,9 +46,6 @@ class DeployController extends Controller
|
||||
return response()->json($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет статус деплоя
|
||||
*/
|
||||
public function status(Request $request): JsonResponse
|
||||
{
|
||||
if (app()->environment() !== 'production') {
|
||||
@@ -58,9 +64,29 @@ class DeployController extends Controller
|
||||
return response()->json($status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает статус деплоя
|
||||
*/
|
||||
public function log(Request $request): JsonResponse
|
||||
{
|
||||
if (!$request->user()->hasRole('super_admin')) {
|
||||
return response()->json(['success' => false], 403);
|
||||
}
|
||||
|
||||
$lines = (int) $request->query('lines', 50);
|
||||
$result = $this->deploySiteAction->getLog($lines);
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
|
||||
public function history(Request $request): JsonResponse
|
||||
{
|
||||
if (!$request->user()->hasRole('super_admin')) {
|
||||
return response()->json(['success' => false], 403);
|
||||
}
|
||||
|
||||
$result = $this->deploySiteAction->getHistory();
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
|
||||
public function clear(Request $request): JsonResponse
|
||||
{
|
||||
if (!$request->user()->hasRole('super_admin')) {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\UI\WEB\Controllers;
|
||||
|
||||
use App\Containers\Dashboard\Actions\IntegrationCredentials\ManageIntegrationCredentialsAction;
|
||||
use App\Containers\Dashboard\Models\IntegrationCredential;
|
||||
use App\Containers\Dashboard\UI\WEB\Requests\StoreIntegrationCredentialRequest;
|
||||
use App\Containers\Dashboard\UI\WEB\Requests\UpdateIntegrationCredentialRequest;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class IntegrationCredentialsController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ManageIntegrationCredentialsAction $action,
|
||||
) {}
|
||||
|
||||
public function index(): \Inertia\Response
|
||||
{
|
||||
return Inertia::render('Dashboard/IntegrationCredentials/Index', [
|
||||
'credentials' => $this->action->list(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function create(): \Inertia\Response
|
||||
{
|
||||
return Inertia::render('Dashboard/IntegrationCredentials/Create');
|
||||
}
|
||||
|
||||
public function store(StoreIntegrationCredentialRequest $request): RedirectResponse
|
||||
{
|
||||
try {
|
||||
$this->action->create($request->validated());
|
||||
|
||||
return redirect()->route('dashboard.integration-credentials.index')
|
||||
->with('success', 'Провайдер успешно добавлен!');
|
||||
} catch (\Exception $e) {
|
||||
return back()
|
||||
->withInput()
|
||||
->with('error', 'Ошибка при добавлении: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function edit(IntegrationCredential $credential): \Inertia\Response
|
||||
{
|
||||
return Inertia::render('Dashboard/IntegrationCredentials/Edit', [
|
||||
'credential' => $credential,
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateIntegrationCredentialRequest $request, IntegrationCredential $credential): RedirectResponse
|
||||
{
|
||||
try {
|
||||
$this->action->update($credential, $request->validated());
|
||||
|
||||
return redirect()->route('dashboard.integration-credentials.index')
|
||||
->with('success', 'Провайдер успешно обновлён!');
|
||||
} catch (\Exception $e) {
|
||||
return back()
|
||||
->withInput()
|
||||
->with('error', 'Ошибка при обновлении: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function destroy(IntegrationCredential $credential): RedirectResponse
|
||||
{
|
||||
try {
|
||||
$this->action->delete($credential);
|
||||
|
||||
return redirect()->route('dashboard.integration-credentials.index')
|
||||
->with('success', 'Провайдер успешно удалён!');
|
||||
} catch (\Exception $e) {
|
||||
return back()
|
||||
->with('error', 'Ошибка при удалении: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\UI\WEB\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreIntegrationCredentialRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'provider' => ['required', 'string', 'max:255', 'unique:integration_credentials,provider'],
|
||||
'payload' => ['required', 'array'],
|
||||
'payload.*' => ['required', 'string'],
|
||||
'is_active' => ['boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Containers\Dashboard\UI\WEB\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UpdateIntegrationCredentialRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'provider' => ['required', 'string', 'max:255', 'unique:integration_credentials,provider,' . $this->route('credential')->id],
|
||||
'payload' => ['required', 'array'],
|
||||
'payload.*' => ['required', 'string'],
|
||||
'is_active' => ['boolean'],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user