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:
F4ilji
2026-07-02 01:03:13 +05:00
parent 09bba25993
commit 8364b220cc
21 changed files with 1191 additions and 16 deletions
@@ -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}")
@@ -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;
}
}
@@ -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();
});
}
}
@@ -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();
}
}
@@ -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;
}
}