Update
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitrix\Landing\Copilot\Connector\AI;
|
||||
|
||||
use Bitrix\Landing\Copilot\Connector\AI\Type\LimitType;
|
||||
use Bitrix\Landing\Metrika\Statuses;
|
||||
|
||||
/**
|
||||
* Data transfer object representing the result of a limit check for AI requests.
|
||||
*
|
||||
* Contains information about the type of limit, a localized message for the user,
|
||||
* and a flag indicating whether the limit has been exceeded.
|
||||
*
|
||||
* @property LimitType $type The type of the limit that was checked.
|
||||
* @property string|null $message Localized message describing the limit status.
|
||||
* @property bool $isExceeded Whether the limit has been exceeded.
|
||||
*/
|
||||
class LimitCheckResult
|
||||
{
|
||||
public LimitType $type;
|
||||
public ?string $message;
|
||||
public bool $isExceeded;
|
||||
|
||||
/**
|
||||
* LimitCheckResult constructor.
|
||||
*
|
||||
* @param LimitType $type The type of the limit that was checked.
|
||||
* @param bool $isExceeded Whether the limit has been exceeded.
|
||||
* @param string|null $message Localized message describing the limit status.
|
||||
*/
|
||||
public function __construct(LimitType $type, bool $isExceeded, ?string $message = null)
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->isExceeded = $isExceeded;
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the type of the limit that was checked.
|
||||
*
|
||||
* @return LimitType|null The type of the limit.
|
||||
*/
|
||||
public function getType(): ?LimitType
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the limit has been exceeded.
|
||||
*
|
||||
* @return bool True if the limit is exceeded, false otherwise.
|
||||
*/
|
||||
public function isExceeded(): bool
|
||||
{
|
||||
return $this->isExceeded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the localized message describing the limit status.
|
||||
*
|
||||
* @return string|null Localized message.
|
||||
*/
|
||||
public function getMessage(): ?string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the metrika status corresponding to the limit type.
|
||||
*
|
||||
* @return Statuses Metrika status for analytics.
|
||||
*/
|
||||
public function getMetrikaStatus(): Statuses
|
||||
{
|
||||
return match ($this->getType())
|
||||
{
|
||||
LimitType::Baas => Statuses::ErrorLimitBaas,
|
||||
LimitType::Daily => Statuses::ErrorLimitDaily,
|
||||
LimitType::Monthly => Statuses::ErrorLimitMonthly,
|
||||
LimitType::Market => Statuses::ErrorMarket,
|
||||
default => Statuses::ErrorB24,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,14 @@ namespace Bitrix\Landing\Copilot\Connector\AI;
|
||||
|
||||
use Bitrix\AI\Cloud;
|
||||
use Bitrix\AI\Context;
|
||||
use Bitrix\AI\Facade\Bitrix24;
|
||||
|
||||
use Bitrix\Landing\Copilot\Connector\AI\Type\ErrorCode;
|
||||
use Bitrix\Landing\Copilot\Connector\AI\Type\HelpdeskCode;
|
||||
use Bitrix\Landing\Copilot\Connector\AI\Type\LimitType;
|
||||
use Bitrix\Landing\Copilot\Connector\AI\Type\MessageCode;
|
||||
use Bitrix\Landing\Copilot\Connector\AI\Type\PromoLimitCode;
|
||||
use Bitrix\Landing\Copilot\Connector\AI\Type\SliderCode;
|
||||
use Bitrix\AI\Limiter\Enums\TypeLimit;
|
||||
use Bitrix\AI\Limiter\LimitControlService;
|
||||
use Bitrix\AI\Limiter\LimitControlBoxService;
|
||||
@@ -26,196 +34,234 @@ use Psr\Container\NotFoundExceptionInterface;
|
||||
* Class RequestLimiter
|
||||
*
|
||||
* Handles checking and messaging for request quotas to AI services (CoPilot) within the Landing module.
|
||||
* Supports both cloud (Bitrix24 module present) and box environments.
|
||||
* Supports both cloud (Bitrix24 module present) and box (on-premise) environments.
|
||||
* Determines if request limits are exceeded and returns localized error messages or null if within limits.
|
||||
*/
|
||||
class RequestLimiter
|
||||
{
|
||||
/** @see \Bitrix\AI\Engine::ERRORS (key 'LIMIT_IS_EXCEEDED') */
|
||||
protected const ERROR_CODE_LIMIT_CLOUD = 'LIMIT_IS_EXCEEDED';
|
||||
/** @see \Bitrix\AI\Engine\Cloud\CloudEngine::ERROR_CODE_LIMIT_BAAS */
|
||||
protected const ERROR_CODE_LIMIT_BAAS_CLOUD = 'LIMIT_IS_EXCEEDED_BAAS';
|
||||
protected const ERROR_CODE_RATE_LIMIT = 'RATE_LIMIT';
|
||||
protected const ERROR_CODE_DAILY = 'LIMIT_IS_EXCEEDED_DAILY';
|
||||
protected const ERROR_CODE_MONTHLY = 'LIMIT_IS_EXCEEDED_MONTHLY';
|
||||
|
||||
/**
|
||||
* Slider feature promoter codes for various limit messages
|
||||
* @var string[]
|
||||
* Stores the result of the most recent limit check, including type, message, and exceeded flag.
|
||||
*/
|
||||
protected const SLIDER_CODES = [
|
||||
'BOOST_COPILOT' => 'limit_boost_copilot',
|
||||
'DAILY' => 'limit_copilot_max_number_daily_requests',
|
||||
'MONTHLY' => 'limit_copilot_requests',
|
||||
'BOOST_COPILOT_BOX' => 'limit_boost_copilot_box',
|
||||
'REQUEST_BOX' => 'limit_copilot_requests_box',
|
||||
'BOX' => 'limit_copilot_box',
|
||||
];
|
||||
private LimitCheckResult $checkResult;
|
||||
|
||||
/**
|
||||
* Helpdesk article codes for support links
|
||||
* @var string[]
|
||||
* Constructor initializes checkResult with a default value (no limit exceeded).
|
||||
*/
|
||||
protected const HELPDESK_CODES = [
|
||||
'RATE' => '24736310',
|
||||
];
|
||||
public function __construct()
|
||||
{
|
||||
$this->checkResult = $this->createLimitResult(LimitType::None, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Promo limit codes matching Usage::PERIODS values
|
||||
* @var string[]
|
||||
* Returns the result object containing information about the current limit check.
|
||||
*
|
||||
* @see Usage::PERIODS
|
||||
* @return LimitCheckResult Limit result data transfer object with type, message, and exceeded flag.
|
||||
*/
|
||||
protected const PROMO_LIMIT_CODES = [
|
||||
'DAILY' => 'Daily',
|
||||
'MONTHLY' => 'Monthly',
|
||||
];
|
||||
public function getCheckResult(): LimitCheckResult
|
||||
{
|
||||
return $this->checkResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an AI service error represents a quota exceed.
|
||||
* Returns the localized message describing the current limit status.
|
||||
*
|
||||
* @param Error $error Error instance returned from the AI service.
|
||||
*
|
||||
* @return string|null Localized error message if limit exceeded, or null otherwise.
|
||||
* @return string|null Localized message for the user about the limit status.
|
||||
*/
|
||||
public function getTextFromError(Error $error): ?string
|
||||
public function getCheckResultMessage(): ?string
|
||||
{
|
||||
return $this->checkResult->getMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given error corresponds to a request limit exceeded situation.
|
||||
*
|
||||
* @param Error $error Error object to check.
|
||||
*
|
||||
* @return bool True if the limit is exceeded, false otherwise.
|
||||
*/
|
||||
public function checkError(Error $error): bool
|
||||
{
|
||||
if (Loader::includeModule('bitrix24'))
|
||||
{
|
||||
return $this->getTextFromLimitCloudError($error);
|
||||
$this->checkResult = $this->checkCloudErrorLimit($error);
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->checkResult = $this->checkBoxErrorLimit($error);
|
||||
}
|
||||
|
||||
return $this->getTextFromLimitBoxError($error);
|
||||
return $this->checkResult->isExceeded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the request count exceeds quota limits.
|
||||
*
|
||||
* @param int $requestCount Number of requests to check.
|
||||
*
|
||||
* @return bool True if the limit is exceeded, false otherwise.
|
||||
*/
|
||||
public function checkQuota(int $requestCount): bool
|
||||
{
|
||||
if ($requestCount <= 0)
|
||||
{
|
||||
$this->checkResult = $this->createLimitResult(LimitType::None, false);
|
||||
|
||||
return $this->checkResult->isExceeded();
|
||||
}
|
||||
|
||||
if (Loader::includeModule('bitrix24'))
|
||||
{
|
||||
$this->checkResult = $this->checkCloudQuotaLimit($requestCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->checkResult = $this->checkBoxQuotaLimit($requestCount);
|
||||
}
|
||||
|
||||
return $this->checkResult->isExceeded();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles cloud-specific AI error codes for quota limits.
|
||||
*
|
||||
* @param Error $error Error object from the cloud AI engine.
|
||||
* @param Error $error Error object to check.
|
||||
*
|
||||
* @return string|null Localized message for BAAS, daily, monthly or promo limits, or null if not a quota error.
|
||||
* @return LimitCheckResult Result object containing a localized message, limit type, and a flag indicating if the limit is exceeded.
|
||||
*/
|
||||
protected function getTextFromLimitCloudError(Error $error): ?string
|
||||
private function checkCloudErrorLimit(Error $error): LimitCheckResult
|
||||
{
|
||||
$errorCode = $error->getCode();
|
||||
$code = $error->getCode();
|
||||
|
||||
//right 2 in board
|
||||
if ($errorCode === self::ERROR_CODE_LIMIT_BAAS_CLOUD)
|
||||
//right 4 in board
|
||||
if ($code === ErrorCode::BaasRateLimit->value)
|
||||
{
|
||||
return self::getLimitMessage(
|
||||
'LANDING_REQUEST_LIMITER_ERROR_BAAS',
|
||||
self::SLIDER_CODES['BOOST_COPILOT'],
|
||||
return $this->createLimitResult(
|
||||
LimitType::Rate,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::BaasRateLimit)
|
||||
);
|
||||
}
|
||||
|
||||
if (str_starts_with($errorCode, self::ERROR_CODE_LIMIT_CLOUD))
|
||||
if ($code === ErrorCode::LimitBaasCloud->value)
|
||||
{
|
||||
//right 3 in board
|
||||
if (Bitrix24::isMarketAvailable())
|
||||
{
|
||||
return $this->createLimitResult(
|
||||
LimitType::Market,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::Market, SliderCode::Market)
|
||||
);
|
||||
}
|
||||
|
||||
//right 2 in board
|
||||
return $this->createLimitResult(
|
||||
LimitType::Baas,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::Baas, SliderCode::BoostCopilot)
|
||||
);
|
||||
}
|
||||
|
||||
if (str_starts_with($code, ErrorCode::LimitCloud->value))
|
||||
{
|
||||
//right 1 in board
|
||||
if (Loader::includeModule('baas') && Baas::getInstance()->isAvailable())
|
||||
{
|
||||
return self::getLimitMessage(
|
||||
'LANDING_REQUEST_LIMITER_ERROR_PROMO',
|
||||
self::SLIDER_CODES['BOOST_COPILOT'],
|
||||
return $this->createLimitResult(
|
||||
LimitType::Promo,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::Promo, SliderCode::BoostCopilot)
|
||||
);
|
||||
}
|
||||
|
||||
//left 1 in board
|
||||
if ($errorCode === self::ERROR_CODE_DAILY)
|
||||
if ($code === ErrorCode::Daily->value)
|
||||
{
|
||||
return self::getLimitMessage(
|
||||
'LANDING_REQUEST_LIMITER_ERROR_DAILY',
|
||||
self::SLIDER_CODES['DAILY'],
|
||||
return $this->createLimitResult(
|
||||
LimitType::Daily,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::Daily, SliderCode::Daily)
|
||||
);
|
||||
}
|
||||
|
||||
//left 2 in board
|
||||
if ($errorCode === self::ERROR_CODE_MONTHLY)
|
||||
if ($code === ErrorCode::Monthly->value)
|
||||
{
|
||||
return self::getLimitMessage(
|
||||
'LANDING_REQUEST_LIMITER_ERROR_MONTHLY',
|
||||
self::SLIDER_CODES['MONTHLY'],
|
||||
return $this->createLimitResult(
|
||||
LimitType::Monthly,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::Monthly, SliderCode::Monthly)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return $this->createLimitResult(LimitType::None, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles box AI error codes for quota limits.
|
||||
*
|
||||
* @param Error $error Error object containing code and optional custom data.
|
||||
* @param Error $error Error object to check.
|
||||
*
|
||||
* @return string Localized message for rate, BAAS, monthly or promo limits.
|
||||
* @return LimitCheckResult Result object containing a localized message, limit type, and a flag indicating if the limit is exceeded.
|
||||
*/
|
||||
protected function getTextFromLimitBoxError(Error $error): string
|
||||
private function checkBoxErrorLimit(Error $error): LimitCheckResult
|
||||
{
|
||||
$customData = $error->getCustomData();
|
||||
$errorCode = $error->getCode();
|
||||
|
||||
if ($errorCode === self::ERROR_CODE_RATE_LIMIT)
|
||||
if ($errorCode === ErrorCode::RateLimit->value)
|
||||
{
|
||||
//top in board
|
||||
return self::getLimitMessage(
|
||||
'LANDING_REQUEST_LIMITER_ERROR_RATE',
|
||||
null,
|
||||
self::HELPDESK_CODES['RATE'],
|
||||
return $this->createLimitResult(
|
||||
LimitType::Rate,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::Rate, null, HelpdeskCode::Rate)
|
||||
);
|
||||
}
|
||||
|
||||
$customData = $error->getCustomData();
|
||||
|
||||
if (
|
||||
isset($customData['showSliderWithMsg'])
|
||||
&& $errorCode === self::ERROR_CODE_LIMIT_BAAS_CLOUD
|
||||
isset($customData['showSliderWithMsg'])
|
||||
&& $errorCode === ErrorCode::LimitBaasCloud->value
|
||||
)
|
||||
{
|
||||
//right 2 in board
|
||||
if ($customData['showSliderWithMsg'] === true)
|
||||
{
|
||||
return self::getLimitMessage(
|
||||
'LANDING_REQUEST_LIMITER_ERROR_BAAS',
|
||||
self::SLIDER_CODES['BOOST_COPILOT_BOX'],
|
||||
return $this->createLimitResult(
|
||||
LimitType::Baas,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::Baas, SliderCode::BoostCopilotBox)
|
||||
);
|
||||
}
|
||||
|
||||
//left 1 in board
|
||||
return self::getLimitMessage(
|
||||
'LANDING_REQUEST_LIMITER_ERROR_MONTHLY',
|
||||
self::SLIDER_CODES['REQUEST_BOX'],
|
||||
return $this->createLimitResult(
|
||||
LimitType::Monthly,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::Monthly, SliderCode::RequestBox)
|
||||
);
|
||||
}
|
||||
|
||||
//right 1 in board
|
||||
return self::getLimitMessage(
|
||||
'LANDING_REQUEST_LIMITER_ERROR_PROMO',
|
||||
self::SLIDER_CODES['BOOST_COPILOT_BOX'],
|
||||
return $this->createLimitResult(
|
||||
LimitType::Promo,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::Promo, SliderCode::BoostCopilotBox)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a batch of AI requests can be sent without exceeding quotas.
|
||||
*
|
||||
* @param int $requestCount Number of AI requests planned.
|
||||
*
|
||||
* @return string|null Localized error message if quota would be exceeded, or null if allowed.
|
||||
*/
|
||||
public function getTextFromCheckLimit(int $requestCount): ?string
|
||||
{
|
||||
if (Loader::includeModule('bitrix24'))
|
||||
{
|
||||
return $this->getTextFromCheckCloudLimit($requestCount);
|
||||
}
|
||||
|
||||
return $this->getTextFromCheckBoxLimit($requestCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cloud-side reservation of request quota.
|
||||
*
|
||||
* @param int $requestCount Number of requests to reserve.
|
||||
*
|
||||
* @return string|null Localized message for BAAS, promo, daily or monthly limits, or null if reserved.
|
||||
* @return LimitCheckResult Result object containing a localized message, limit type, and a flag indicating if the limit is exceeded.
|
||||
*/
|
||||
protected function getTextFromCheckCloudLimit(int $requestCount): ?string
|
||||
private function checkCloudQuotaLimit(int $requestCount): LimitCheckResult
|
||||
{
|
||||
$reservedRequest = (new LimitControlService())->reserveRequest(
|
||||
new Usage(Context::getFake()),
|
||||
@@ -224,48 +270,73 @@ class RequestLimiter
|
||||
|
||||
if ($reservedRequest->isSuccess())
|
||||
{
|
||||
return null;
|
||||
return $this->createLimitResult(LimitType::None, false);
|
||||
}
|
||||
|
||||
$errorLimit = $reservedRequest->getErrorLimit();
|
||||
//right 4 in board
|
||||
if ($errorLimit === ErrorLimit::BAAS_RATE_LIMIT)
|
||||
{
|
||||
return $this->createLimitResult(
|
||||
LimitType::Rate,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::BaasRateLimit)
|
||||
);
|
||||
}
|
||||
|
||||
$typeLimit = $reservedRequest->getTypeLimit();
|
||||
//right 2 in board
|
||||
if ($errorLimit === ErrorLimit::BAAS_LIMIT && Bitrix24::isMarketAvailable())
|
||||
{
|
||||
//right 3 in board
|
||||
return $this->createLimitResult(
|
||||
LimitType::Market,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::Market, SliderCode::Market)
|
||||
);
|
||||
}
|
||||
|
||||
if ($typeLimit === TypeLimit::BAAS)
|
||||
{
|
||||
return self::getLimitMessage(
|
||||
'LANDING_REQUEST_LIMITER_ERROR_BAAS',
|
||||
self::SLIDER_CODES['BOOST_COPILOT'],
|
||||
//right 2 in board
|
||||
return $this->createLimitResult(
|
||||
LimitType::Baas,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::Baas, SliderCode::BoostCopilot)
|
||||
);
|
||||
}
|
||||
|
||||
//right 1 in board
|
||||
if (Loader::includeModule('baas') && Baas::getInstance()->isAvailable())
|
||||
{
|
||||
return self::getLimitMessage(
|
||||
'LANDING_REQUEST_LIMITER_ERROR_PROMO',
|
||||
self::SLIDER_CODES['BOOST_COPILOT'],
|
||||
return $this->createLimitResult(
|
||||
LimitType::Promo,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::Promo, SliderCode::BoostCopilot)
|
||||
);
|
||||
}
|
||||
|
||||
$promoLimitCode = $reservedRequest->getPromoLimitCode();
|
||||
//left 1 in board
|
||||
if ($promoLimitCode === self::PROMO_LIMIT_CODES['DAILY'])
|
||||
if ($promoLimitCode === PromoLimitCode::Daily->value)
|
||||
{
|
||||
return self::getLimitMessage(
|
||||
'LANDING_REQUEST_LIMITER_ERROR_DAILY',
|
||||
self::SLIDER_CODES['DAILY'],
|
||||
return $this->createLimitResult(
|
||||
LimitType::Daily,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::Daily, SliderCode::Daily)
|
||||
);
|
||||
}
|
||||
|
||||
//left 2 in board
|
||||
if ($promoLimitCode === self::PROMO_LIMIT_CODES['MONTHLY'])
|
||||
if ($promoLimitCode === PromoLimitCode::Monthly->value)
|
||||
{
|
||||
return self::getLimitMessage(
|
||||
'LANDING_REQUEST_LIMITER_ERROR_MONTHLY',
|
||||
self::SLIDER_CODES['MONTHLY'],
|
||||
return $this->createLimitResult(
|
||||
LimitType::Monthly,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::Monthly, SliderCode::Monthly)
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
return $this->createLimitResult(LimitType::None, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,23 +344,29 @@ class RequestLimiter
|
||||
*
|
||||
* @param int $requestCount Number of requests to reserve.
|
||||
*
|
||||
* @return string|null Localized message for cloud registration, rate, BAAS, monthly or promo limits, or null if reserved.
|
||||
* @return LimitCheckResult Result object containing a localized message, limit type, and a flag indicating if the limit is exceeded.
|
||||
*
|
||||
* @throws GenerationException When request reservation fails due to argument or object not found exceptions.
|
||||
*/
|
||||
protected function getTextFromCheckBoxLimit(int $requestCount): ?string
|
||||
private function checkBoxQuotaLimit(int $requestCount): LimitCheckResult
|
||||
{
|
||||
$cloudConfiguration = new Cloud\Configuration();
|
||||
$registrationDto = $cloudConfiguration->getCloudRegistrationData();
|
||||
$cloudConfig = new Cloud\Configuration();
|
||||
$registrationDto = $cloudConfig->getCloudRegistrationData();
|
||||
if (!$registrationDto)
|
||||
{
|
||||
//top in board
|
||||
return self::getLimitMessage('LANDING_REQUEST_LIMITER_ERROR_CLOUD_REGISTRATION',);
|
||||
return $this->createLimitResult(
|
||||
LimitType::Unregistered,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::CloudRegistration)
|
||||
);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$reservedBoxRequest = (new LimitControlBoxService())->isAllowedQuery($requestCount);
|
||||
}
|
||||
catch (ArgumentException | ObjectNotFoundException | NotFoundExceptionInterface $e)
|
||||
catch (ArgumentException|ObjectNotFoundException|NotFoundExceptionInterface)
|
||||
{
|
||||
throw new GenerationException(GenerationErrors::notSendRequest);
|
||||
}
|
||||
@@ -301,7 +378,7 @@ class RequestLimiter
|
||||
|
||||
if ($reservedBoxRequest->isSuccess())
|
||||
{
|
||||
return null;
|
||||
return $this->createLimitResult(LimitType::None, false);
|
||||
}
|
||||
|
||||
$limitError = $reservedBoxRequest->getErrorByLimit();
|
||||
@@ -309,10 +386,10 @@ class RequestLimiter
|
||||
if ($limitError === ErrorLimit::RATE_LIMIT)
|
||||
{
|
||||
//top in board
|
||||
return self::getLimitMessage(
|
||||
'LANDING_REQUEST_LIMITER_ERROR_RATE',
|
||||
null,
|
||||
self::HELPDESK_CODES['RATE'],
|
||||
return $this->createLimitResult(
|
||||
LimitType::Rate,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::Rate, null, HelpdeskCode::Rate)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -321,16 +398,18 @@ class RequestLimiter
|
||||
if (Loader::includeModule('baas') && Baas::getInstance()->isAvailable())
|
||||
{
|
||||
//right 1 in board
|
||||
return self::getLimitMessage(
|
||||
'LANDING_REQUEST_LIMITER_ERROR_PROMO',
|
||||
self::SLIDER_CODES['BOOST_COPILOT_BOX'],
|
||||
return $this->createLimitResult(
|
||||
LimitType::Promo,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::Promo, SliderCode::BoostCopilotBox)
|
||||
);
|
||||
}
|
||||
|
||||
//right 3 in board
|
||||
return self::getLimitMessage(
|
||||
'LANDING_REQUEST_LIMITER_ERROR_PROMO',
|
||||
self::SLIDER_CODES['BOX'],
|
||||
return $this->createLimitResult(
|
||||
LimitType::Promo,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::Promo, SliderCode::Box)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -338,57 +417,78 @@ class RequestLimiter
|
||||
if ($typeLimit === TypeLimit::BAAS)
|
||||
{
|
||||
//right 2 in board
|
||||
return self::getLimitMessage(
|
||||
'LANDING_REQUEST_LIMITER_ERROR_BAAS',
|
||||
self::SLIDER_CODES['BOOST_COPILOT_BOX'],
|
||||
return $this->createLimitResult(
|
||||
LimitType::Baas,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::Baas, SliderCode::BoostCopilotBox)
|
||||
);
|
||||
}
|
||||
|
||||
if ($limitError === ErrorLimit::PROMO_LIMIT)
|
||||
{
|
||||
//left 1 in board
|
||||
return self::getLimitMessage(
|
||||
'LANDING_REQUEST_LIMITER_ERROR_MONTHLY',
|
||||
self::SLIDER_CODES['REQUEST_BOX'],
|
||||
return $this->createLimitResult(
|
||||
LimitType::Monthly,
|
||||
true,
|
||||
self::buildLimitMessage(MessageCode::Monthly, SliderCode::RequestBox)
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
return $this->createLimitResult(LimitType::None, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the final text phrase with substituted links
|
||||
* Creates a LimitCheckResult object.
|
||||
*
|
||||
* @param string $phraseCode Localization phrase code
|
||||
* @param string|null $featurePromoterCode Feature promoter code (FEATURE_PROMOTER)
|
||||
* @param string|null $helpdeskCode Helpdesk promoter code
|
||||
* @param LimitType $limitType Type of the limit that was exceeded, or null if not applicable.
|
||||
* @param bool $isExceeded True if the limit is exceeded, false otherwise.
|
||||
* @param string|null $message Localized message for the user, or null if no limit is exceeded.
|
||||
*
|
||||
* @return string
|
||||
* @return LimitCheckResult
|
||||
*/
|
||||
private static function getLimitMessage(
|
||||
string $phraseCode,
|
||||
?string $featurePromoterCode = null,
|
||||
?string $helpdeskCode = null,
|
||||
private function createLimitResult(LimitType $limitType, bool $isExceeded, ?string $message = null): LimitCheckResult
|
||||
{
|
||||
if ($message === null)
|
||||
{
|
||||
return new LimitCheckResult($limitType, $isExceeded);
|
||||
}
|
||||
|
||||
return new LimitCheckResult($limitType, $isExceeded, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the final localized message with substituted links for limits.
|
||||
*
|
||||
* @param MessageCode $phraseCode Localization phrase code.
|
||||
* @param SliderCode|null $featurePromoterCode Optional feature promoter code for link substitution.
|
||||
* @param HelpdeskCode|null $helpdeskCode Optional helpdesk code for help link substitution.
|
||||
*
|
||||
* @return string Localized message with links.
|
||||
*/
|
||||
private static function buildLimitMessage(
|
||||
MessageCode $phraseCode,
|
||||
?SliderCode $featurePromoterCode = null,
|
||||
?HelpdeskCode $helpdeskCode = null,
|
||||
): string
|
||||
{
|
||||
if ($featurePromoterCode !== null)
|
||||
{
|
||||
return Loc::getMessage($phraseCode, [
|
||||
'#LINK#' => "[url=/?FEATURE_PROMOTER={$featurePromoterCode}]",
|
||||
'#/LINK#' => '[/url]'
|
||||
return Loc::getMessage($phraseCode->value, [
|
||||
'#LINK#' => "[url=/?FEATURE_PROMOTER=$featurePromoterCode->value]",
|
||||
'#/LINK#' => '[/url]',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($helpdeskCode !== null)
|
||||
{
|
||||
$helpUrl = Util::getArticleUrlByCode($helpdeskCode);
|
||||
$helpUrl = Util::getArticleUrlByCode($helpdeskCode->value);
|
||||
|
||||
return Loc::getMessage($phraseCode, [
|
||||
return Loc::getMessage($phraseCode->value, [
|
||||
'#HELP#' => "[url=$helpUrl]",
|
||||
'#/HELP#' => '[/url]'
|
||||
'#/HELP#' => '[/url]',
|
||||
]);
|
||||
}
|
||||
|
||||
return Loc::getMessage($phraseCode);
|
||||
return Loc::getMessage($phraseCode->value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitrix\Landing\Copilot\Connector\AI\Type;
|
||||
|
||||
enum ErrorCode: string
|
||||
{
|
||||
/** @see \Bitrix\AI\Engine::ERRORS (key 'LIMIT_IS_EXCEEDED') */
|
||||
case LimitCloud = 'LIMIT_IS_EXCEEDED';
|
||||
/** @see \Bitrix\AI\Engine\Cloud\CloudEngine::ERROR_CODE_LIMIT_BAAS */
|
||||
case LimitBaasCloud = 'LIMIT_IS_EXCEEDED_BAAS';
|
||||
case RateLimit = 'RATE_LIMIT';
|
||||
case Daily = 'LIMIT_IS_EXCEEDED_DAILY';
|
||||
case Monthly = 'LIMIT_IS_EXCEEDED_MONTHLY';
|
||||
case BaasRateLimit = 'LIMIT_IS_EXCEEDED_BAAS_RATE_LIMIT';
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitrix\Landing\Copilot\Connector\AI\Type;
|
||||
|
||||
enum HelpdeskCode: string
|
||||
{
|
||||
case Rate = '24736310';
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitrix\Landing\Copilot\Connector\AI\Type;
|
||||
|
||||
enum LimitType: string
|
||||
{
|
||||
case None = 'none';
|
||||
case Baas = 'baas';
|
||||
case Promo = 'promo';
|
||||
case Daily = 'daily';
|
||||
case Monthly = 'monthly';
|
||||
case Rate = 'rate';
|
||||
case Market = 'market';
|
||||
case Unregistered = 'unregistered';
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitrix\Landing\Copilot\Connector\AI\Type;
|
||||
|
||||
enum MessageCode: string
|
||||
{
|
||||
case Baas = 'LANDING_REQUEST_LIMITER_ERROR_BAAS';
|
||||
case Promo = 'LANDING_REQUEST_LIMITER_ERROR_PROMO';
|
||||
case Daily = 'LANDING_REQUEST_LIMITER_ERROR_DAILY';
|
||||
case Monthly = 'LANDING_REQUEST_LIMITER_ERROR_MONTHLY';
|
||||
case Rate = 'LANDING_REQUEST_LIMITER_ERROR_RATE';
|
||||
case CloudRegistration = 'LANDING_REQUEST_LIMITER_ERROR_CLOUD_REGISTRATION';
|
||||
case Market = 'LANDING_REQUEST_LIMITER_ERROR_MARKET';
|
||||
case BaasRateLimit = 'LANDING_REQUEST_LIMITER_ERROR_BAAS_RATE_LIMIT';
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitrix\Landing\Copilot\Connector\AI\Type;
|
||||
|
||||
enum PromoLimitCode: string
|
||||
{
|
||||
case Daily = 'Daily';
|
||||
case Monthly = 'Monthly';
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitrix\Landing\Copilot\Connector\AI\Type;
|
||||
|
||||
enum SliderCode: string
|
||||
{
|
||||
case BoostCopilot = 'limit_boost_copilot';
|
||||
case Daily = 'limit_copilot_max_number_daily_requests';
|
||||
case Monthly = 'limit_copilot_requests';
|
||||
case BoostCopilotBox = 'limit_boost_copilot_box';
|
||||
case RequestBox = 'limit_copilot_requests_box';
|
||||
case Box = 'limit_copilot_box';
|
||||
case Market = 'limit_subscription_market_access_buy_marketplus';
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use Bitrix\AI\Chatbot\Message\SystemMessage;
|
||||
use Bitrix\Landing\Copilot\Data\Wishes;
|
||||
use Bitrix\Landing\Copilot\Generation;
|
||||
use Bitrix\Landing\Copilot\Data;
|
||||
use Bitrix\Landing\Metrika;
|
||||
use Bitrix\Main\LoaderException;
|
||||
use Bitrix\Main\Localization\Loc;
|
||||
use Bitrix\Main\SystemException;
|
||||
@@ -162,6 +163,13 @@ class CreateSiteChatBot extends CopilotChatBot
|
||||
->setScenario(new Generation\Scenario\CreateSite())
|
||||
->setChatId($chatId)
|
||||
->setWishes($wishes)
|
||||
->setMetrikaFields(new Metrika\FieldsDto(
|
||||
params: [[
|
||||
3,
|
||||
'context',
|
||||
$wishes->isDemoWishes() ? 'no' : 'user'
|
||||
]],
|
||||
))
|
||||
->execute()
|
||||
)
|
||||
{
|
||||
|
||||
@@ -12,6 +12,8 @@ class Wishes
|
||||
private array $wishes = [];
|
||||
private ?string $company = null;
|
||||
|
||||
protected bool $isDemo = false;
|
||||
|
||||
/**
|
||||
* Replace all wishes to passed array. Empty strings will be skipped
|
||||
* @param array $wishes
|
||||
@@ -48,10 +50,20 @@ class Wishes
|
||||
public function setDemoWishes(): self
|
||||
{
|
||||
$this->wishes = [self::getDemoWish()];
|
||||
$this->isDemo = true;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check is demo wishes used
|
||||
* @return bool
|
||||
*/
|
||||
public function isDemoWishes(): bool
|
||||
{
|
||||
return $this->isDemo;
|
||||
}
|
||||
|
||||
private static function getDemoWish(): string
|
||||
{
|
||||
return Loc::getMessage('LANDING_COPILOT_DEMO_WISH_' . (rand(1, 20)));
|
||||
|
||||
@@ -10,7 +10,9 @@ use Bitrix\Landing\Copilot\Generation\GenerationException;
|
||||
use Bitrix\Landing\Copilot\Generation\Scenario\IScenario;
|
||||
use Bitrix\Landing\Copilot\Generation\Scenario\Scenarist;
|
||||
use Bitrix\Landing\Copilot\Generation\Timer;
|
||||
use Bitrix\Landing\Copilot\Generation\Type\GenerationErrors;
|
||||
use Bitrix\Landing\Copilot\Model\GenerationsTable;
|
||||
use Bitrix\Landing\Metrika;
|
||||
use Bitrix\Main\Application;
|
||||
use Bitrix\Main\ArgumentException;
|
||||
use Bitrix\Main\ORM\Query\Query;
|
||||
@@ -19,6 +21,7 @@ use Bitrix\Main\Web\Json;
|
||||
|
||||
/**
|
||||
* This class is responsible for generating site content using various managers and connectors.
|
||||
* It manages the generation process, scenario execution, metrika analytics, and error handling.
|
||||
*/
|
||||
class Generation
|
||||
{
|
||||
@@ -28,6 +31,8 @@ class Generation
|
||||
private const EVENT_GENERATION_ERROR = 'onGenerationError';
|
||||
private const EVENT_GENERATION_FINISH = 'onGenerationFinish';
|
||||
|
||||
private const DATA_METRIKA_KEY = 'metrikaFields';
|
||||
|
||||
private int $id;
|
||||
private ?int $chatId = null;
|
||||
private IScenario $scenario;
|
||||
@@ -38,18 +43,28 @@ class Generation
|
||||
|
||||
private Scenarist $scenarist;
|
||||
private Timer $timer;
|
||||
protected ?Generation\Event $event = null;
|
||||
private ?Generation\Event $event = null;
|
||||
private ?Metrika\FieldsDto $metrikaFields = null;
|
||||
private Metrika\MetrikaProviderParamService $metrikaProviderParamService;
|
||||
|
||||
|
||||
/**
|
||||
* Generation constructor.
|
||||
* Initializes authorId, siteData, and timer.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->authorId = Landing\Manager::getUserId();
|
||||
|
||||
$this->siteData = new Data\Site();
|
||||
$this->timer = new Timer();
|
||||
|
||||
$this->metrikaProviderParamService = new Metrika\MetrikaProviderParamService();
|
||||
}
|
||||
|
||||
/**
|
||||
* ID of user who created the site
|
||||
* Returns the ID of the user who created the site.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getAuthorId(): int
|
||||
@@ -58,8 +73,10 @@ class Generation
|
||||
}
|
||||
|
||||
/**
|
||||
* If generation start by chat - set ID
|
||||
* @param int $chatId
|
||||
* Sets the chat ID if generation is started by chat.
|
||||
*
|
||||
* @param int $chatId Chat ID.
|
||||
*
|
||||
* @return Generation
|
||||
*/
|
||||
public function setChatId(int $chatId): self
|
||||
@@ -73,7 +90,8 @@ class Generation
|
||||
}
|
||||
|
||||
/**
|
||||
* If generation started by chat - get ID
|
||||
* Gets the chat ID if generation is started by chat.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getChatId(): ?int
|
||||
@@ -82,7 +100,10 @@ class Generation
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the scenario for this generation.
|
||||
*
|
||||
* @param IScenario $scenario
|
||||
*
|
||||
* @return Generation
|
||||
*/
|
||||
public function setScenario(IScenario $scenario): self
|
||||
@@ -93,6 +114,8 @@ class Generation
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the scenario for this generation.
|
||||
*
|
||||
* @return IScenario|null
|
||||
*/
|
||||
public function getScenario(): ?IScenario
|
||||
@@ -100,6 +123,13 @@ class Generation
|
||||
return $this->scenario ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the site data for this generation.
|
||||
*
|
||||
* @param Data\Site $siteData
|
||||
*
|
||||
* @return Generation
|
||||
*/
|
||||
public function setSiteData(Data\Site $siteData): self
|
||||
{
|
||||
$this->siteData = $siteData;
|
||||
@@ -107,15 +137,22 @@ class Generation
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the site data for this generation.
|
||||
*
|
||||
* @return Data\Site
|
||||
*/
|
||||
public function getSiteData(): Data\Site
|
||||
{
|
||||
return $this->siteData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom data for generations
|
||||
* @param string|null $key
|
||||
* @return mixed|null - null if key not set
|
||||
* Gets custom data for this generation.
|
||||
*
|
||||
* @param string|null $key Optional key to retrieve a specific value.
|
||||
*
|
||||
* @return mixed|null Returns the value for the given key, all data if key is null, or null if not set.
|
||||
*/
|
||||
public function getData(?string $key = null): mixed
|
||||
{
|
||||
@@ -128,9 +165,11 @@ class Generation
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom data for generations
|
||||
* @param string $key
|
||||
* @param mixed $data
|
||||
* Sets custom data for this generation.
|
||||
*
|
||||
* @param string $key Data key.
|
||||
* @param mixed $data Data value.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setData(string $key, mixed $data): void
|
||||
@@ -144,8 +183,10 @@ class Generation
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete one key from data
|
||||
* @param string $key
|
||||
* Deletes a key from the custom data.
|
||||
*
|
||||
* @param string $key Data key to delete.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function deleteData(string $key): void
|
||||
@@ -156,6 +197,13 @@ class Generation
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the wishes for the site data.
|
||||
*
|
||||
* @param Data\Wishes $wishes
|
||||
*
|
||||
* @return Generation
|
||||
*/
|
||||
public function setWishes(Data\Wishes $wishes): self
|
||||
{
|
||||
$this->siteData->setWishes($wishes);
|
||||
@@ -163,27 +211,118 @@ class Generation
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the generation ID.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current step of the generation.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getStep(): ?int
|
||||
{
|
||||
return $this->step ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the timer object for this generation.
|
||||
*
|
||||
* @return Timer
|
||||
*/
|
||||
public function getTimer(): Timer
|
||||
{
|
||||
return $this->timer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to find exists generation and init current by them data
|
||||
* Sets metrika fields for analytics and stores them in custom data.
|
||||
*
|
||||
* @param int $generationId
|
||||
* @param Metrika\FieldsDto $fields
|
||||
*
|
||||
* @return bool
|
||||
* @return Generation
|
||||
*/
|
||||
public function setMetrikaFields(Metrika\FieldsDto $fields): self
|
||||
{
|
||||
$this->metrikaFields = $fields;
|
||||
$this->setData(self::DATA_METRIKA_KEY, $this->metrikaFields->toArray());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets metrika fields for analytics.
|
||||
*
|
||||
* @return Metrika\FieldsDto|null
|
||||
*/
|
||||
private function getMetrikaFields(): ?Metrika\FieldsDto
|
||||
{
|
||||
$saved =
|
||||
$this->getData(self::DATA_METRIKA_KEY)
|
||||
? Metrika\FieldsDto::fromArray($this->getData(self::DATA_METRIKA_KEY))
|
||||
: null
|
||||
;
|
||||
if (!$saved)
|
||||
{
|
||||
return $this->metrikaFields;
|
||||
}
|
||||
|
||||
return $saved->addFields($this->metrikaFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Metrika analytics object for the given event.
|
||||
*
|
||||
* @param Metrika\Events $event
|
||||
*
|
||||
* @return Metrika\Metrika
|
||||
*/
|
||||
public function getMetrika(Metrika\Events $event): Metrika\Metrika
|
||||
{
|
||||
$metrika = new Metrika\Metrika(
|
||||
$this->scenario->getAnalyticCategory(),
|
||||
$event,
|
||||
Metrika\Tools::ai
|
||||
);
|
||||
|
||||
$this->metrikaProviderParamService->setParams($metrika, $event);
|
||||
|
||||
$fields = $this->getMetrikaFields();
|
||||
if (isset($fields))
|
||||
{
|
||||
foreach ($fields->params ?? [] as $param)
|
||||
{
|
||||
if (count($param) === 3)
|
||||
{
|
||||
$metrika->setParam(
|
||||
(int)$param[0],
|
||||
(string)$param[1],
|
||||
(string)$param[2],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($fields->subSection))
|
||||
{
|
||||
$metrika->setSubSection($this->metrikaFields->subSection);
|
||||
}
|
||||
}
|
||||
|
||||
return $metrika;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find an existing generation by ID and initializes this instance with its data.
|
||||
*
|
||||
* @param int $generationId Generation ID.
|
||||
*
|
||||
* @return bool True if found and initialized, false otherwise.
|
||||
*/
|
||||
public function initById(int $generationId): bool
|
||||
{
|
||||
@@ -201,11 +340,12 @@ class Generation
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to find exists generation by site ID and init current by them data
|
||||
* Tries to find an existing generation by site ID and scenario, and initializes this instance with its data.
|
||||
*
|
||||
* @param int $siteId
|
||||
* @param IScenario $scenario
|
||||
* @return bool
|
||||
* @param int $siteId Site ID.
|
||||
* @param IScenario $scenario Scenario instance.
|
||||
*
|
||||
* @return bool True if found and initialized, false otherwise.
|
||||
*/
|
||||
public function initBySiteId(int $siteId, IScenario $scenario): bool
|
||||
{
|
||||
@@ -218,6 +358,12 @@ class Generation
|
||||
return $this->initExists($filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this instance with data from an existing generation matching the filter.
|
||||
* @param Filter\ConditionTree $filter ORM filter for the query.
|
||||
*
|
||||
* @return bool True if found and initialized, false otherwise.
|
||||
*/
|
||||
private function initExists(Filter\ConditionTree $filter): bool
|
||||
{
|
||||
$generation = GenerationsTable::query()
|
||||
@@ -275,8 +421,9 @@ class Generation
|
||||
}
|
||||
|
||||
/**
|
||||
* Run process.
|
||||
* @return bool - false if error while executing
|
||||
* Runs the generation process.
|
||||
*
|
||||
* @return bool False if an error occurs while executing, true otherwise.
|
||||
*/
|
||||
public function execute(): bool
|
||||
{
|
||||
@@ -319,6 +466,7 @@ class Generation
|
||||
}
|
||||
catch (GenerationException $e)
|
||||
{
|
||||
$this->sendMetrikaError($e);
|
||||
$this->scenario->getChatbot()?->sendErrorMessage(new ChatBotMessageDto(
|
||||
$this->getChatId() ?? 0,
|
||||
$this->id,
|
||||
@@ -351,6 +499,11 @@ class Generation
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the generation is executable (all required properties are set).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function isExecutable(): bool
|
||||
{
|
||||
return isset(
|
||||
@@ -360,23 +513,39 @@ class Generation
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the lock name for this generation.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getLockName(): string
|
||||
{
|
||||
return 'landing_copilot_generation_' . ($this->id ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules an agent to retry execution.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function setAgent(): void
|
||||
{
|
||||
Agent::addUniqueAgent('executeGeneration', [$this->id], 60, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the scheduled agent for this generation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function deleteAgent(): void
|
||||
{
|
||||
Agent::deleteUniqueAgent('executeGeneration', [$this->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finish all generation processed, mark as completed
|
||||
* Finishes the generation process and marks it as completed.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function finish(): void
|
||||
@@ -391,7 +560,8 @@ class Generation
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if scenario execute all steps
|
||||
* Checks if the scenario has executed all steps.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFinished(): bool
|
||||
@@ -404,13 +574,19 @@ class Generation
|
||||
return $this->scenarist->isFinished();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles actions to perform when the generation is finished.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function onFinish(): void
|
||||
{
|
||||
$this->getEvent()->send(self::EVENT_GENERATION_FINISH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if at least one scenario step has error and not execute
|
||||
* Checks if at least one scenario step has an error and was not executed.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isError(): bool
|
||||
@@ -424,7 +600,8 @@ class Generation
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare scenario to restart generation after error
|
||||
* Prepares the scenario to restart generation after an error by clearing errors.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function clearErrors(): self
|
||||
@@ -437,6 +614,11 @@ class Generation
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the Scenarist object for this generation.
|
||||
*
|
||||
* @return bool True if successfully initialized, false otherwise.
|
||||
*/
|
||||
private function initScenarist(): bool
|
||||
{
|
||||
if (!isset(
|
||||
@@ -459,6 +641,11 @@ class Generation
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the current generation state to the database.
|
||||
*
|
||||
* @return bool True on success, false otherwise.
|
||||
*/
|
||||
private function save(): bool
|
||||
{
|
||||
if (!isset(
|
||||
@@ -503,7 +690,8 @@ class Generation
|
||||
}
|
||||
|
||||
/**
|
||||
* Get object for send front and backend events
|
||||
* Gets the event object for sending front-end and back-end events.
|
||||
*
|
||||
* @return Generation\Event
|
||||
*/
|
||||
public function getEvent(): Generation\Event
|
||||
@@ -524,6 +712,13 @@ class Generation
|
||||
return $this->event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a generation with the given ID exists.
|
||||
*
|
||||
* @param int $id Generation ID.
|
||||
*
|
||||
* @return bool True if exists, false otherwise.
|
||||
*/
|
||||
public static function checkExists(int $id): bool
|
||||
{
|
||||
static $generations = [];
|
||||
@@ -540,6 +735,13 @@ class Generation
|
||||
return $generations[$id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets block data for the given blocks as a JSON string.
|
||||
*
|
||||
* @param array $blocks Array of block objects.
|
||||
*
|
||||
* @return string JSON-encoded block data, or empty string on error.
|
||||
*/
|
||||
public function getBlocksData(array $blocks): string
|
||||
{
|
||||
$siteData = $this->getSiteData();
|
||||
@@ -566,4 +768,52 @@ class Generation
|
||||
|
||||
return $blockDataEncoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends metrika analytics for an error during generation.
|
||||
*
|
||||
* @param GenerationException $e
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function sendMetrikaError(GenerationException $e): void
|
||||
{
|
||||
$errorCode = $e->getErrorCode();
|
||||
/**
|
||||
* @var Metrika\Statuses $status
|
||||
*/
|
||||
$status = match ($errorCode)
|
||||
{
|
||||
GenerationErrors::requestQuotaExceeded => $e->getParams()['metrikaStatus'] ?? Metrika\Statuses::ErrorB24,
|
||||
GenerationErrors::restrictedRequest => Metrika\Statuses::ErrorContentPolicy,
|
||||
GenerationErrors::notExistResponse,
|
||||
GenerationErrors::notFullyResponse,
|
||||
GenerationErrors::notCorrectResponse => Metrika\Statuses::ErrorProvider,
|
||||
default => Metrika\Statuses::ErrorB24,
|
||||
};
|
||||
|
||||
$scenario = $this->getScenario();
|
||||
if ($scenario)
|
||||
{
|
||||
$stepsMap = $scenario->getMap();
|
||||
if (isset($stepsMap[$this->step]))
|
||||
{
|
||||
$step = $stepsMap[$this->step];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($step))
|
||||
{
|
||||
$analyticEvent = $step->getAnalyticEvent();
|
||||
}
|
||||
|
||||
if (!isset($analyticEvent))
|
||||
{
|
||||
$analyticEvent = Metrika\Events::unknown;
|
||||
}
|
||||
|
||||
$metrika = $this->getMetrika($analyticEvent);
|
||||
$metrika->setStatus($status);
|
||||
$metrika->send();
|
||||
}
|
||||
}
|
||||
@@ -7,52 +7,75 @@ namespace Bitrix\Landing\Copilot\Generation;
|
||||
use Bitrix\Main\SystemException;
|
||||
use Bitrix\Landing\Copilot\Generation\Type\GenerationErrors;
|
||||
|
||||
/**
|
||||
* Exception class for errors occurring during AI generation processes.
|
||||
* Stores an error code, optional additional message, and parameters for detailed context.
|
||||
*/
|
||||
class GenerationException extends SystemException
|
||||
{
|
||||
/**
|
||||
* Array with error message templates.
|
||||
* @var array Parameters for the error message template.
|
||||
*/
|
||||
|
||||
private ?array $params;
|
||||
private array $params;
|
||||
|
||||
/**
|
||||
* Exception constructor.
|
||||
*
|
||||
* @param GenerationErrors $code Error code.
|
||||
* @param string $message Additional message (optional).
|
||||
* @var GenerationErrors Error code enum for this exception.
|
||||
*/
|
||||
public function __construct(GenerationErrors $code, string $message = '', ?array $params = null)
|
||||
private GenerationErrors $errorCode;
|
||||
|
||||
/**
|
||||
* GenerationException constructor.
|
||||
*
|
||||
* @param GenerationErrors $errorCode Error code enum describing the type of generation error.
|
||||
* @param string $message Optional additional message for more context.
|
||||
* @param array $params Optional parameters for the error message template.
|
||||
*/
|
||||
public function __construct(GenerationErrors $errorCode, string $message = '', array $params = [])
|
||||
{
|
||||
$this->params = $params;
|
||||
$errorMessage = $this->buildErrorMessage($code, $message);
|
||||
$this->errorCode = $errorCode;
|
||||
$errorMessage = $this->buildErrorMessage($errorCode, $message);
|
||||
|
||||
parent::__construct($errorMessage, $code->value);
|
||||
parent::__construct($errorMessage, $errorCode->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get exception code transform to enum
|
||||
* @return ?GenerationErrors
|
||||
* Returns the exception code as a GenerationErrors enum instance.
|
||||
*
|
||||
* @return GenerationErrors The error code as an enum value.
|
||||
*/
|
||||
public function getCodeObject(): ?GenerationErrors
|
||||
public function getCodeObject(): GenerationErrors
|
||||
{
|
||||
return GenerationErrors::tryFrom($this->getCode()) ?? GenerationErrors::default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|null
|
||||
* Returns the parameters associated with this exception.
|
||||
*
|
||||
* @return array Parameters for the error message template.
|
||||
*/
|
||||
public function getParams(): ?array
|
||||
public function getParams(): array
|
||||
{
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives an error message based on the code.
|
||||
* Returns the error code enum for this exception.
|
||||
*
|
||||
* @param GenerationErrors $code Error code DTO
|
||||
* @param string $additionalMessage Additional message.
|
||||
* @return GenerationErrors The error code.
|
||||
*/
|
||||
public function getErrorCode(): GenerationErrors
|
||||
{
|
||||
return $this->errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the final error message based on the code and an optional additional message.
|
||||
*
|
||||
* @return string Error message.
|
||||
* @param GenerationErrors $code Error code enum.
|
||||
* @param string $additionalMessage Optional additional message for more context.
|
||||
*
|
||||
* @return string The complete error message.
|
||||
*/
|
||||
protected function buildErrorMessage(GenerationErrors $code, string $additionalMessage): string
|
||||
{
|
||||
@@ -66,6 +89,13 @@ class GenerationException extends SystemException
|
||||
return $message . '.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a default error message for a given error code.
|
||||
*
|
||||
* @param GenerationErrors $code Error code enum.
|
||||
*
|
||||
* @return string The default error message for the code.
|
||||
*/
|
||||
private static function getErrorMessage(GenerationErrors $code): string
|
||||
{
|
||||
$messages = [
|
||||
|
||||
@@ -20,6 +20,12 @@ use Bitrix\Main\Type\DateTime;
|
||||
use Bitrix\Main\Web;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Class Request
|
||||
*
|
||||
* Handles the lifecycle of a single AI request within a generation step,
|
||||
* including sending, result/error processing, persistence, and status management.
|
||||
*/
|
||||
class Request
|
||||
{
|
||||
// todo: get individual time from step
|
||||
@@ -39,6 +45,12 @@ class Request
|
||||
private Type\RequestStatus $status = Type\RequestStatus::New;
|
||||
private RequestLimiter $requestLimiter;
|
||||
|
||||
/**
|
||||
* Request constructor.
|
||||
*
|
||||
* @param int $generationId The ID of the generation this request belongs to.
|
||||
* @param int $stepId The step ID within the generation.
|
||||
*/
|
||||
public function __construct(int $generationId, int $stepId)
|
||||
{
|
||||
$this->generationId = $generationId;
|
||||
@@ -46,11 +58,14 @@ class Request
|
||||
}
|
||||
|
||||
/**
|
||||
* Send request to AI provider
|
||||
* @param Prompt $prompt
|
||||
* @param IConnector $connector
|
||||
* @return bool
|
||||
* @throws GenerationException
|
||||
* Sends a request to the AI provider using the given prompt and connector.
|
||||
*
|
||||
* @param Prompt $prompt The prompt to send.
|
||||
* @param IConnector $connector The AI connector to use.
|
||||
*
|
||||
* @return bool True on successful send and save, false otherwise.
|
||||
*
|
||||
* @throws GenerationException If the request fails or a quota/limit is exceeded.
|
||||
*/
|
||||
public function send(Prompt $prompt, IConnector $connector): bool
|
||||
{
|
||||
@@ -115,9 +130,13 @@ class Request
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Main\Error|null $error
|
||||
* Processes an error from the AI provider and throws a GenerationException.
|
||||
*
|
||||
* @param Main\Error|null $error The error object from the provider.
|
||||
*
|
||||
* @return void
|
||||
* @throws GenerationException
|
||||
*
|
||||
* @throws GenerationException Always throws; either for quota exceeded or generic request error.
|
||||
*/
|
||||
private function processError(?Main\Error $error): void
|
||||
{
|
||||
@@ -126,18 +145,23 @@ class Request
|
||||
throw new GenerationException(GenerationErrors::notSendRequest);
|
||||
}
|
||||
|
||||
$errorText = $this->getRequestLimiter()->getTextFromError($error);
|
||||
|
||||
if ($errorText)
|
||||
if ($this->getRequestLimiter()->checkError($error))
|
||||
{
|
||||
$params = [
|
||||
'errorText' => $errorText,
|
||||
];
|
||||
|
||||
throw new GenerationException(GenerationErrors::requestQuotaExceeded, $error->getMessage(), $params);
|
||||
$message = $this->getRequestLimiter()->getCheckResultMessage();
|
||||
if (is_string($message))
|
||||
{
|
||||
throw new GenerationException(
|
||||
GenerationErrors::requestQuotaExceeded,
|
||||
$error->getMessage(),
|
||||
[
|
||||
'errorText' => $message,
|
||||
'metrikaStatus' => $this->getRequestLimiter()->getCheckResult()?->getMetrikaStatus(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw new GenerationException(GenerationErrors::errorInRequest, $error->getMessage(), null);
|
||||
throw new GenerationException(GenerationErrors::errorInRequest, $error->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,6 +179,11 @@ class Request
|
||||
return $this->requestLimiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the request as applied to the step, updating the status and persistence.
|
||||
*
|
||||
* @return bool True if successfully applied, false otherwise.
|
||||
*/
|
||||
public function setApplied(): bool
|
||||
{
|
||||
if ($this->status->value < Type\RequestStatus::Received->value)
|
||||
@@ -186,9 +215,11 @@ class Request
|
||||
}
|
||||
|
||||
/**
|
||||
* Save result of AI request
|
||||
* @param array $result - data array
|
||||
* @return bool
|
||||
* Saves the result of the AI request.
|
||||
*
|
||||
* @param array $result The result data array.
|
||||
*
|
||||
* @return bool True on success, false otherwise.
|
||||
*/
|
||||
public function saveResult(array $result): bool
|
||||
{
|
||||
@@ -205,9 +236,11 @@ class Request
|
||||
}
|
||||
|
||||
/**
|
||||
* Save error code and message
|
||||
* @param Generation\Error $error
|
||||
* @return bool
|
||||
* Saves an error code and message for this request.
|
||||
*
|
||||
* @param Generation\Error $error The error DTO.
|
||||
*
|
||||
* @return bool True on success, false otherwise.
|
||||
*/
|
||||
public function saveError(Generation\Error $error): bool
|
||||
{
|
||||
@@ -223,12 +256,22 @@ class Request
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks this request as deleted and persists the change.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setDeleted(): void
|
||||
{
|
||||
$this->isDeleted = true;
|
||||
$this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the current state of the request to the database.
|
||||
*
|
||||
* @return bool True on success, false otherwise.
|
||||
*/
|
||||
private function save(): bool
|
||||
{
|
||||
if ($this->status->value < Type\RequestStatus::Sent->value)
|
||||
@@ -286,8 +329,9 @@ class Request
|
||||
}
|
||||
|
||||
/**
|
||||
* Return ID of current generation
|
||||
* @return int
|
||||
* Returns the ID of the current generation.
|
||||
*
|
||||
* @return int Generation ID.
|
||||
*/
|
||||
public function getGenerationId(): int
|
||||
{
|
||||
@@ -295,8 +339,9 @@ class Request
|
||||
}
|
||||
|
||||
/**
|
||||
* If request received - return result data. Else - return null
|
||||
* @return array|null
|
||||
* Returns the result data if the request was received, or null otherwise.
|
||||
*
|
||||
* @return array|null Result data array or null.
|
||||
*/
|
||||
public function getResult(): ?array
|
||||
{
|
||||
@@ -304,8 +349,9 @@ class Request
|
||||
}
|
||||
|
||||
/**
|
||||
* If request finish with error - get error DTO
|
||||
* @return ?Generation\Error
|
||||
* Returns the error DTO if the request finished with an error, or null otherwise.
|
||||
*
|
||||
* @return Generation\Error|null Error DTO or null.
|
||||
*/
|
||||
public function getError(): ?Generation\Error
|
||||
{
|
||||
@@ -313,7 +359,8 @@ class Request
|
||||
}
|
||||
|
||||
/**
|
||||
* If request receive answer
|
||||
* Returns true if the request has received an answer (result or error).
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isReceived(): bool
|
||||
@@ -322,7 +369,8 @@ class Request
|
||||
}
|
||||
|
||||
/**
|
||||
* If request answer was applied to step
|
||||
* Returns true if the request answer was applied to the step.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isApplied(): bool
|
||||
@@ -331,8 +379,9 @@ class Request
|
||||
}
|
||||
|
||||
/**
|
||||
* ID in DB
|
||||
* @return int|null
|
||||
* Returns the database ID of this request.
|
||||
*
|
||||
* @return int|null Request ID or null if not persisted.
|
||||
*/
|
||||
public function getId(): ?int
|
||||
{
|
||||
@@ -340,9 +389,12 @@ class Request
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $generationId
|
||||
* @param int $stepId
|
||||
* @return array<Request> - array of exists requests
|
||||
* Returns all existing requests for a given generation and step.
|
||||
*
|
||||
* @param int $generationId Generation ID.
|
||||
* @param int $stepId Step ID.
|
||||
*
|
||||
* @return Request[] Array of existing Request objects.
|
||||
*/
|
||||
public static function getByGeneration(int $generationId, int $stepId): array
|
||||
{
|
||||
@@ -356,6 +408,13 @@ class Request
|
||||
return self::getExists($filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request by its hash, or null if not found.
|
||||
*
|
||||
* @param string $hash Request hash.
|
||||
*
|
||||
* @return Request|null The found Request object or null.
|
||||
*/
|
||||
public static function getByHash(string $hash): ?self
|
||||
{
|
||||
$filter =
|
||||
@@ -368,6 +427,13 @@ class Request
|
||||
return array_shift($exists);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the request by its database ID, or null if not found.
|
||||
*
|
||||
* @param int $id Request ID.
|
||||
*
|
||||
* @return Request|null The found Request object or null.
|
||||
*/
|
||||
public static function getById(int $id): ?self
|
||||
{
|
||||
$filter =
|
||||
@@ -381,8 +447,11 @@ class Request
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Filter\ConditionTree $filter - ORM filter object
|
||||
* @return Request[]
|
||||
* Returns all existing requests matching the given ORM filter.
|
||||
*
|
||||
* @param Filter\ConditionTree $filter ORM filter object.
|
||||
*
|
||||
* @return Request[] Array of Request objects.
|
||||
*/
|
||||
private static function getExists(Filter\ConditionTree $filter): array
|
||||
{
|
||||
@@ -431,6 +500,13 @@ class Request
|
||||
return $exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the Request object from an EO_Requests entity.
|
||||
*
|
||||
* @param EO_Requests $request The ORM entity object.
|
||||
*
|
||||
* @return self The initialized Request object.
|
||||
*/
|
||||
private function initByEntity(EO_Requests $request): self
|
||||
{
|
||||
$this->id = $request->getId();
|
||||
@@ -477,6 +553,11 @@ class Request
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the request has exceeded the maximum expected time without a result or error.
|
||||
*
|
||||
* @return bool True if time is over, false otherwise.
|
||||
*/
|
||||
private function isTimeIsOver(): bool
|
||||
{
|
||||
if (
|
||||
|
||||
@@ -3,6 +3,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Bitrix\Landing\Copilot\Generation\Scenario;
|
||||
|
||||
use Bitrix\Landing\Metrika;
|
||||
|
||||
abstract class BaseScenario implements IScenario
|
||||
{
|
||||
/**
|
||||
@@ -12,6 +14,13 @@ abstract class BaseScenario implements IScenario
|
||||
*/
|
||||
abstract public function getQuotaCalculateStep(): int;
|
||||
|
||||
abstract public function getAnalyticCategory(): Metrika\Categories;
|
||||
|
||||
public function getAsyncRelations(): ?array
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
||||
@@ -7,6 +7,7 @@ use Bitrix\Landing\Copilot\Connector;
|
||||
use Bitrix\Landing\Copilot\Connector\Chat\ICopilotChatBot;
|
||||
use Bitrix\Landing\Copilot\Generation;
|
||||
use Bitrix\Landing\Copilot\Generation\Step;
|
||||
use Bitrix\Landing\Metrika;
|
||||
|
||||
class ChangeBlock extends BaseScenario
|
||||
{
|
||||
@@ -45,6 +46,20 @@ class ChangeBlock extends BaseScenario
|
||||
return 10;
|
||||
}
|
||||
|
||||
public function getAnalyticCategory(): Metrika\Categories
|
||||
{
|
||||
return Metrika\Categories::BlockEdition;
|
||||
}
|
||||
|
||||
public function getAsyncRelations(): ?array
|
||||
{
|
||||
return [
|
||||
30 => [
|
||||
40,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
||||
@@ -47,6 +47,11 @@ class CreateSite extends BaseScenario
|
||||
return 20;
|
||||
}
|
||||
|
||||
public function getAnalyticCategory(): Metrika\Categories
|
||||
{
|
||||
return Metrika\Categories::SiteGeneration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Bitrix\Landing\Copilot\Generation\Scenario;
|
||||
use Bitrix\Landing\Copilot\Connector\Chat\ICopilotChatBot;
|
||||
use Bitrix\Landing\Copilot\Generation\Step\IStep;
|
||||
use Bitrix\Landing\Copilot\Generation;
|
||||
use Bitrix\Landing\Metrika;
|
||||
|
||||
interface IScenario
|
||||
{
|
||||
@@ -21,6 +22,15 @@ interface IScenario
|
||||
*/
|
||||
public function getChatbot(): ?ICopilotChatBot;
|
||||
|
||||
public function getAnalyticCategory(): Metrika\Categories;
|
||||
|
||||
/**
|
||||
* If some steps must be run only after async step - need set relations
|
||||
* f.e. [asyncStepId => [dependent_step_ids]]
|
||||
* @return array|null
|
||||
*/
|
||||
public function getAsyncRelations(): ?array;
|
||||
|
||||
/**
|
||||
* Get ID of first step in scenario
|
||||
* @return int|null - if null - has no steps in map
|
||||
|
||||
@@ -3,19 +3,26 @@ declare(strict_types=1);
|
||||
|
||||
namespace Bitrix\Landing\Copilot\Generation\Scenario;
|
||||
|
||||
use Bitrix\AI\Limiter\Enums\TypeLimit;
|
||||
use Bitrix\Landing\Copilot\Connector;
|
||||
use Bitrix\Landing\Copilot\Connector\AI\RequestLimiter;
|
||||
use Bitrix\Landing\Copilot\Connector\Chat\ChatBotMessageDto;
|
||||
use Bitrix\Landing\Copilot\Generation;
|
||||
use Bitrix\Landing\Copilot\Generation\GenerationException;
|
||||
use Bitrix\Landing\Copilot\Generation\Type\GenerationErrors;
|
||||
use Bitrix\Landing\Copilot\Generation\GenerationException;
|
||||
use Bitrix\Landing\Copilot\Generation\Type\RequestQuotaDto;
|
||||
use Bitrix\Landing\Copilot\Generation\Type\ScenarioStepDto;
|
||||
use Bitrix\Landing\Copilot\Generation\Type\StepStatuses;
|
||||
use Bitrix\Landing\Copilot\Model\StepsTable;
|
||||
use Bitrix\Landing\Metrika;
|
||||
use Bitrix\Main\Loader;
|
||||
|
||||
/**
|
||||
* Class Scenarist
|
||||
*
|
||||
* Manages the execution flow of a scenario consisting of multiple steps for AI-powered content generation.
|
||||
* Handles step status tracking, error handling, quota checks, analytics, and callback hooks for step changes and
|
||||
* completion.
|
||||
*/
|
||||
class Scenarist
|
||||
{
|
||||
private const EVENT_STEP = 'onExecuteStep';
|
||||
@@ -26,27 +33,33 @@ class Scenarist
|
||||
private ?int $stepId;
|
||||
|
||||
/**
|
||||
* @var ScenarioStepDto[]
|
||||
* @var ScenarioStepDto[] Array of scenario steps indexed by step ID.
|
||||
*/
|
||||
private array $steps;
|
||||
|
||||
/**
|
||||
* @var callable - call when stepId changed and must be saved
|
||||
* @var callable|null Callback invoked when the step ID changes and must be saved.
|
||||
*/
|
||||
private $onStepChangeCallback;
|
||||
|
||||
/**
|
||||
* @var callable - call when site data changed and must be saved
|
||||
* @var callable|null Callback invoked when site data changes and must be saved.
|
||||
*/
|
||||
private $onSaveCallback;
|
||||
|
||||
/**
|
||||
* @var callable - call when scenario finished
|
||||
* @var callable|null Callback invoked when the scenario is finished.
|
||||
*/
|
||||
private $onFinishCallback;
|
||||
|
||||
private RequestLimiter $requestLimiter;
|
||||
|
||||
/**
|
||||
* Scenarist constructor.
|
||||
*
|
||||
* @param IScenario $scenario The scenario instance to execute.
|
||||
* @param Generation $generation The generation context.
|
||||
*/
|
||||
public function __construct(IScenario $scenario, Generation $generation)
|
||||
{
|
||||
$this->scenario = $scenario;
|
||||
@@ -57,6 +70,11 @@ class Scenarist
|
||||
$this->initSteps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the steps array from the scenario map and loads persisted step statuses.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function initSteps(): void
|
||||
{
|
||||
foreach ($this->scenario->getMap() as $stepId => $step)
|
||||
@@ -88,14 +106,20 @@ class Scenarist
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current step ID.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getStep(): ?int
|
||||
{
|
||||
return $this->stepId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if scenario execute all steps
|
||||
* @return bool
|
||||
* Checks if all scenario steps are finished.
|
||||
*
|
||||
* @return bool True if all steps are finished, false otherwise.
|
||||
*/
|
||||
public function isFinished(): bool
|
||||
{
|
||||
@@ -111,7 +135,8 @@ class Scenarist
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all scenario steps as completed
|
||||
* Marks all scenario steps as finished.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function finish(): void
|
||||
@@ -126,8 +151,9 @@ class Scenarist
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if at least one scenario step has error and not execute
|
||||
* @return bool
|
||||
* Checks if at least one scenario step has an error and is not executed.
|
||||
*
|
||||
* @return bool True if any step is in error state, false otherwise.
|
||||
*/
|
||||
public function isError(): bool
|
||||
{
|
||||
@@ -143,7 +169,8 @@ class Scenarist
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare scenario to restart generation after error.
|
||||
* Prepares the scenario to restart generation after an error by clearing errors in all steps.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearErrors(): void
|
||||
@@ -162,6 +189,13 @@ class Scenarist
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a callback to be called when the step ID changes.
|
||||
*
|
||||
* @param callable $callback
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function onStepChange(callable $callback): self
|
||||
{
|
||||
$this->onStepChangeCallback = $callback;
|
||||
@@ -169,6 +203,11 @@ class Scenarist
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the step change callback if set.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function callOnStepChange(): void
|
||||
{
|
||||
if (isset($this->onStepChangeCallback) && is_int($this->stepId))
|
||||
@@ -177,6 +216,13 @@ class Scenarist
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a callback to be called when the scenario state should be saved.
|
||||
*
|
||||
* @param callable $callback
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function onSave(callable $callback): self
|
||||
{
|
||||
$this->onSaveCallback = $callback;
|
||||
@@ -184,6 +230,11 @@ class Scenarist
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the save callback if set.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function callOnSave(): void
|
||||
{
|
||||
if (isset($this->onSaveCallback))
|
||||
@@ -192,6 +243,13 @@ class Scenarist
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a callback to be called when the scenario is finished.
|
||||
*
|
||||
* @param callable $callback
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function onFinish(callable $callback): self
|
||||
{
|
||||
$this->onFinishCallback = $callback;
|
||||
@@ -199,6 +257,11 @@ class Scenarist
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the finish callback if set.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function callOnFinish(): void
|
||||
{
|
||||
if (isset($this->onFinishCallback))
|
||||
@@ -208,9 +271,11 @@ class Scenarist
|
||||
}
|
||||
|
||||
/**
|
||||
* Run scenario
|
||||
* Executes the scenario, running steps in order and handling errors, quotas, and analytics.
|
||||
*
|
||||
* @return void
|
||||
* @throws GenerationException
|
||||
*
|
||||
* @throws GenerationException If a step fails or a quota is exceeded.
|
||||
*/
|
||||
public function execute(): void
|
||||
{
|
||||
@@ -225,6 +290,11 @@ class Scenarist
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->stepId === $this->scenario->getFirstStepId())
|
||||
{
|
||||
$this->sendMetrikaStart();
|
||||
}
|
||||
|
||||
foreach ($this->steps as $stepId => $step)
|
||||
{
|
||||
if ($stepId > $this->stepId)
|
||||
@@ -232,10 +302,7 @@ class Scenarist
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
$step->status === StepStatuses::Finished
|
||||
|| $step->status === StepStatuses::Error
|
||||
)
|
||||
if (!$this->isNeedExecuteStep($step))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -251,14 +318,11 @@ class Scenarist
|
||||
}
|
||||
|
||||
if (
|
||||
$this->stepId === $stepId
|
||||
&& (
|
||||
$step->step->isFinished()
|
||||
|| $step->step->isAsync()
|
||||
)
|
||||
$step->step->isFinished()
|
||||
|| $step->step->isAsync()
|
||||
)
|
||||
{
|
||||
$this->stepId = $this->scenario->getNextStepId($this->stepId);
|
||||
$this->stepId = $this->getNextStep($stepId);
|
||||
if (!$this->stepId)
|
||||
{
|
||||
break;
|
||||
@@ -276,11 +340,69 @@ class Scenarist
|
||||
}
|
||||
}
|
||||
|
||||
private function isNeedExecuteStep(ScenarioStepDto $step): bool
|
||||
{
|
||||
if (
|
||||
$step->status === StepStatuses::Finished
|
||||
|| $step->status === StepStatuses::Error
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$relations = $this->scenario->getAsyncRelations();
|
||||
if ($relations)
|
||||
{
|
||||
foreach ($relations as $parent => $dependents)
|
||||
{
|
||||
if (
|
||||
in_array($step->stepId, $dependents, true)
|
||||
&& !$this->steps[$parent]?->step->isFinished()
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function getNextStep(int $stepId): ?int
|
||||
{
|
||||
$scenario = $this->scenario;
|
||||
$recursiveGetNext = function ($currentId) use ($scenario, &$recursiveGetNext)
|
||||
{
|
||||
$nextId = $scenario->getNextStepId($currentId);
|
||||
$relations = $scenario->getAsyncRelations();
|
||||
if ($relations)
|
||||
{
|
||||
foreach ($relations as $parent => $dependents)
|
||||
{
|
||||
if (
|
||||
in_array($nextId, $dependents, true)
|
||||
&& !$this->steps[$parent]?->step->isFinished()
|
||||
)
|
||||
{
|
||||
return $recursiveGetNext($nextId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $nextId;
|
||||
};
|
||||
|
||||
return $recursiveGetNext($stepId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find current step and run them
|
||||
* @param ScenarioStepDto $step
|
||||
* Executes a single scenario step, handling quota checks and status updates.
|
||||
*
|
||||
* @param ScenarioStepDto $step The step to execute.
|
||||
*
|
||||
* @return void
|
||||
* @throws GenerationException
|
||||
*
|
||||
* @throws GenerationException If the step fails or a quota is exceeded.
|
||||
*/
|
||||
private function executeStep(ScenarioStepDto $step): void
|
||||
{
|
||||
@@ -289,9 +411,22 @@ class Scenarist
|
||||
if (
|
||||
!$step->step->isStarted()
|
||||
&& $step->stepId === $this->scenario->getQuotaCalculateStep()
|
||||
&& $this->checkRequestQuotas()
|
||||
)
|
||||
{
|
||||
$this->checkRequestQuotas();
|
||||
$requestLimiter = $this->getRequestLimiter();
|
||||
$message = $requestLimiter->getCheckResultMessage();
|
||||
if (is_string($message))
|
||||
{
|
||||
throw new GenerationException(
|
||||
GenerationErrors::requestQuotaExceeded,
|
||||
$message,
|
||||
[
|
||||
'errorText' => $message,
|
||||
'metrikaStatus' => $requestLimiter->getCheckResult()?->getMetrikaStatus(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$step->step->execute();
|
||||
@@ -304,6 +439,10 @@ class Scenarist
|
||||
if ($step->step->isFinished())
|
||||
{
|
||||
$this->saveStepStatus($step, StepStatuses::Finished);
|
||||
if ($step->step->isChanged())
|
||||
{
|
||||
$this->sendMetrikaStepSuccess($step);
|
||||
}
|
||||
}
|
||||
|
||||
if ($step->step->isChanged() || $step->step->isFinished())
|
||||
@@ -314,6 +453,14 @@ class Scenarist
|
||||
$this->generation->getEvent()->send(self::EVENT_STEP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the status of a scenario step to the database.
|
||||
*
|
||||
* @param ScenarioStepDto $step The step whose status is being saved.
|
||||
* @param StepStatuses $status The new status.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function saveStepStatus(ScenarioStepDto $step, StepStatuses $status): void
|
||||
{
|
||||
$step->status = $status;
|
||||
@@ -341,21 +488,15 @@ class Scenarist
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws GenerationException
|
||||
* Checks if the request quota for the scenario is exceeded.
|
||||
*
|
||||
* @return bool True if the request quota is exceeded, false otherwise.
|
||||
*/
|
||||
private function checkRequestQuotas(): void
|
||||
private function checkRequestQuotas(): bool
|
||||
{
|
||||
$quotaLimitText = $this->getQuotaLimitText();
|
||||
if (is_string($quotaLimitText))
|
||||
if (!Loader::includeModule('ai'))
|
||||
{
|
||||
throw new GenerationException(
|
||||
GenerationErrors::requestQuotaExceeded,
|
||||
$quotaLimitText,
|
||||
[
|
||||
'errorText' => $quotaLimitText,
|
||||
]
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -363,37 +504,30 @@ class Scenarist
|
||||
|| $this->generation->getChatId() <= 0
|
||||
)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->generation->getScenario()?->getChatbot()?->onRequestQuotaOk(
|
||||
new ChatBotMessageDto(
|
||||
$this->generation->getChatId(),
|
||||
$this->generation->getId(),
|
||||
)
|
||||
);
|
||||
}
|
||||
$requestLimiter = $this->getRequestLimiter();
|
||||
|
||||
private function getQuotaLimitText(): ?string
|
||||
{
|
||||
if (!Loader::includeModule('ai'))
|
||||
$isRequestQuotaExceeded = true;
|
||||
if (!$requestLimiter->checkQuota($this->getRequestQuotasSum()))
|
||||
{
|
||||
return null;
|
||||
$isRequestQuotaExceeded = false;
|
||||
$this->generation->getScenario()?->getChatbot()?->onRequestQuotaOk(
|
||||
new ChatBotMessageDto(
|
||||
$this->generation->getChatId(),
|
||||
$this->generation->getId(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$requestCount = $this->getRequestQuotasSum();
|
||||
if ($requestCount <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getRequestLimiter()->getTextFromCheckLimit($requestCount);
|
||||
return $isRequestQuotaExceeded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return sum of request limits by all steps
|
||||
* Returns an array of request quotas for all steps in the scenario.
|
||||
*
|
||||
* @return RequestQuotaDto[]
|
||||
* @return RequestQuotaDto[] Array of request quota DTOs.
|
||||
*/
|
||||
private function getRequestQuotas(): array
|
||||
{
|
||||
@@ -423,8 +557,9 @@ class Scenarist
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sum of all request quotas, ignore types
|
||||
* @return int
|
||||
* Returns the sum of all request quotas, ignoring types.
|
||||
*
|
||||
* @return int Total request count.
|
||||
*/
|
||||
private function getRequestQuotasSum(): int
|
||||
{
|
||||
@@ -440,15 +575,43 @@ class Scenarist
|
||||
/**
|
||||
* Retrieves the RequestLimiter instance, initializing it if not already set.
|
||||
*
|
||||
* @return RequestLimiter The RequestLimiter instance.
|
||||
* @return RequestLimiter
|
||||
*/
|
||||
private function getRequestLimiter(): RequestLimiter
|
||||
{
|
||||
if (empty($this->requestLimiter))
|
||||
if (!isset($this->requestLimiter))
|
||||
{
|
||||
$this->requestLimiter = new RequestLimiter();
|
||||
}
|
||||
|
||||
return $this->requestLimiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a Metrika analytics event for the start of the scenario.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function sendMetrikaStart(): void
|
||||
{
|
||||
$metrika = $this->generation->getMetrika(Metrika\Events::start);
|
||||
$metrika->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a Metrika analytics event for a successful step execution.
|
||||
*
|
||||
* @param ScenarioStepDto $step The step for which to send analytics.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function sendMetrikaStepSuccess(ScenarioStepDto $step): void
|
||||
{
|
||||
$event = $step->step->getAnalyticEvent();
|
||||
if (isset($event))
|
||||
{
|
||||
$metrika = $this->generation->getMetrika($event);
|
||||
$metrika->send();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use Bitrix\Landing\Copilot\Data;
|
||||
use Bitrix\Landing\Copilot\Generation;
|
||||
use Bitrix\Landing\Copilot\Generation\GenerationException;
|
||||
use Bitrix\Landing\Copilot\Generation\Type\GenerationErrors;
|
||||
use Bitrix\Landing\Metrika;
|
||||
|
||||
abstract class BaseStep implements IStep
|
||||
{
|
||||
@@ -98,4 +99,14 @@ abstract class BaseStep implements IStep
|
||||
{
|
||||
return $this->generation->getEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Step may, or not may send analytic event.
|
||||
* The Scenarist decides when to send the event.
|
||||
* @return Metrika\Events|null
|
||||
*/
|
||||
public function getAnalyticEvent(): ?Metrika\Events
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use Bitrix\Landing\Copilot\Data\Site;
|
||||
use Bitrix\Landing\Copilot\Generation;
|
||||
use Bitrix\Landing\Copilot\Generation\GenerationException;
|
||||
use Bitrix\Landing\Copilot\Generation\Type\RequestQuotaDto;
|
||||
use Bitrix\Landing\Metrika;
|
||||
|
||||
interface IStep
|
||||
{
|
||||
@@ -31,7 +32,6 @@ interface IStep
|
||||
*/
|
||||
public function isAsync(): bool;
|
||||
|
||||
|
||||
/**
|
||||
* Check if step was start executing
|
||||
* @return bool
|
||||
@@ -64,4 +64,11 @@ interface IStep
|
||||
* @return RequestQuotaDto|null
|
||||
*/
|
||||
public static function getRequestQuota(Site $siteData): ?RequestQuotaDto;
|
||||
|
||||
/**
|
||||
* Step may, or not may send analytic event.
|
||||
* The Scenarist decides when to send the event.
|
||||
* @return Metrika\Events|null
|
||||
*/
|
||||
public function getAnalyticEvent(): ?Metrika\Events;
|
||||
}
|
||||
@@ -6,7 +6,6 @@ namespace Bitrix\Landing\Copilot\Generation\Step;
|
||||
use Bitrix\Landing\Copilot\Connector\AI;
|
||||
use Bitrix\Landing\Copilot\Connector\AI\Prompt;
|
||||
use Bitrix\Landing\Copilot\Converter;
|
||||
use Bitrix\Landing\Copilot\Data\Block\Operator;
|
||||
use Bitrix\Landing\Copilot\Data\Type\NodeType;
|
||||
use Bitrix\Landing\Copilot\Generation\Error;
|
||||
use Bitrix\Landing\Copilot\Generation\GenerationException;
|
||||
@@ -15,6 +14,7 @@ use Bitrix\Landing\Copilot\Generation\Type\Errors;
|
||||
use Bitrix\Landing\Copilot\Generation\Type\GenerationErrors;
|
||||
use Bitrix\Landing\Copilot\Generation\Type\RequestQuotaDto;
|
||||
use Bitrix\Landing\Copilot\Data\Site;
|
||||
use Bitrix\Landing\Metrika;
|
||||
use Bitrix\Landing;
|
||||
use Bitrix\Landing\Rights;
|
||||
|
||||
@@ -45,6 +45,11 @@ class RequestBlockContent extends RequestSingle
|
||||
return new RequestQuotaDto(self::getConnectorClass(), 1);
|
||||
}
|
||||
|
||||
public function getAnalyticEvent(): ?Metrika\Events
|
||||
{
|
||||
return Metrika\Events::textsGeneration;
|
||||
}
|
||||
|
||||
protected function getPrompt(): Prompt
|
||||
{
|
||||
$prompt = new Prompt('landing_ai_block_content');
|
||||
|
||||
@@ -14,6 +14,7 @@ use Bitrix\Landing\Copilot\Generation\Type\RequestEntityDto;
|
||||
use Bitrix\Landing\Copilot\Generation\Type\RequestQuotaDto;
|
||||
use Bitrix\Landing\Copilot\Generation\Type\RequestEntities;
|
||||
use Bitrix\Landing\Copilot\Model\RequestToEntitiesTable;
|
||||
use Bitrix\Landing\Metrika;
|
||||
use Bitrix\Main\ORM\Query\Query;
|
||||
|
||||
class RequestImages extends RequestMultiple
|
||||
@@ -48,6 +49,11 @@ class RequestImages extends RequestMultiple
|
||||
);
|
||||
}
|
||||
|
||||
public function getAnalyticEvent(): ?Metrika\Events
|
||||
{
|
||||
return Metrika\Events::imagesGeneration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get not avatar images count, check all blocks and all nodes
|
||||
*
|
||||
|
||||
@@ -81,10 +81,7 @@ abstract class RequestMultiple extends AIStep
|
||||
{
|
||||
foreach ($this->getEntitiesToRequest() as $entity)
|
||||
{
|
||||
if (
|
||||
isset($entity->requestId)
|
||||
&& isset($this->requests[$entity->requestId])
|
||||
)
|
||||
if (isset($entity->requestId, $this->requests[$entity->requestId]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use Bitrix\Landing\Copilot\Generation\PromptGenerator;
|
||||
use Bitrix\Landing\Copilot\Generation\PromptTemplateProvider;
|
||||
use Bitrix\Landing\Copilot\Generation\Type\GenerationErrors;
|
||||
use Bitrix\Landing\Copilot\Generation\Type\RequestQuotaDto;
|
||||
use Bitrix\Landing\Metrika;
|
||||
use Bitrix\Main\Web;
|
||||
use Exception;
|
||||
|
||||
@@ -44,6 +45,11 @@ class RequestSiteContent extends RequestSingle
|
||||
return new RequestQuotaDto(self::getConnectorClass(), 1);
|
||||
}
|
||||
|
||||
public function getAnalyticEvent(): ?Metrika\Events
|
||||
{
|
||||
return Metrika\Events::textsGeneration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
@@ -206,7 +212,7 @@ class RequestSiteContent extends RequestSingle
|
||||
|
||||
if ($countPromptTexts !== $countPlaceholders)
|
||||
{
|
||||
$diffCount = $countPlaceholders - $countPromptTexts;
|
||||
$diffCount = $countPlaceholders - $countPromptTexts;
|
||||
if ($diffCount > 0)
|
||||
{
|
||||
for ($i = 0; $i < $diffCount; $i++)
|
||||
|
||||
@@ -12,6 +12,7 @@ use Bitrix\Landing\Copilot\Generation\Markers;
|
||||
use Bitrix\Landing\Copilot\Generation\Type\Errors;
|
||||
use Bitrix\Landing\Copilot\Generation\Type\GenerationErrors;
|
||||
use Bitrix\Landing\Copilot\Generation\Type\RequestQuotaDto;
|
||||
use Bitrix\Landing\Metrika;
|
||||
|
||||
class RequestSiteData extends RequestSingle
|
||||
{
|
||||
@@ -40,6 +41,11 @@ class RequestSiteData extends RequestSingle
|
||||
return new RequestQuotaDto(self::getConnectorClass(), 1);
|
||||
}
|
||||
|
||||
public function getAnalyticEvent(): ?Metrika\Events
|
||||
{
|
||||
return Metrika\Events::dataGeneration;
|
||||
}
|
||||
|
||||
protected function getPrompt(): Prompt
|
||||
{
|
||||
$prompt = new Prompt('landing_ai_data');
|
||||
|
||||
@@ -10,4 +10,5 @@ enum Errors: string
|
||||
case requestError = 'REQUEST_ERROR';
|
||||
case requestInvalid = 'REQUEST_INVALID';
|
||||
case requestNotAllowed = 'REQUEST_NOT_ALLOWED';
|
||||
case requestTimeout = 'REQUEST_TIMEOUT';
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Bitrix\Landing\Mainpage;
|
||||
use Bitrix\AI\Integration;
|
||||
use Bitrix\Landing;
|
||||
use Bitrix\Landing\Site\Type;
|
||||
use Bitrix\Landing\Metrika;
|
||||
use Bitrix\Main\Loader;
|
||||
use Bitrix\Rest\AppTable;
|
||||
use Bitrix\Rest\Configuration\Action\Import;
|
||||
@@ -57,6 +58,12 @@ class Installer
|
||||
return null;
|
||||
}
|
||||
|
||||
$metrika = new Metrika\Metrika(
|
||||
Metrika\Categories::Vibe,
|
||||
Metrika\Events::createTemplateApi,
|
||||
);
|
||||
$metrika->setParam(1, 'templateCode', $code->value);
|
||||
|
||||
$app = AppTable::getByClientId($appCode);
|
||||
$isAppInstalled =
|
||||
!empty($app['ACTIVE'])
|
||||
@@ -72,6 +79,8 @@ class Installer
|
||||
|| isset($installResult['error'])
|
||||
)
|
||||
{
|
||||
$metrika->setError($installResult['error'], Metrika\Statuses::ErrorMarket)->send();
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -83,6 +92,8 @@ class Installer
|
||||
$zipId = (int)($appSite['ITEMS'][0]['ID'] ?? 0);
|
||||
if ($zipId <= 0)
|
||||
{
|
||||
$metrika->setError('Wrong_zip_id', Metrika\Statuses::ErrorMarket)->send();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -138,6 +149,8 @@ class Installer
|
||||
}
|
||||
}
|
||||
|
||||
$metrika->send();
|
||||
|
||||
return $newLandingId;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ use Bitrix\Main\Application;
|
||||
|
||||
enum TemplateRegions: string
|
||||
{
|
||||
//Templates::Enterprise
|
||||
case EnterpriseWestAr = 'alaio.vibe_enterprise_west_ar';
|
||||
case EnterpriseWestBr = 'alaio.vibe_enterprise_west_br';
|
||||
case EnterpriseWestDe = 'alaio.vibe_enterprise_west_de';
|
||||
@@ -27,58 +28,299 @@ enum TemplateRegions: string
|
||||
case EnterpriseChineseEn = 'alaio.vibe_enterprise_chinese_en';
|
||||
case EnterpriseChineseSc = 'alaio.vibe_enterprise_chinese_sc';
|
||||
case EnterpriseChineseTc = 'alaio.vibe_enterprise_chinese_tc';
|
||||
//for zones 'ru', 'by', 'kz'
|
||||
//for zones 'ru', 'kz'
|
||||
case EnterpriseRu = 'bitrix.vibe_enterprise_ru';
|
||||
//for zone 'by'
|
||||
case EnterpriseBy = 'bitrix.vibe_enterprise_by';
|
||||
//Templates::Automation
|
||||
case AutomationRu = 'bitrix.vibe_automation_ru';
|
||||
case AutomationEn = 'alaio.vibe_automation_en';
|
||||
case AutomationDe = 'alaio.vibe_automation_de';
|
||||
case AutomationLa = 'alaio.vibe_automation_la';
|
||||
case AutomationBr = 'alaio.vibe_automation_br';
|
||||
case AutomationFr = 'alaio.vibe_automation_fr';
|
||||
case AutomationPl = 'alaio.vibe_automation_pl';
|
||||
case AutomationIt = 'alaio.vibe_automation_it';
|
||||
case AutomationTr = 'alaio.vibe_automation_tr';
|
||||
case AutomationJa = 'alaio.vibe_automation_ja';
|
||||
case AutomationVn = 'alaio.vibe_automation_vn';
|
||||
case AutomationAr = 'alaio.vibe_automation_ar';
|
||||
case AutomationId = 'alaio.vibe_automation_id';
|
||||
case AutomationKz = 'alaio.vibe_automation_kz';
|
||||
case AutomationMs = 'alaio.vibe_automation_ms';
|
||||
case AutomationTh = 'alaio.vibe_automation_th';
|
||||
case AutomationChineseSc = 'alaio.vibe_automation_chinese_sc';
|
||||
case AutomationChineseTc = 'alaio.vibe_automation_chinese_tc';
|
||||
case AutomationChineseEn = 'alaio.vibe_automation_chinese_en';
|
||||
//Templates::Collaboration
|
||||
case CollaborationRu = 'bitrix.vibe_collaboration_ru';
|
||||
case CollaborationEn = 'alaio.vibe_collaboration_en';
|
||||
case CollaborationDe = 'alaio.vibe_collaboration_de';
|
||||
case CollaborationLa = 'alaio.vibe_collaboration_la';
|
||||
case CollaborationBr = 'alaio.vibe_collaboration_br';
|
||||
case CollaborationFr = 'alaio.vibe_collaboration_fr';
|
||||
case CollaborationPl = 'alaio.vibe_collaboration_pl';
|
||||
case CollaborationIt = 'alaio.vibe_collaboration_it';
|
||||
case CollaborationTr = 'alaio.vibe_collaboration_tr';
|
||||
case CollaborationJa = 'alaio.vibe_collaboration_ja';
|
||||
case CollaborationVn = 'alaio.vibe_collaboration_vn';
|
||||
case CollaborationAr = 'alaio.vibe_collaboration_ar';
|
||||
case CollaborationId = 'alaio.vibe_collaboration_id';
|
||||
case CollaborationKz = 'alaio.vibe_collaboration_kz';
|
||||
case CollaborationMs = 'alaio.vibe_collaboration_ms';
|
||||
case CollaborationTh = 'alaio.vibe_collaboration_th';
|
||||
case CollaborationChineseSc = 'alaio.vibe_collaboration_chinese_sc';
|
||||
case CollaborationChineseTc = 'alaio.vibe_collaboration_chinese_tc';
|
||||
case CollaborationChineseEn = 'alaio.vibe_collaboration_chinese_en';
|
||||
//Templates::Boards
|
||||
case BoardsRu = 'bitrix.vibe_boards_ru';
|
||||
case BoardsEn = 'alaio.vibe_boards_en';
|
||||
case BoardsDe = 'alaio.vibe_boards_de';
|
||||
case BoardsLa = 'alaio.vibe_boards_la';
|
||||
case BoardsBr = 'alaio.vibe_boards_br';
|
||||
case BoardsFr = 'alaio.vibe_boards_fr';
|
||||
case BoardsPl = 'alaio.vibe_boards_pl';
|
||||
case BoardsIt = 'alaio.vibe_boards_it';
|
||||
case BoardsTr = 'alaio.vibe_boards_tr';
|
||||
case BoardsJa = 'alaio.vibe_boards_ja';
|
||||
case BoardsVn = 'alaio.vibe_boards_vn';
|
||||
case BoardsAr = 'alaio.vibe_boards_ar';
|
||||
case BoardsId = 'alaio.vibe_boards_id';
|
||||
case BoardsKz = 'alaio.vibe_boards_kz';
|
||||
case BoardsMs = 'alaio.vibe_boards_ms';
|
||||
case BoardsTh = 'alaio.vibe_boards_th';
|
||||
case BoardsChineseSc = 'alaio.vibe_boards_chinese_sc';
|
||||
case BoardsChineseTc = 'alaio.vibe_boards_chinese_tc';
|
||||
case BoardsChineseEn = 'alaio.vibe_boards_chinese_en';
|
||||
//Templates::Booking
|
||||
case BookingRu = 'bitrix.vibe_booking_ru';
|
||||
case BookingEn = 'alaio.vibe_booking_en';
|
||||
case BookingDe = 'alaio.vibe_booking_de';
|
||||
case BookingLa = 'alaio.vibe_booking_la';
|
||||
case BookingBr = 'alaio.vibe_booking_br';
|
||||
case BookingFr = 'alaio.vibe_booking_fr';
|
||||
case BookingPl = 'alaio.vibe_booking_pl';
|
||||
case BookingIt = 'alaio.vibe_booking_it';
|
||||
case BookingTr = 'alaio.vibe_booking_tr';
|
||||
case BookingJa = 'alaio.vibe_booking_ja';
|
||||
case BookingVn = 'alaio.vibe_booking_vn';
|
||||
case BookingAr = 'alaio.vibe_booking_ar';
|
||||
case BookingId = 'alaio.vibe_booking_id';
|
||||
case BookingKz = 'alaio.vibe_booking_kz';
|
||||
case BookingMs = 'alaio.vibe_booking_ms';
|
||||
case BookingTh = 'alaio.vibe_booking_th';
|
||||
case BookingChineseSc = 'alaio.vibe_booking_chinese_sc';
|
||||
case BookingChineseTc = 'alaio.vibe_booking_chinese_tc';
|
||||
case BookingChineseEn = 'alaio.vibe_booking_chinese_en';
|
||||
|
||||
private const CIS_ZONES = ['ru', 'kz', 'by', 'uz'];
|
||||
private const DEFAULT_CIS_ZONE = 'ru';
|
||||
private const CHINESE_ZONES = ['cn', 'tc', 'sc'];
|
||||
private const CONFIG_SECTION_CIS = 'cis';
|
||||
private const CONFIG_SECTION_CHINESE = 'chinese';
|
||||
private const CONFIG_SECTION_WEST = 'west';
|
||||
private const DEFAULT_LANG = 'en';
|
||||
|
||||
/**
|
||||
* Attempt to resolve template based on the provided code.
|
||||
*
|
||||
* @param Templates $code
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public static function resolve(Templates $code): ?string
|
||||
{
|
||||
$regionCode = null;
|
||||
return self::getTemplateCode($code->value)?->value;
|
||||
}
|
||||
|
||||
switch ($code)
|
||||
/**
|
||||
* Get template code for specified template section.
|
||||
*
|
||||
* @param string $code
|
||||
*
|
||||
* @return self|null
|
||||
*/
|
||||
private static function getTemplateCode(string $code): ?self
|
||||
{
|
||||
$templateConfig = self::getTemplateConfig($code);
|
||||
if (!$templateConfig)
|
||||
{
|
||||
case Templates::Enterprise:
|
||||
$portalZone = \CBitrix24::getPortalZone();
|
||||
$lang = Application::getInstance()->getContext()->getLanguage();
|
||||
if (in_array($portalZone, ['ru', 'by', 'kz', 'uz']))
|
||||
{
|
||||
$regionCode = self::EnterpriseRu;
|
||||
}
|
||||
elseif (in_array($portalZone, ['cn', 'tc', 'sc']))
|
||||
{
|
||||
$regionCodes = [
|
||||
'en' => self::EnterpriseChineseEn,
|
||||
'sc' => self::EnterpriseChineseSc,
|
||||
'tc' => self::EnterpriseChineseTc,
|
||||
];
|
||||
$regionCode = $regionCodes[$lang] ?? self::EnterpriseChineseEn;
|
||||
}
|
||||
else
|
||||
{
|
||||
$regionCodes = [
|
||||
'ar' => self::EnterpriseWestAr,
|
||||
'br' => self::EnterpriseWestBr,
|
||||
'de' => self::EnterpriseWestDe,
|
||||
'en' => self::EnterpriseWestEn,
|
||||
'fr' => self::EnterpriseWestFr,
|
||||
'id' => self::EnterpriseWestId,
|
||||
'it' => self::EnterpriseWestIt,
|
||||
'ja' => self::EnterpriseWestJa,
|
||||
'kz' => self::EnterpriseWestKz,
|
||||
'la' => self::EnterpriseWestLa,
|
||||
'ms' => self::EnterpriseWestMs,
|
||||
'pl' => self::EnterpriseWestPl,
|
||||
'th' => self::EnterpriseWestTh,
|
||||
'tr' => self::EnterpriseWestTr,
|
||||
'vn' => self::EnterpriseWestVn,
|
||||
'ru' => self::EnterpriseWestEn,
|
||||
'ua' => self::EnterpriseWestEn,
|
||||
];
|
||||
$regionCode = $regionCodes[$lang] ?? self::EnterpriseWestEn;
|
||||
}
|
||||
|
||||
break;
|
||||
return null;
|
||||
}
|
||||
|
||||
return $regionCode?->value;
|
||||
$portalZone = \CBitrix24::getPortalZone();
|
||||
|
||||
//is CIS zone
|
||||
if (in_array($portalZone, self::CIS_ZONES, true))
|
||||
{
|
||||
$cis = $templateConfig[self::CONFIG_SECTION_CIS];
|
||||
|
||||
return $cis[$portalZone] ?? $cis[self::DEFAULT_CIS_ZONE];
|
||||
}
|
||||
|
||||
$lang = Application::getInstance()->getContext()->getLanguage();
|
||||
|
||||
//is Chinese zone
|
||||
if (in_array($portalZone, self::CHINESE_ZONES, true))
|
||||
{
|
||||
return $templateConfig[self::CONFIG_SECTION_CHINESE][$lang]
|
||||
?? $templateConfig[self::CONFIG_SECTION_CHINESE][self::DEFAULT_LANG];
|
||||
}
|
||||
|
||||
return $templateConfig[self::CONFIG_SECTION_WEST][$lang]
|
||||
?? $templateConfig[self::CONFIG_SECTION_WEST][self::DEFAULT_LANG];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the configuration for the specified template code.
|
||||
*
|
||||
* @param string $code
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
private static function getTemplateConfig(string $code): ?array
|
||||
{
|
||||
$templatesConfig = [
|
||||
Templates::Enterprise->value => [
|
||||
self::CONFIG_SECTION_CIS => [
|
||||
'by' => self::EnterpriseBy,
|
||||
self::DEFAULT_CIS_ZONE => self::EnterpriseRu,
|
||||
],
|
||||
self::CONFIG_SECTION_CHINESE => [
|
||||
'sc' => self::EnterpriseChineseSc,
|
||||
'tc' => self::EnterpriseChineseTc,
|
||||
self::DEFAULT_LANG => self::EnterpriseChineseEn,
|
||||
],
|
||||
self::CONFIG_SECTION_WEST => [
|
||||
'ar' => self::EnterpriseWestAr,
|
||||
'br' => self::EnterpriseWestBr,
|
||||
'de' => self::EnterpriseWestDe,
|
||||
'fr' => self::EnterpriseWestFr,
|
||||
'id' => self::EnterpriseWestId,
|
||||
'it' => self::EnterpriseWestIt,
|
||||
'ja' => self::EnterpriseWestJa,
|
||||
'kz' => self::EnterpriseWestKz,
|
||||
'la' => self::EnterpriseWestLa,
|
||||
'ms' => self::EnterpriseWestMs,
|
||||
'pl' => self::EnterpriseWestPl,
|
||||
'th' => self::EnterpriseWestTh,
|
||||
'tr' => self::EnterpriseWestTr,
|
||||
'vn' => self::EnterpriseWestVn,
|
||||
'ru' => self::EnterpriseWestEn,
|
||||
'ua' => self::EnterpriseWestEn,
|
||||
self::DEFAULT_LANG => self::EnterpriseWestEn,
|
||||
],
|
||||
],
|
||||
Templates::Automation->value => [
|
||||
self::CONFIG_SECTION_CIS => [
|
||||
self::DEFAULT_CIS_ZONE => self::AutomationRu,
|
||||
],
|
||||
self::CONFIG_SECTION_CHINESE => [
|
||||
'sc' => self::AutomationChineseSc,
|
||||
'tc' => self::AutomationChineseTc,
|
||||
self::DEFAULT_LANG => self::AutomationChineseEn,
|
||||
],
|
||||
self::CONFIG_SECTION_WEST => [
|
||||
'de' => self::AutomationDe,
|
||||
'la' => self::AutomationLa,
|
||||
'br' => self::AutomationBr,
|
||||
'fr' => self::AutomationFr,
|
||||
'pl' => self::AutomationPl,
|
||||
'it' => self::AutomationIt,
|
||||
'tr' => self::AutomationTr,
|
||||
'ja' => self::AutomationJa,
|
||||
'vn' => self::AutomationVn,
|
||||
'ar' => self::AutomationAr,
|
||||
'id' => self::AutomationId,
|
||||
'kz' => self::AutomationKz,
|
||||
'ms' => self::AutomationMs,
|
||||
'th' => self::AutomationTh,
|
||||
self::DEFAULT_LANG => self::AutomationEn,
|
||||
],
|
||||
],
|
||||
Templates::Collaboration->value => [
|
||||
self::CONFIG_SECTION_CIS => [
|
||||
self::DEFAULT_CIS_ZONE => self::CollaborationRu,
|
||||
],
|
||||
self::CONFIG_SECTION_CHINESE => [
|
||||
'sc' => self::CollaborationChineseSc,
|
||||
'tc' => self::CollaborationChineseTc,
|
||||
self::DEFAULT_LANG => self::CollaborationChineseEn,
|
||||
],
|
||||
self::CONFIG_SECTION_WEST => [
|
||||
'de' => self::CollaborationDe,
|
||||
'la' => self::CollaborationLa,
|
||||
'br' => self::CollaborationBr,
|
||||
'fr' => self::CollaborationFr,
|
||||
'pl' => self::CollaborationPl,
|
||||
'it' => self::CollaborationIt,
|
||||
'tr' => self::CollaborationTr,
|
||||
'ja' => self::CollaborationJa,
|
||||
'vn' => self::CollaborationVn,
|
||||
'ar' => self::CollaborationAr,
|
||||
'id' => self::CollaborationId,
|
||||
'kz' => self::CollaborationKz,
|
||||
'ms' => self::CollaborationMs,
|
||||
'th' => self::CollaborationTh,
|
||||
self::DEFAULT_LANG => self::CollaborationEn,
|
||||
],
|
||||
],
|
||||
Templates::Boards->value => [
|
||||
self::CONFIG_SECTION_CIS => [
|
||||
self::DEFAULT_CIS_ZONE => self::BoardsRu,
|
||||
],
|
||||
self::CONFIG_SECTION_CHINESE => [
|
||||
'sc' => self::BoardsChineseSc,
|
||||
'tc' => self::BoardsChineseTc,
|
||||
self::DEFAULT_LANG => self::BoardsChineseEn,
|
||||
],
|
||||
self::CONFIG_SECTION_WEST => [
|
||||
'de' => self::BoardsDe,
|
||||
'la' => self::BoardsLa,
|
||||
'br' => self::BoardsBr,
|
||||
'fr' => self::BoardsFr,
|
||||
'pl' => self::BoardsPl,
|
||||
'it' => self::BoardsIt,
|
||||
'tr' => self::BoardsTr,
|
||||
'ja' => self::BoardsJa,
|
||||
'vn' => self::BoardsVn,
|
||||
'ar' => self::BoardsAr,
|
||||
'id' => self::BoardsId,
|
||||
'kz' => self::BoardsKz,
|
||||
'ms' => self::BoardsMs,
|
||||
'th' => self::BoardsTh,
|
||||
self::DEFAULT_LANG => self::BoardsEn,
|
||||
],
|
||||
],
|
||||
Templates::Booking->value => [
|
||||
self::CONFIG_SECTION_CIS => [
|
||||
self::DEFAULT_CIS_ZONE => self::BookingRu,
|
||||
],
|
||||
self::CONFIG_SECTION_CHINESE => [
|
||||
'sc' => self::BookingChineseSc,
|
||||
'tc' => self::BookingChineseTc,
|
||||
self::DEFAULT_LANG => self::BookingChineseEn,
|
||||
],
|
||||
self::CONFIG_SECTION_WEST => [
|
||||
'de' => self::BookingDe,
|
||||
'la' => self::BookingLa,
|
||||
'br' => self::BookingBr,
|
||||
'fr' => self::BookingFr,
|
||||
'pl' => self::BookingPl,
|
||||
'it' => self::BookingIt,
|
||||
'tr' => self::BookingTr,
|
||||
'ja' => self::BookingJa,
|
||||
'vn' => self::BookingVn,
|
||||
'ar' => self::BookingAr,
|
||||
'id' => self::BookingId,
|
||||
'kz' => self::BookingKz,
|
||||
'ms' => self::BookingMs,
|
||||
'th' => self::BookingTh,
|
||||
self::DEFAULT_LANG => self::BookingEn,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return $templatesConfig[$code] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,4 +7,8 @@ namespace Bitrix\Landing\Mainpage;
|
||||
enum Templates: string
|
||||
{
|
||||
case Enterprise = 'vibe_enterprise';
|
||||
case Automation = 'vibe_automation';
|
||||
case Collaboration = 'vibe_collaboration';
|
||||
case Boards = 'vibe_boards';
|
||||
case Booking = 'vibe_booking';
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ enum Categories: string
|
||||
case CrmForms = 'crm_forms';
|
||||
case ExternalPictureEditor = 'external_picture_editor';
|
||||
case SiteGeneration = 'site_generation';
|
||||
case BlockEdition = 'block_edition';
|
||||
|
||||
public static function getBySiteType(string $siteType): Categories
|
||||
{
|
||||
|
||||
@@ -3,17 +3,22 @@ declare(strict_types=1);
|
||||
|
||||
namespace Bitrix\Landing\Metrika;
|
||||
|
||||
/**
|
||||
* Enum representing all possible Metrika events.
|
||||
*/
|
||||
enum Events: string
|
||||
{
|
||||
case open = 'open';
|
||||
case save = 'save';
|
||||
case cancel = 'cancel';
|
||||
case start = 'start';
|
||||
case select = 'select';
|
||||
case openStartPage = 'open_start_page';
|
||||
case openSettingsMain = 'open_settings_main';
|
||||
case openMarket = 'open_market';
|
||||
case previewTemplate = 'preview_template';
|
||||
case createTemplate = 'create_template';
|
||||
case createTemplateApi = 'create_template_api';
|
||||
case replaceTemplate = 'replace_template';
|
||||
case openEditor = 'open_editor';
|
||||
case publishSite = 'publish_site';
|
||||
@@ -21,4 +26,10 @@ enum Events: string
|
||||
case addWidget = 'add_widget';
|
||||
case deleteWidget = 'delete_widget';
|
||||
case clickOnButton = 'click_on_button';
|
||||
case dataGeneration = 'data_generation';
|
||||
case textsGeneration = 'texts_generation';
|
||||
case imagesGeneration = 'images_generation';
|
||||
case addFavourite = 'add_favourite';
|
||||
case deleteFavourite = 'delete_favourite';
|
||||
case unknown = 'unknown';
|
||||
}
|
||||
|
||||
@@ -11,8 +11,77 @@ class FieldsDto
|
||||
public ?Sections $section = null,
|
||||
public ?string $subSection = null,
|
||||
public ?string $element = null,
|
||||
/**
|
||||
* @var array|null - array of arrays [position, name, value]
|
||||
* F.e.
|
||||
* [
|
||||
* [1, foo, bar],
|
||||
* [2, baz, bat],
|
||||
* ]
|
||||
*/
|
||||
public ?array $params = null,
|
||||
public ?string $error = null,
|
||||
)
|
||||
{}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'event' => $this->event,
|
||||
'type' => $this->type,
|
||||
'section' => $this->section,
|
||||
'subSection' => $this->subSection,
|
||||
'element' => $this->element,
|
||||
'params' => $this->params,
|
||||
'error' => $this->error,
|
||||
];
|
||||
}
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
if (isset($data['event']))
|
||||
{
|
||||
$event = Events::tryFrom($data['event']);
|
||||
}
|
||||
if (isset($data['type']))
|
||||
{
|
||||
$type = Types::tryFrom($data['type']);
|
||||
}
|
||||
if (isset($data['section']))
|
||||
{
|
||||
$section = Sections::tryFrom($data['section']);
|
||||
}
|
||||
$subSection = $data['subSection'] ?? null;
|
||||
$element = $data['element'] ?? null;
|
||||
$params = $data['params'] ?? null;
|
||||
$error = $data['error'] ?? null;
|
||||
|
||||
return new self(
|
||||
$event,
|
||||
$type,
|
||||
$section,
|
||||
$subSection,
|
||||
$element,
|
||||
$params,
|
||||
$error,
|
||||
);
|
||||
}
|
||||
|
||||
public function addFields(?FieldsDto $addFields): self
|
||||
{
|
||||
if ($addFields === null)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->event = $addFields->event ?? $this->event;
|
||||
$this->type = $addFields->type ?? $this->type;
|
||||
$this->section = $addFields->section ?? $this->section;
|
||||
$this->subSection = $addFields->subSection ?? $this->subSection;
|
||||
$this->element = $addFields->element ?? $this->element;
|
||||
$this->params = $addFields->params ?? $this->params;
|
||||
$this->error = $addFields->error ?? $this->error;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bitrix\Landing\Metrika;
|
||||
|
||||
use Bitrix\AI\Tuning;
|
||||
use Bitrix\Landing\Connector\Ai;
|
||||
use Bitrix\Main\Loader;
|
||||
|
||||
/**
|
||||
* Service for managing and setting provider parameters in Metrika analytics events.
|
||||
*
|
||||
* This service encapsulates the logic for mapping Metrika events to AI provider codes,
|
||||
* retrieving provider codes from the Tuning manager, and setting provider-related parameters
|
||||
* in Metrika analytics objects.
|
||||
*/
|
||||
class MetrikaProviderParamService
|
||||
{
|
||||
/**
|
||||
* Position of the provider parameter in the Metrika params array.
|
||||
* @var int
|
||||
*/
|
||||
private const PROVIDER_PARAM_POSITION = 2;
|
||||
|
||||
/**
|
||||
* Name of the provider parameter in Metrika.
|
||||
* @var string
|
||||
*/
|
||||
private const PROVIDER_PARAM_NAME = 'provider';
|
||||
|
||||
/**
|
||||
* Tuning manager instance for retrieving provider codes.
|
||||
* @var Tuning\Manager
|
||||
*/
|
||||
private Tuning\Manager $tuningManager;
|
||||
|
||||
/**
|
||||
* MetrikaProviderParamService constructor.
|
||||
*
|
||||
* @param Tuning\Manager|null $tuningManager Optional tuning manager instance. If not provided, a new instance will
|
||||
* be created.
|
||||
*/
|
||||
public function __construct(?Tuning\Manager $tuningManager = null)
|
||||
{
|
||||
if (Loader::includeModule('ai'))
|
||||
{
|
||||
$this->tuningManager = $tuningManager ?? new Tuning\Manager();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all relevant parameters in the Metrika analytics object for the specified event.
|
||||
* Implements MetrikaParamSetterInterface.
|
||||
*
|
||||
* @param Metrika $metrika Metrika analytics object.
|
||||
* @param Events $event Metrika event.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setParams(Metrika $metrika, Events $event): void
|
||||
{
|
||||
$this->setProviderParam($metrika, $event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the provider parameter in the given Metrika analytics object for the specified event.
|
||||
*
|
||||
* @param Metrika $metrika Metrika analytics object.
|
||||
* @param Events $event Metrika event.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function setProviderParam(Metrika $metrika, Events $event): void
|
||||
{
|
||||
$providerCode = $this->getProviderCodeByEvent($event);
|
||||
if ($providerCode !== null)
|
||||
{
|
||||
$metrika->setParam(
|
||||
self::PROVIDER_PARAM_POSITION,
|
||||
self::PROVIDER_PARAM_NAME,
|
||||
$providerCode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provider code for a given Metrika event.
|
||||
*
|
||||
* @param Events $event Metrika event.
|
||||
*
|
||||
* @return string|null Provider code if found, null otherwise.
|
||||
*/
|
||||
private function getProviderCodeByEvent(Events $event): ?string
|
||||
{
|
||||
$providerSetting = self::getProviderSettingName($event->value);
|
||||
if (!isset($providerSetting))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
$item = $this->tuningManager->getItem($providerSetting);
|
||||
if ($item === null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
$providerCode = $item->getValue();
|
||||
if (empty($providerCode))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return $item->getOptions()[$providerCode] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get setting name for AI tuning
|
||||
* @param string $eventName
|
||||
* @return string|null
|
||||
*/
|
||||
public static function getProviderSettingName(string $eventName): ?string
|
||||
{
|
||||
$event = Events::tryFrom($eventName);
|
||||
|
||||
return match ($event)
|
||||
{
|
||||
Events::dataGeneration,
|
||||
Events::textsGeneration => Ai::TUNING_CODE_SITE_TEXT_PROVIDER,
|
||||
Events::imagesGeneration => Ai::TUNING_CODE_SITE_IMAGE_PROVIDER,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,13 @@ enum Statuses: string
|
||||
{
|
||||
case Success = 'success';
|
||||
case Error = 'error';
|
||||
case ErrorContentPolicy = 'error_content_policy';
|
||||
case ErrorB24 = 'error_b24';
|
||||
case ErrorProvider = 'error_provider';
|
||||
case ErrorLimitDaily = 'error_limit_daily';
|
||||
case ErrorLimitMonthly = 'error_limit_monthly';
|
||||
case ErrorLimitBaas = 'error_limit_baas';
|
||||
case ErrorMarket = 'error_market';
|
||||
case ErrorTurnedOff = 'error_turnedoff';
|
||||
case UnsupportedBlock = 'unsupported_block';
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Bitrix\Landing\Assets;
|
||||
|
||||
use Bitrix\Landing\Site\Type;
|
||||
use \Bitrix\Main\Localization\Loc;
|
||||
use Bitrix\Main\UI\Extension;
|
||||
use Bitrix\Main;
|
||||
@@ -151,14 +152,13 @@ class Manager
|
||||
self::REGISTERED_KEY_CODE => $code,
|
||||
self::REGISTERED_KEY_LOCATION => $location,
|
||||
];
|
||||
|
||||
if($code !== 'main.core' && $code !== 'core')
|
||||
|
||||
if ($code !== 'main.core' && $code !== 'core')
|
||||
{
|
||||
\CJSCore::markExtensionLoaded($code);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recursive (by 'rel' key) adding assets in WP packege
|
||||
*
|
||||
@@ -200,14 +200,22 @@ class Manager
|
||||
// get data from CJSCore
|
||||
if ($ext = \CJSCore::getExtInfo($code))
|
||||
{
|
||||
if (!Type::isExtensionAllow($code))
|
||||
{
|
||||
return;
|
||||
}
|
||||
$asset = $ext;
|
||||
}
|
||||
else if ($ext = Extension::getConfig($code))
|
||||
elseif ($ext = Extension::getConfig($code))
|
||||
{
|
||||
if (!Type::isExtensionAllow($code))
|
||||
{
|
||||
return;
|
||||
}
|
||||
$asset = $ext;
|
||||
}
|
||||
// if name - it path
|
||||
else if ($type = self::detectType($code))
|
||||
elseif ($type = self::detectType($code))
|
||||
{
|
||||
$asset = [$type => [$code]];
|
||||
}
|
||||
@@ -243,7 +251,6 @@ class Manager
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get parts of asset and add them in pack
|
||||
*
|
||||
@@ -286,7 +293,7 @@ class Manager
|
||||
$this->resources->addString($this->createStringFromPath($path, $type));
|
||||
}
|
||||
// todo: check is file exist
|
||||
else if (self::detectType($path))
|
||||
elseif (self::detectType($path))
|
||||
{
|
||||
$this->resources->add($path, $type, $location);
|
||||
}
|
||||
@@ -355,7 +362,6 @@ class Manager
|
||||
return $externalLink;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Detect type by path.
|
||||
*
|
||||
|
||||
@@ -4447,8 +4447,12 @@ class Block extends \Bitrix\Landing\Internals\BaseTable
|
||||
|
||||
if ($resultNode)
|
||||
{
|
||||
$contentBefore = $resultNode->getOuterHTML();
|
||||
if ((int)$resultNode->getNodeType() === $resultNode::ELEMENT_NODE)
|
||||
$contentBefore = $resultNode->getOuterHTML();
|
||||
if (
|
||||
isset($data[$relativeSelector]['classList'])
|
||||
&& is_array($data[$relativeSelector]['classList'])
|
||||
&& (int)$resultNode->getNodeType() === $resultNode::ELEMENT_NODE
|
||||
)
|
||||
{
|
||||
$resultNode->setClassName(
|
||||
implode(' ', $data[$relativeSelector]['classList'])
|
||||
|
||||
@@ -11,7 +11,6 @@ use Bitrix\Landing\Manager;
|
||||
use Bitrix\Landing\Repo;
|
||||
use Bitrix\Landing\Internals;
|
||||
use Bitrix\Landing\Site\Type;
|
||||
use Bitrix\Main\Application;
|
||||
use Bitrix\Main\Event;
|
||||
use Bitrix\Main\EventResult;
|
||||
use Bitrix\Main\Loader;
|
||||
@@ -46,6 +45,7 @@ class BlockRepo
|
||||
* Sections with special conditions
|
||||
*/
|
||||
private const SECTION_LAST = 'last';
|
||||
private const SECTION_FAVOURITE = 'favourite';
|
||||
|
||||
/**
|
||||
* Section or block type with special conditions
|
||||
@@ -264,7 +264,7 @@ class BlockRepo
|
||||
if ($cache->initCache($cacheTime, $cacheId, $cachePath))
|
||||
{
|
||||
$this->repository = $cache->getVars();
|
||||
if (is_array($this->repository) && !empty($this->repository))
|
||||
if (!empty($this->repository))
|
||||
{
|
||||
return $this->fillLastUsedBlocks();
|
||||
}
|
||||
@@ -582,8 +582,7 @@ class BlockRepo
|
||||
|
||||
private function fillLastUsedBlocks(): static
|
||||
{
|
||||
$request = Application::getInstance()->getContext()->getRequest();
|
||||
if ($request->get('landing_mode') !== 'edit')
|
||||
if (Landing::getEditMode() === false)
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
@@ -609,9 +608,9 @@ class BlockRepo
|
||||
foreach ($cat['items'] as $code => &$block)
|
||||
{
|
||||
if (
|
||||
in_array($code, $lastUsed)
|
||||
&& $catCode != self::SECTION_LAST
|
||||
$catCode !== self::SECTION_LAST
|
||||
&& !empty($block)
|
||||
&& in_array($code, $lastUsed, true)
|
||||
)
|
||||
{
|
||||
$block['section'][] = self::SECTION_LAST;
|
||||
@@ -676,17 +675,17 @@ class BlockRepo
|
||||
* @param string|array $item
|
||||
* @return array|null
|
||||
*/
|
||||
$prepareType = function (string|array $item): ?array
|
||||
$prepareType = static function (string|array $item): ?array
|
||||
{
|
||||
$type = (array)$item;
|
||||
$type = array_map('strtoupper', $type);
|
||||
if (in_array('PAGE', $type))
|
||||
if (in_array('PAGE', $type, true))
|
||||
{
|
||||
$type[] = 'SMN';
|
||||
}
|
||||
if (
|
||||
in_array('NULL', $type)
|
||||
|| in_array('', $type)
|
||||
in_array('NULL', $type, true)
|
||||
|| in_array('', $type, true)
|
||||
)
|
||||
{
|
||||
return null;
|
||||
@@ -706,17 +705,17 @@ class BlockRepo
|
||||
$sectionTypes = $prepareType($section['type'] ?? []);
|
||||
|
||||
if (
|
||||
$this->isFilterActive(self::FILTER_SKIP_COMMON_BLOCKS)
|
||||
&& empty($sectionTypes)
|
||||
&& $sectionCode !== self::SECTION_LAST
|
||||
empty($sectionTypes)
|
||||
&& $this->isFilterActive(self::FILTER_SKIP_COMMON_BLOCKS)
|
||||
&& !in_array($sectionCode, [self::SECTION_LAST, self::SECTION_FAVOURITE], true)
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
$this->isFilterActive(self::FILTER_SKIP_HIDDEN_BLOCKS)
|
||||
&& $sectionTypes === null
|
||||
$sectionTypes === null
|
||||
&& $this->isFilterActive(self::FILTER_SKIP_HIDDEN_BLOCKS)
|
||||
)
|
||||
{
|
||||
continue;
|
||||
@@ -743,16 +742,16 @@ class BlockRepo
|
||||
$blockTypes = $prepareType($block['type'] ?? []);
|
||||
|
||||
if (
|
||||
$this->isFilterActive(self::FILTER_SKIP_COMMON_BLOCKS)
|
||||
&& empty($blockTypes)
|
||||
empty($blockTypes)
|
||||
&& $this->isFilterActive(self::FILTER_SKIP_COMMON_BLOCKS)
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
$this->isFilterActive(self::FILTER_SKIP_HIDDEN_BLOCKS)
|
||||
&& $blockTypes === null
|
||||
$blockTypes === null
|
||||
&& $this->isFilterActive(self::FILTER_SKIP_HIDDEN_BLOCKS)
|
||||
)
|
||||
{
|
||||
continue;
|
||||
@@ -773,9 +772,9 @@ class BlockRepo
|
||||
}
|
||||
|
||||
if (
|
||||
$this->isFilterActive(self::FILTER_SKIP_SYSTEM_BLOCKS)
|
||||
&& isset($block['system'])
|
||||
isset($block['system'])
|
||||
&& $block['system'] === true
|
||||
&& $this->isFilterActive(self::FILTER_SKIP_SYSTEM_BLOCKS)
|
||||
)
|
||||
{
|
||||
continue;
|
||||
@@ -788,13 +787,40 @@ class BlockRepo
|
||||
$filtered[$sectionCode]['items'][$blockCode] = $block;
|
||||
}
|
||||
|
||||
if (empty($filtered[$sectionCode]['items']))
|
||||
if (empty($filtered[$sectionCode]['items']) && $sectionCode !== self::SECTION_FAVOURITE)
|
||||
{
|
||||
unset($filtered[$sectionCode]);
|
||||
}
|
||||
}
|
||||
|
||||
return $filtered;
|
||||
return $this->filterLastUsed($filtered);
|
||||
}
|
||||
|
||||
private function filterLastUsed(array $repository): array
|
||||
{
|
||||
if (!isset($repository[self::SECTION_LAST]['items']))
|
||||
{
|
||||
return $repository;
|
||||
}
|
||||
|
||||
$removeLastSection = static function(array $sections)
|
||||
{
|
||||
return array_diff($sections, [self::SECTION_LAST]);
|
||||
};
|
||||
|
||||
$filteredBlocks = [];
|
||||
$allowableSections = $removeLastSection(array_keys($repository));
|
||||
foreach ($repository[self::SECTION_LAST]['items'] as $code => $block)
|
||||
{
|
||||
$blockSections = $removeLastSection($block['section'] ?? []);
|
||||
if (!empty(array_intersect($blockSections, $allowableSections)))
|
||||
{
|
||||
$filteredBlocks[$code] = $block;
|
||||
}
|
||||
}
|
||||
$repository[self::SECTION_LAST]['items'] = $filteredBlocks;
|
||||
|
||||
return $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
namespace Bitrix\Landing\Connector;
|
||||
|
||||
use \Bitrix\Landing\Manager;
|
||||
use \Bitrix\Main\Loader;
|
||||
use \Bitrix\Main\Localization\Loc;
|
||||
use \Bitrix\Main\Web\Json;
|
||||
use \Bitrix\MobileApp\Janative;
|
||||
@@ -24,6 +25,11 @@ class Mobile
|
||||
*/
|
||||
public static function onMobileMenuStructureBuilt($menu): array
|
||||
{
|
||||
if (!Loader::includeModule('mobileapp'))
|
||||
{
|
||||
return $menu;
|
||||
}
|
||||
|
||||
if (!isset($menu[0]['items']) || !is_array($menu[0]['items']))
|
||||
{
|
||||
return $menu;
|
||||
@@ -163,7 +169,7 @@ JS
|
||||
if ($mobileHit === null)
|
||||
{
|
||||
$mobileHit = \Bitrix\Main\ModuleManager::isModuleInstalled('intranet')
|
||||
&& mb_strpos(Manager::getCurDir(), SITE_DIR . 'mobile/') === 0;
|
||||
&& mb_strpos(Manager::getCurDir(), SITE_DIR . 'mobile/') === 0;
|
||||
}
|
||||
|
||||
return $mobileHit;
|
||||
|
||||
@@ -8,6 +8,7 @@ use Bitrix\Landing;
|
||||
use Bitrix\Landing\Copilot\Generation;
|
||||
use Bitrix\Landing\Copilot\Data;
|
||||
use Bitrix\Landing\Copilot\Connector;
|
||||
use Bitrix\Landing\Metrika;
|
||||
use Bitrix\Main\Error;
|
||||
|
||||
class Copilot extends Controller
|
||||
@@ -54,10 +55,26 @@ class Copilot extends Controller
|
||||
|
||||
public static function checkBlockGeneratableAction(int $blockId, ?int $chatId = null): bool
|
||||
{
|
||||
$result = Data\Block\Operator::isBlockAvailableForScenarioChangeBlock($blockId);
|
||||
$isGeneratable = Data\Block\Operator::isBlockAvailableForScenarioChangeBlock($blockId);
|
||||
|
||||
$metrika = new Metrika\Metrika(
|
||||
Metrika\Categories::BlockEdition,
|
||||
Metrika\Events::select,
|
||||
Metrika\Tools::ai
|
||||
);
|
||||
$metrika
|
||||
->setSection(Metrika\Sections::siteEditor)
|
||||
->setParam(4, 'block', (new Landing\Block($blockId))->getCode())
|
||||
;
|
||||
if (!$isGeneratable)
|
||||
{
|
||||
$metrika->setStatus(Metrika\Statuses::UnsupportedBlock);
|
||||
}
|
||||
$metrika->send();
|
||||
|
||||
if (!$chatId || $chatId <= 0)
|
||||
{
|
||||
return $result;
|
||||
return $isGeneratable;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,12 +84,12 @@ class Copilot extends Controller
|
||||
if ($chatBot)
|
||||
{
|
||||
$message = new Landing\Copilot\Connector\Chat\ChatBotMessageDto($chatId);
|
||||
$result
|
||||
$isGeneratable
|
||||
? $chatBot->sendSelectBlockSuccessMessage($message)
|
||||
: $chatBot->sendSelectBlockWrongMessage($message);
|
||||
}
|
||||
|
||||
return $result;
|
||||
return $isGeneratable;
|
||||
}
|
||||
|
||||
public function sendBlockGenerationNeedSelectMessageAction(int $siteId)
|
||||
@@ -131,6 +148,15 @@ class Copilot extends Controller
|
||||
->setSiteData($siteData)
|
||||
->setWishes((new Data\Wishes())->setWishes([$wishes]))
|
||||
->setChatId($chat->getChatForSite($siteId) ?? 0)
|
||||
->setMetrikaFields((new Metrika\FieldsDto(
|
||||
params: [
|
||||
[
|
||||
4,
|
||||
'block',
|
||||
(new Landing\Block($blockId))->getCode(),
|
||||
],
|
||||
]
|
||||
)))
|
||||
;
|
||||
|
||||
if ($generation->execute())
|
||||
|
||||
@@ -12,7 +12,17 @@ class Landing extends Controller
|
||||
{
|
||||
return [
|
||||
new Engine\ActionFilter\Authentication(),
|
||||
new ActionFilter\Extranet(),
|
||||
];
|
||||
}
|
||||
|
||||
public function configureActions(): array
|
||||
{
|
||||
return [
|
||||
'getByIdAction' => [
|
||||
'+prefilters' => [
|
||||
new ActionFilter\Extranet(),
|
||||
],
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
@@ -40,4 +50,9 @@ class Landing extends Controller
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isPhoneRegionCodeTourAlreadySeenAction(): bool
|
||||
{
|
||||
return \CUserOptions::GetOption('ui-tour', 'landing_phone_aha_shown', null) !== null;
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,20 @@ namespace Bitrix\Landing\Controller;
|
||||
|
||||
use Bitrix\Main\Engine;
|
||||
use Bitrix\Main\Engine\Controller;
|
||||
use Bitrix\Landing;
|
||||
use Bitrix\Landing\Copilot\Generation;
|
||||
use Bitrix\Landing\Copilot\Data;
|
||||
use Bitrix\Landing\Copilot\Connector;
|
||||
use Bitrix\Main\Error;
|
||||
use Bitrix\Main\Loader;
|
||||
use Bitrix\Main\Localization\Loc;
|
||||
use Bitrix\Main\Web\HttpClient;
|
||||
use Bitrix\Landing;
|
||||
use Bitrix\Landing\Manager;
|
||||
use Bitrix\Landing\Assets;
|
||||
use Bitrix\Landing\Copilot\Connector;
|
||||
use Bitrix\Rest;
|
||||
|
||||
class Vibe extends Controller
|
||||
{
|
||||
private const SUBTYPE_WIDGET = 'widgetvue';
|
||||
|
||||
public function getDefaultPreFilters(): array
|
||||
{
|
||||
return [
|
||||
@@ -21,13 +27,164 @@ class Vibe extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Save relations between chat, site and user
|
||||
* @param int $siteId
|
||||
* @param int $chatId
|
||||
* @return bool
|
||||
* Get core extensions and styles configs, load relations, load lang phrases
|
||||
*
|
||||
* @return array - array of assets by type
|
||||
*/
|
||||
public function setDemoTestAction(): bool
|
||||
public static function getCoreConfigAction(): array
|
||||
{
|
||||
return (bool)\CBitrix24::setLicenseType('demo');
|
||||
$coreExts = [
|
||||
'main.core',
|
||||
'ui.design-tokens',
|
||||
];
|
||||
|
||||
$assetsManager = (new Assets\Manager())
|
||||
->enableSandbox()
|
||||
->addAsset($coreExts)
|
||||
;
|
||||
|
||||
$siteTemplatePath =
|
||||
(defined('SITE_TEMPLATE_PATH') ? SITE_TEMPLATE_PATH : '/bitrix/templates/bitrix24');
|
||||
$style = $siteTemplatePath . '/dist/bitrix24.bundle.css';
|
||||
$assetsManager->addAsset($style);
|
||||
|
||||
return $assetsManager->getOutput();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extensions configs, load relations, load lang phrases
|
||||
*
|
||||
* @param array $extCodes - array of extensions codes
|
||||
* @return array - array of assets by type
|
||||
*/
|
||||
public static function getAssetsConfigAction(array $extCodes): array
|
||||
{
|
||||
$assetsManager = (new Assets\Manager())
|
||||
->enableSandbox()
|
||||
->addAsset($extCodes)
|
||||
;
|
||||
|
||||
return $assetsManager->getOutput();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $blockId
|
||||
* @param array $params
|
||||
*/
|
||||
public function fetchDataAction(int $blockId, array $params = [])
|
||||
{
|
||||
$block = new Landing\Block($blockId);
|
||||
if (!$block->getId())
|
||||
{
|
||||
$this->addError(
|
||||
new Error(Loc::getMessage('LANDING_WIDGET_BLOCK_NOT_FOUND'), 'BLOCK_NOT_FOUND')
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Loader::includeModule('rest'))
|
||||
{
|
||||
$this->addError(
|
||||
new Error(Loc::getMessage('LANDING_WIDGET_REST_NOT_FOUND'), 'REST_NOT_FOUND')
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// check app
|
||||
$repoId = $block->getRepoId();
|
||||
$app = Landing\Repo::getAppInfo($repoId);
|
||||
if (
|
||||
!$repoId
|
||||
|| empty($app)
|
||||
|| !isset($app['CLIENT_ID'])
|
||||
)
|
||||
{
|
||||
$this->addError(
|
||||
new Error(Loc::getMessage('LANDING_WIDGET_APP_NOT_FOUND'), 'APP_NOT_FOUND')
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// get auth
|
||||
$appHasAccess = \CRestUtil::checkAppAccess($app['ID'] ?? 0);
|
||||
if (!$appHasAccess)
|
||||
{
|
||||
$this->addError(
|
||||
// todo: open after translate
|
||||
// new Error(Loc::getMessage('LANDING_WIDGET_APP_NO_ACCESS'), 'APP_NO_ACCESS')
|
||||
new Error('Landing widget app has no access', 'APP_NO_ACCESS')
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$auth = Rest\Application::getAuthProvider()?->get(
|
||||
$app['CLIENT_ID'],
|
||||
'landing',
|
||||
[],
|
||||
Manager::getUserId()
|
||||
);
|
||||
if ($auth && isset($auth['error']))
|
||||
{
|
||||
$this->addError(
|
||||
new Error(
|
||||
$auth['error_description'] ?? '',
|
||||
'APP_AUTH_ERROR__' . $auth['error']
|
||||
)
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
$params['auth'] = $auth;
|
||||
|
||||
// check subtype
|
||||
$manifest = $block->getManifest();
|
||||
if (
|
||||
!in_array(self::SUBTYPE_WIDGET, (array)$manifest['block']['subtype'], true)
|
||||
|| !is_array($manifest['block']['subtype_params'])
|
||||
|| !isset($manifest['block']['subtype_params']['handler'])
|
||||
)
|
||||
{
|
||||
$this->addError(
|
||||
new Error(Loc::getMessage('LANDING_WIDGET_HANDLER_NOT_FOUND_2'), 'HANDLER_NOT_FOUND')
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// request
|
||||
$url = (string)$manifest['block']['subtype_params']['handler'];
|
||||
$http = new HttpClient();
|
||||
$data = $http->post(
|
||||
$url,
|
||||
$params
|
||||
);
|
||||
|
||||
if ($http->getStatus() !== 200)
|
||||
{
|
||||
$this->addError(
|
||||
new Error(Loc::getMessage('LANDING_WIDGET_HANDLER_NOT_ALLOW'), 'HANDLER_NOT_ALLOW')
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$type = empty($params) ? 'fetch' : 'fetch_params';
|
||||
Rest\UsageStatTable::logLandingWidget($app['CLIENT_ID'], $type);
|
||||
Rest\UsageStatTable::finalize();
|
||||
|
||||
if (isset($data['error']))
|
||||
{
|
||||
$this->addError(
|
||||
new Error($data['error'], $data['error_description'] ?? '')
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ class Text extends \Bitrix\Landing\Field
|
||||
$this->searchable = isset($params['searchable']) && $params['searchable'] === true;
|
||||
$this->placeholder = isset($params['placeholder']) ? $params['placeholder'] : '';
|
||||
$this->maxlength = isset($params['maxlength']) ? (int)$params['maxlength'] : 0;
|
||||
$this->fetchModificator = isset($params['fetch_data_modification']) ? $params['fetch_data_modification'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@ use Bitrix\Landing\Field;
|
||||
use Bitrix\Landing\Manager;
|
||||
use Bitrix\Main\Loader;
|
||||
use Bitrix\Main\Localization\Loc;
|
||||
use Bitrix\Main\Page\Asset;
|
||||
use Bitrix\UI;
|
||||
|
||||
class Fonts extends \Bitrix\Landing\Hook\Page
|
||||
@@ -65,6 +66,11 @@ class Fonts extends \Bitrix\Landing\Hook\Page
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Default domain for Google Fonts.
|
||||
*/
|
||||
public const DEFAULT_DOMAIN = 'fonts.googleapis.com';
|
||||
|
||||
/**
|
||||
* Set fonts on the page.
|
||||
* @var array
|
||||
@@ -164,10 +170,7 @@ class Fonts extends \Bitrix\Landing\Hook\Page
|
||||
|
||||
if ($setFonts)
|
||||
{
|
||||
Manager::setPageView(
|
||||
'BeforeHeadClose',
|
||||
implode('', $setFonts)
|
||||
);
|
||||
Asset::getInstance()->addString(implode('', $setFonts));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,7 +214,8 @@ class Fonts extends \Bitrix\Landing\Hook\Page
|
||||
*/
|
||||
public static function generateFontTags(string $fontName): string
|
||||
{
|
||||
$fontUrl = "https://fonts.bitrix24.ru/css2?family="
|
||||
$proxyDomain = self::getProxyDomain();
|
||||
$fontUrl = "https://{$proxyDomain}/css2?family="
|
||||
. str_replace(' ', '+', $fontName)
|
||||
. ":wght@100;200;300;400;500;600;700;800;900";
|
||||
$fontClass = strtolower(str_replace(' ', '-', $fontName));
|
||||
@@ -221,7 +225,7 @@ class Fonts extends \Bitrix\Landing\Hook\Page
|
||||
<link rel="preload" href="$fontUrl" data-font="g-font-$fontClass" onload="this.removeAttribute('onload');this.rel='stylesheet'" as="style">
|
||||
<style data-id="g-font-$fontClass">.g-font-$fontClass { font-family: "$fontName", sans-serif; }</style>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Proxy font url to bitrix servers
|
||||
@@ -230,16 +234,28 @@ class Fonts extends \Bitrix\Landing\Hook\Page
|
||||
*/
|
||||
protected static function proxyFontUrl(string $fontString): string
|
||||
{
|
||||
$defaultDomain = 'fonts.googleapis.com';
|
||||
$proxyDomain = $defaultDomain;
|
||||
if (Loader::includeModule('ui'))
|
||||
{
|
||||
$proxyDomain = UI\Fonts\Proxy::resolveDomain(Manager::getZone());
|
||||
}
|
||||
$defaultDomain = self::DEFAULT_DOMAIN;
|
||||
$proxyDomain = self::getProxyDomain();
|
||||
|
||||
return ($defaultDomain !== $proxyDomain)
|
||||
? str_replace($defaultDomain, $proxyDomain, $fontString)
|
||||
: $fontString
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the proxy domain for fonts, depending on the current zone and UI module.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function getProxyDomain(): string
|
||||
{
|
||||
$proxyDomain = self::DEFAULT_DOMAIN;
|
||||
if (Loader::includeModule('ui'))
|
||||
{
|
||||
$proxyDomain = UI\Fonts\Proxy::resolveDomain(Manager::getZone());
|
||||
}
|
||||
|
||||
return $proxyDomain;
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ class PixelVk extends \Bitrix\Landing\Hook\Page
|
||||
var t=document.createElement("script");
|
||||
t.type="text/javascript",
|
||||
t.async=!0,
|
||||
t.src="https://vk.com/js/api/openapi.js?160",
|
||||
t.src="https://vk.ru/js/api/openapi.js?160",
|
||||
t.onload=function(){VK.Retargeting.Init("' . $counter . '"),
|
||||
VK.Retargeting.Hit()},document.head.appendChild(t)
|
||||
}();'
|
||||
@@ -84,7 +84,7 @@ class PixelVk extends \Bitrix\Landing\Hook\Page
|
||||
Manager::setPageView(
|
||||
'Noscript',
|
||||
'<noscript>
|
||||
<img src="https://vk.com/rtrg?p=' . $counter . '" style="position:fixed; left:-999px;" alt=""/>
|
||||
<img src="https://vk.ru/rtrg?p=' . $counter . '" style="position:fixed; left:-999px;" alt=""/>
|
||||
</noscript>'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ class Settings extends \Bitrix\Landing\Hook\Page
|
||||
'BRAND_PROPERTY' => 'BRAND_REF',
|
||||
'CART_POSITION' => 'BL',
|
||||
'AGREEMENT_ID' => 0,
|
||||
'AGREEMENTS' => null,
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -156,9 +157,8 @@ class Settings extends \Bitrix\Landing\Hook\Page
|
||||
default:
|
||||
{
|
||||
$field = new Field\Text($code, array(
|
||||
'title' => isset($params['NAME'])
|
||||
? $params['NAME']
|
||||
: ''
|
||||
'title' => $params['NAME'] ?? '',
|
||||
'fetch_data_modification' => $params['FETCH_DATA_MODIFICATION'] ?? null,
|
||||
));
|
||||
break;
|
||||
}
|
||||
@@ -322,6 +322,40 @@ class Settings extends \Bitrix\Landing\Hook\Page
|
||||
$fields['AGREEMENT_ID'] = self::getFieldByType(
|
||||
null, 'AGREEMENT_ID'
|
||||
);
|
||||
$fields['AGREEMENTS'] = self::getFieldByType(
|
||||
null,
|
||||
'AGREEMENTS',
|
||||
[
|
||||
'FETCH_DATA_MODIFICATION' => function ($agreements) {
|
||||
if (!is_array($agreements))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
$resultAgreements = [];
|
||||
$existAgreementIds = [];
|
||||
foreach ($agreements as $agreement)
|
||||
{
|
||||
$agreement['ID'] = (int)$agreement['ID'];
|
||||
if (
|
||||
empty($agreement['ID'])
|
||||
|| isset($existAgreementIds[$agreement['ID']])
|
||||
)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$existAgreementIds[$agreement['ID']] = true;
|
||||
$resultAgreements[] = [
|
||||
'ID' => $agreement['ID'],
|
||||
'CHECKED' => $agreement['CHECKED'] === 'Y' ? 'Y' : 'N',
|
||||
'REQUIRED' => $agreement['REQUIRED'] === 'Y' ? 'Y' : 'N',
|
||||
];
|
||||
}
|
||||
|
||||
return $resultAgreements;
|
||||
}
|
||||
],
|
||||
);
|
||||
|
||||
// cart position
|
||||
$positions = array_fill_keys(
|
||||
@@ -423,11 +457,19 @@ class Settings extends \Bitrix\Landing\Hook\Page
|
||||
if($hooks['SETTINGS']['AGREEMENT_USE'] === 'N')
|
||||
{
|
||||
$settings[$id]['AGREEMENT_ID'] = 0;
|
||||
$settings[$id]['AGREEMENTS'] = [];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$settings[$id]['AGREEMENT_USE'] = $settings[$id]['AGREEMENT_ID'] ? 'Y' : 'N';
|
||||
if ($settings[$id]['AGREEMENTS'] === null)
|
||||
{
|
||||
$settings[$id]['AGREEMENT_USE'] = $settings[$id]['AGREEMENT_ID'] ? 'Y' : 'N';
|
||||
}
|
||||
else
|
||||
{
|
||||
$settings[$id]['AGREEMENT_USE'] = $settings[$id]['AGREEMENTS'] ? 'Y' : 'N';
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($hooks['SETTINGS']['CART_POSITION']))
|
||||
|
||||
50
core/bitrix/modules/landing/lib/internals/blockfavourite.php
Normal file
50
core/bitrix/modules/landing/lib/internals/blockfavourite.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
namespace Bitrix\Landing\Internals;
|
||||
|
||||
use \Bitrix\Main\Entity;
|
||||
use \Bitrix\Main\Localization\Loc;
|
||||
|
||||
Loc::loadMessages(__FILE__);
|
||||
|
||||
/**
|
||||
* Class BlockFavouriteTable
|
||||
*/
|
||||
class BlockFavouriteTable extends Entity\DataManager
|
||||
{
|
||||
/**
|
||||
* Returns DB table name for entity.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getTableName(): string
|
||||
{
|
||||
return 'b_landing_block_favourite';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns entity map definition.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getMap(): array
|
||||
{
|
||||
return array(
|
||||
'ID' => new Entity\IntegerField('ID', array(
|
||||
'title' => 'ID',
|
||||
'primary' => true,
|
||||
'autocomplete' => true,
|
||||
)),
|
||||
'USER_ID' => new Entity\IntegerField('USER_ID', array(
|
||||
'title' => Loc::getMessage('LANDING_TABLE_BLOCK_FAVOURITE_FIELD_USER_ID'),
|
||||
'required' => true
|
||||
)),
|
||||
'CODE' => new Entity\StringField('CODE', array(
|
||||
'title' => Loc::getMessage('LANDING_TABLE_BLOCK_FAVOURITE_FIELD_CODE'),
|
||||
'required' => true
|
||||
)),
|
||||
'DATE_CREATE' => new Entity\DatetimeField('DATE_CREATE', array(
|
||||
'title' => Loc::getMessage('LANDING_TABLE_BLOCK_FAVOURITE_FIELD_DATE_CREATE')
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,7 @@ class SiteTable extends Entity\DataManager
|
||||
'default_value' => 'Y'
|
||||
)),
|
||||
'DELETED' => new Entity\StringField('DELETED', array(
|
||||
'title' => Loc::getMessage('LANDING_TABLE_FIELD_LANDING_DELETED'),
|
||||
'title' => Loc::getMessage('LANDING_TABLE_FIELD_SITE_DELETED'),
|
||||
'default_value' => 'N'
|
||||
)),
|
||||
'TITLE' => new Entity\StringField('TITLE', array(
|
||||
@@ -937,34 +937,7 @@ class SiteTable extends Entity\DataManager
|
||||
{
|
||||
try
|
||||
{
|
||||
//todo: revert changes after change .by domain
|
||||
if (
|
||||
!str_ends_with($domainName, '.b24site.online')
|
||||
&& !str_ends_with($domainName, '.b24shop.online')
|
||||
)
|
||||
{
|
||||
$domainExist = $siteController::isDomainExists($domainName);
|
||||
}
|
||||
else
|
||||
{
|
||||
$byDomainName = '';
|
||||
if (str_ends_with($domainName, '.b24site.online'))
|
||||
{
|
||||
$byDomainName = str_replace('.b24site.online', '.bitrix24site.by', $domainName);
|
||||
}
|
||||
if (str_ends_with($domainName, '.b24shop.online'))
|
||||
{
|
||||
$byDomainName = str_replace('.b24shop.online', '.bitrix24shop.by', $domainName);
|
||||
}
|
||||
if ($byDomainName !== '' && $siteController::isDomainExists($byDomainName))
|
||||
{
|
||||
$domainExist = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
$domainExist = $siteController::isDomainExists($domainName);
|
||||
}
|
||||
}
|
||||
$domainExist = $siteController::isDomainExists($domainName);
|
||||
}
|
||||
catch (SystemException $ex)
|
||||
{
|
||||
|
||||
@@ -1639,7 +1639,14 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
|
||||
$urls['LANDING'][$lid] = \htmlspecialcharsbx($urls['LANDING'][$lid]);
|
||||
}
|
||||
$urls['LANDING'][$lid] .= ($isIframe ? '?IFRAME=Y' : '');
|
||||
$urls['BLOCK'][$bid] = $urls['LANDING'][$lid] . '#' . $anchorsPublicId[$bid];
|
||||
if (Site\Type::getCurrentScopeId() === Site\Type::SCOPE_CODE_MAINPAGE)
|
||||
{
|
||||
$urls['BLOCK'][$bid] = '#' . $anchorsPublicId[$bid];
|
||||
}
|
||||
else
|
||||
{
|
||||
$urls['BLOCK'][$bid] = $urls['LANDING'][$lid] . '#' . $anchorsPublicId[$bid];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2281,11 +2288,6 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
|
||||
*/
|
||||
public function addBlock(string $code, array $data = array(), bool $saveInLastUsed = false)
|
||||
{
|
||||
$metrika = new Metrika\Metrika(
|
||||
Metrika\Categories::getBySiteType(self::$siteType),
|
||||
Metrika\Events::addWidget,
|
||||
);
|
||||
|
||||
if (!$this->canEdit())
|
||||
{
|
||||
$this->error->addError(
|
||||
@@ -2293,12 +2295,9 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
|
||||
Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED')
|
||||
);
|
||||
|
||||
if (self::$siteType === Type::SCOPE_CODE_MAINPAGE)
|
||||
if ($saveInLastUsed)
|
||||
{
|
||||
$metrika
|
||||
->setError('ACCESS_DENIED')
|
||||
->send()
|
||||
;
|
||||
$this->sendAddBlockErrorMetrika('ACCESS_DENIED');
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -2316,6 +2315,7 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
|
||||
if ($saveInLastUsed)
|
||||
{
|
||||
Block::markAsUsed($code);
|
||||
$this->sendAddBlockMetrika($block, $data);
|
||||
}
|
||||
|
||||
$this->touch();
|
||||
@@ -2327,25 +2327,69 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
|
||||
$history->push('ADD_BLOCK', ['block' => $block]);
|
||||
}
|
||||
|
||||
if (self::$siteType === Type::SCOPE_CODE_MAINPAGE)
|
||||
{
|
||||
$this->parametrizeMetrikaByBlock($metrika, $block)->send();
|
||||
}
|
||||
|
||||
return $block->getId();
|
||||
}
|
||||
|
||||
if (self::$siteType === Type::SCOPE_CODE_MAINPAGE)
|
||||
$this->error->addError(
|
||||
'BLOCK_NOT_FOUND',
|
||||
Loc::getMessage('LANDING_BLOCK_NOT_FOUND')
|
||||
);
|
||||
|
||||
if ($saveInLastUsed)
|
||||
{
|
||||
$metrika
|
||||
->setError('BLOCK_NOT_FOUND')
|
||||
->send()
|
||||
;
|
||||
$this->sendAddBlockErrorMetrika('BLOCK_NOT_FOUND');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Metrika object for the addWidget event.
|
||||
*
|
||||
* @param Metrika\Events $event The event name for which the Metrika object is created.
|
||||
*
|
||||
* @return Metrika\Metrika The created Metrika object.
|
||||
*/
|
||||
protected function createMetrika(Metrika\Events $event): Metrika\Metrika
|
||||
{
|
||||
return new Metrika\Metrika(
|
||||
Metrika\Categories::getBySiteType(self::$siteType),
|
||||
$event,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send add block metrika if needed.
|
||||
*
|
||||
* @param array $data
|
||||
* @param Block $block
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function sendAddBlockMetrika(Block $block, array $data): void
|
||||
{
|
||||
$metrika = $this->createMetrika(Metrika\Events::addWidget);
|
||||
|
||||
$metrika->setSubSection($data['CATEGORY'] ?? '');
|
||||
$this->parametrizeMetrikaByBlock($metrika, $block)->send();
|
||||
|
||||
$metrika->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a Metrika event for an error that occurred while adding a block.
|
||||
*
|
||||
* @param string $error The error code or message to be sent to Metrika.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function sendAddBlockErrorMetrika(string $error): void
|
||||
{
|
||||
$metrika = $this->createMetrika(Metrika\Events::addWidget);
|
||||
|
||||
$metrika->setError($error)->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete one block from current landing.
|
||||
* @param int $id Block id.
|
||||
@@ -2393,10 +2437,7 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
|
||||
$this->blocks[$id] = new Block($id);
|
||||
}
|
||||
|
||||
$metrika = new Metrika\Metrika(
|
||||
Metrika\Categories::getBySiteType(self::$siteType),
|
||||
Metrika\Events::deleteWidget,
|
||||
);
|
||||
$metrika = $this->createMetrika(Metrika\Events::deleteWidget);
|
||||
|
||||
if (
|
||||
isset($this->blocks[$id]) &&
|
||||
@@ -2415,13 +2456,10 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
|
||||
}
|
||||
if ($this->blocks[$id]->save())
|
||||
{
|
||||
if (self::$siteType === Type::SCOPE_CODE_MAINPAGE)
|
||||
{
|
||||
$this
|
||||
->parametrizeMetrikaByBlock($metrika, $this->blocks[$id])
|
||||
->send()
|
||||
;
|
||||
}
|
||||
$this
|
||||
->parametrizeMetrikaByBlock($metrika, $this->blocks[$id])
|
||||
->send()
|
||||
;
|
||||
|
||||
if ($mark)
|
||||
{
|
||||
@@ -2449,13 +2487,10 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
|
||||
$this->blocks[$id]->getError()
|
||||
);
|
||||
|
||||
if (self::$siteType === Type::SCOPE_CODE_MAINPAGE)
|
||||
{
|
||||
$metrika
|
||||
->setError('SAVE_ERROR')
|
||||
->send()
|
||||
;
|
||||
}
|
||||
$metrika
|
||||
->setError('SAVE_ERROR')
|
||||
->send()
|
||||
;
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -2464,13 +2499,10 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
|
||||
'ACCESS_DENIED',
|
||||
Loc::getMessage('LANDING_BLOCK_ACCESS_DENIED')
|
||||
);
|
||||
if (self::$siteType === Type::SCOPE_CODE_MAINPAGE)
|
||||
{
|
||||
$metrika
|
||||
->setError('ACCESS_DENIED')
|
||||
->send()
|
||||
;
|
||||
}
|
||||
$metrika
|
||||
->setError('ACCESS_DENIED')
|
||||
->send()
|
||||
;
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -2481,13 +2513,10 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
|
||||
'BLOCK_NOT_FOUND',
|
||||
Loc::getMessage('LANDING_BLOCK_NOT_FOUND')
|
||||
);
|
||||
if (self::$siteType === Type::SCOPE_CODE_MAINPAGE)
|
||||
{
|
||||
$metrika
|
||||
->setError('BLOCK_NOT_FOUND')
|
||||
->send()
|
||||
;
|
||||
}
|
||||
$metrika
|
||||
->setError('BLOCK_NOT_FOUND')
|
||||
->send()
|
||||
;
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -2497,9 +2526,6 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
|
||||
|
||||
protected function parametrizeMetrikaByBlock(Metrika\Metrika $metrika, Block $block): Metrika\Metrika
|
||||
{
|
||||
// todo: Now method callsed just for widget
|
||||
// todo: If need for all types - add self::$siteType === Type::SCOPE_CODE_MAINPAGE checking
|
||||
|
||||
if ($block->getRepoId())
|
||||
{
|
||||
$metrika->setType(Types::widgetPartner);
|
||||
@@ -2529,6 +2555,7 @@ class Landing extends \Bitrix\Landing\Internals\BaseTable
|
||||
}
|
||||
|
||||
$metrika->setParam(2, 'widgetId', $block->getCode());
|
||||
$metrika->setParam(3, 'siteId', $this->siteId);
|
||||
|
||||
return $metrika;
|
||||
}
|
||||
|
||||
@@ -140,6 +140,42 @@ class View
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of views (sum of VIEWS field) for the given landing.
|
||||
* @param int $lid Landing id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function getNumberTotalViews(int $lid): int
|
||||
{
|
||||
$lid = (int)$lid;
|
||||
$res = ViewTable::getList([
|
||||
'select' => [
|
||||
'SUM'
|
||||
],
|
||||
'filter' => [
|
||||
'LID' => $lid
|
||||
],
|
||||
'runtime' => [
|
||||
new Entity\ExpressionField(
|
||||
'SUM', 'SUM(%s)', ['VIEWS']
|
||||
)
|
||||
]
|
||||
]);
|
||||
|
||||
if ($row = $res->fetch())
|
||||
{
|
||||
$totalViews = (int)$row['SUM'];
|
||||
}
|
||||
|
||||
if (isset($totalViews) && is_int($totalViews))
|
||||
{
|
||||
return $totalViews;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static function getUniqueUserData(int $lid): array
|
||||
{
|
||||
$res = ViewTable::getList([
|
||||
|
||||
@@ -1106,7 +1106,7 @@ class Manager
|
||||
{
|
||||
if (!defined('LANDING_PREVIEW_URL'))
|
||||
{
|
||||
define('LANDING_PREVIEW_URL', self::getRegionPreviewDomain());
|
||||
define('LANDING_PREVIEW_URL', self::getPreviewDomain());
|
||||
}
|
||||
|
||||
return LANDING_PREVIEW_URL;
|
||||
@@ -1121,7 +1121,7 @@ class Manager
|
||||
{
|
||||
if (!defined('LANDING_PREVIEW_WEBHOOK'))
|
||||
{
|
||||
$host = self::getRegionPreviewDomain();
|
||||
$host = self::getPreviewDomain();
|
||||
define('LANDING_PREVIEW_WEBHOOK', $host . '/rest/1/gvsn3ngrn7vb4t1m/');
|
||||
}
|
||||
|
||||
@@ -1546,7 +1546,7 @@ class Manager
|
||||
* Get preview domain based on region.
|
||||
* @return string
|
||||
*/
|
||||
private static function getRegionPreviewDomain(): string
|
||||
private static function getPreviewDomain(): string
|
||||
{
|
||||
$region = Application::getInstance()->getLicense()->getRegion();
|
||||
|
||||
|
||||
@@ -206,6 +206,11 @@ class Component extends \Bitrix\Landing\Node
|
||||
*/
|
||||
public static function prepareManifest(Block $block, array $manifest, array &$manifestFull = array())
|
||||
{
|
||||
// set predefined
|
||||
Component::setPredefineForDynamicProps(array(
|
||||
'LANDING_MODE' => (\Bitrix\Landing\Landing::getEditMode() === true) ? 'Y' : 'N',
|
||||
));
|
||||
|
||||
if (
|
||||
!isset($manifest['extra']['editable']) ||
|
||||
!is_array($manifest['extra']['editable'])
|
||||
@@ -339,6 +344,7 @@ class Component extends \Bitrix\Landing\Node
|
||||
'original_type' => 'component',
|
||||
'component_type' => $newExtra[$field]['TYPE'] ?? '',
|
||||
'attribute' => $field,
|
||||
'placeholder' => $newExtra[$field]['PLACEHOLDER'] ?? '',
|
||||
'value' => self::preparePropValue(
|
||||
$newExtra[$field]['VALUE'],
|
||||
$fieldItem
|
||||
@@ -621,7 +627,7 @@ class Component extends \Bitrix\Landing\Node
|
||||
}
|
||||
default:
|
||||
{
|
||||
$item['placeholder'] = '';
|
||||
$item['placeholder'] = $item['placeholder'] ?? '';
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1114,33 +1114,4 @@ class Block
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extensions configs, load relations, load lang phrases
|
||||
*
|
||||
* @param array $extCodes - array of extensions codes
|
||||
* @param array $tplCodes - array of site templates
|
||||
* @return PublicActionResult - array of assets by type
|
||||
*/
|
||||
public static function getAssetsConfig(array $extCodes, array $tplCodes = []): PublicActionResult
|
||||
{
|
||||
$result = new PublicActionResult();
|
||||
|
||||
$assetsManager = (new Assets\Manager())
|
||||
->enableSandbox()
|
||||
->addAsset($extCodes)
|
||||
;
|
||||
|
||||
foreach ($tplCodes as $tpl)
|
||||
{
|
||||
$siteTemplatePath =
|
||||
(defined('SITE_TEMPLATE_PATH') ? SITE_TEMPLATE_PATH : '/bitrix/templates/.default');
|
||||
$style = $siteTemplatePath . "/template_styles.css";
|
||||
$assetsManager->addAsset($style);
|
||||
}
|
||||
|
||||
$result->setResult($assetsManager->getOutput());
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,33 +239,10 @@ class Domain
|
||||
$siteController = Manager::getExternalSiteController();
|
||||
if ($siteController)
|
||||
{
|
||||
//todo: revert changes after change .by domain
|
||||
$domainName = $return['domain'];
|
||||
$byDomainName = '';
|
||||
$isOnlineSite = str_ends_with($domainName, '.b24site.online');
|
||||
$isOnlineShop = str_ends_with($domainName, '.b24shop.online');
|
||||
if ($isOnlineSite)
|
||||
{
|
||||
$byDomainName = str_replace('.b24site.online', '.bitrix24site.by', $domainName);
|
||||
}
|
||||
if ($isOnlineShop)
|
||||
{
|
||||
$byDomainName = str_replace('.b24shop.online', '.bitrix24shop.by', $domainName);
|
||||
}
|
||||
$checkResult = $siteController::isDomainExists(
|
||||
$domainName
|
||||
$checkResult = $siteController::isDomainExists(
|
||||
$return['domain']
|
||||
);
|
||||
if ($byDomainName === '')
|
||||
{
|
||||
$return['available'] = $checkResult < 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
$checkResultBy = $siteController::isDomainExists(
|
||||
$byDomainName
|
||||
);
|
||||
$return['available'] = $checkResult < 2 && $checkResultBy < 2;
|
||||
}
|
||||
$return['available'] = $checkResult < 2;
|
||||
}
|
||||
}
|
||||
catch (SystemException $ex)
|
||||
|
||||
@@ -11,6 +11,7 @@ use Bitrix\Landing\Block as BlockCore;
|
||||
use Bitrix\Landing\TemplateRef;
|
||||
use Bitrix\Landing\Landing as LandingCore;
|
||||
use Bitrix\Landing\PublicActionResult;
|
||||
use Bitrix\Landing\Internals\BlockFavouriteTable;
|
||||
use Bitrix\Landing\Internals\HookDataTable;
|
||||
use Bitrix\Landing\History;
|
||||
use Bitrix\Main\Localization\Loc;
|
||||
@@ -19,6 +20,14 @@ Loc::loadMessages(__FILE__);
|
||||
|
||||
class Landing
|
||||
{
|
||||
private const ACTION_ADD = 'add';
|
||||
private const ACTION_REMOVE = 'remove';
|
||||
|
||||
private const STATUS_ADDED = 'added';
|
||||
private const STATUS_ALREADY_EXISTS = 'already_exists';
|
||||
private const STATUS_DELETED = 'deleted';
|
||||
private const STATUS_NOT_FOUND = 'not_found';
|
||||
|
||||
/**
|
||||
* Clear disallow keys from add/update fields.
|
||||
* @param array $fields
|
||||
@@ -215,6 +224,10 @@ class Landing
|
||||
$bad
|
||||
);
|
||||
}
|
||||
if (isset($fields['CATEGORY']))
|
||||
{
|
||||
$data['CATEGORY'] = $fields['CATEGORY'];
|
||||
}
|
||||
// sort
|
||||
if (isset($fields['AFTER_ID']))
|
||||
{
|
||||
@@ -271,6 +284,139 @@ class Landing
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or remove a block code from the user's list of favourite blocks.
|
||||
*
|
||||
* @param string $codeBlock The code of the block to add or remove from favourites.
|
||||
* @param string $action The action to perform: 'add' to add to favourites, 'remove' to remove from favourites.
|
||||
*
|
||||
* @return PublicActionResult Result object with status or error information.
|
||||
*/
|
||||
public static function markFavouriteBlock(string $codeBlock, string $action, string $type): PublicActionResult
|
||||
{
|
||||
$result = new PublicActionResult();
|
||||
$userId = Manager::getUserId();
|
||||
|
||||
if ($userId <= 0 || !$codeBlock || !in_array($action, [self::ACTION_ADD, self::ACTION_REMOVE], true))
|
||||
{
|
||||
$error = new \Bitrix\Landing\Error;
|
||||
$error->addError('DB_ERROR_ADD','Invalid user, codeBlock or action');
|
||||
$result->setError($error);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$existing = BlockFavouriteTable::getList([
|
||||
'filter' => [
|
||||
'=USER_ID' => $userId,
|
||||
'=CODE' => $codeBlock,
|
||||
],
|
||||
'select' => ['ID'],
|
||||
])->fetch();
|
||||
|
||||
switch ($action) {
|
||||
case self::ACTION_ADD:
|
||||
if (!$existing)
|
||||
{
|
||||
$addResult = BlockFavouriteTable::add([
|
||||
'USER_ID' => $userId,
|
||||
'CODE' => $codeBlock,
|
||||
'DATE_CREATE' => new \Bitrix\Main\Type\DateTime(),
|
||||
]);
|
||||
|
||||
if ($addResult->isSuccess())
|
||||
{
|
||||
$result->setResult(['status' => self::STATUS_ADDED]);
|
||||
}
|
||||
else
|
||||
{
|
||||
$error = new \Bitrix\Landing\Error;
|
||||
$error->addError('DB_ERROR_ADD', $addResult->getErrorMessages());
|
||||
$result->setError($error);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$result->setResult(['status' => self::STATUS_ALREADY_EXISTS]);
|
||||
}
|
||||
$metrikaEvent = Metrika\Events::addFavourite;
|
||||
|
||||
break;
|
||||
case self::ACTION_REMOVE:
|
||||
if ($existing)
|
||||
{
|
||||
$deleteResult = BlockFavouriteTable::delete($existing['ID']);
|
||||
if ($deleteResult->isSuccess())
|
||||
{
|
||||
$result->setResult(['status' => self::STATUS_DELETED]);
|
||||
}
|
||||
else
|
||||
{
|
||||
$error = new \Bitrix\Landing\Error;
|
||||
$error->addError('DB_ERROR_REMOVE', $deleteResult->getErrorMessages());
|
||||
$result->setError($error);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$result->setResult(['status' => self::STATUS_NOT_FOUND]);
|
||||
}
|
||||
$metrikaEvent = Metrika\Events::deleteFavourite;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (isset($metrikaEvent))
|
||||
{
|
||||
$metrika = new Metrika\Metrika(Metrika\Categories::getBySiteType($type), $metrikaEvent);
|
||||
$metrika
|
||||
->setSection(Metrika\Sections::siteEditor)
|
||||
->setSubSection('code_' . $codeBlock)
|
||||
->send()
|
||||
;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of block codes marked as favourite by the current user.
|
||||
*
|
||||
* @return PublicActionResult Result object containing an array of favourite block codes or error information.
|
||||
*/
|
||||
public static function getFavouriteBlocks(): PublicActionResult
|
||||
{
|
||||
$result = new PublicActionResult();
|
||||
$userId = Manager::getUserId();
|
||||
|
||||
if ($userId <= 0)
|
||||
{
|
||||
$error = new \Bitrix\Landing\Error;
|
||||
$error->addError('INVALID_USER', 'Invalid user');
|
||||
$result->setError($error);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$codes = [];
|
||||
$res = BlockFavouriteTable::getList([
|
||||
'filter' => [
|
||||
'=USER_ID' => $userId
|
||||
],
|
||||
'select' => ['CODE'],
|
||||
'order' => ['DATE_CREATE' => 'DESC']
|
||||
]);
|
||||
|
||||
while ($row = $res->fetch())
|
||||
{
|
||||
$codes[] = $row['CODE'];
|
||||
}
|
||||
|
||||
$result->setResult($codes);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark delete or not the block.
|
||||
* @param int $lid Id of landing.
|
||||
|
||||
@@ -74,134 +74,6 @@ class RepoWidget extends Repo
|
||||
$manifest = Scope\Mainpage::prepareBlockManifest($manifest);
|
||||
}
|
||||
|
||||
// todo: move to non-rest namespace?
|
||||
/**
|
||||
* @param int $blockId
|
||||
* @param array $params
|
||||
* @return PublicActionResult
|
||||
*/
|
||||
public static function fetchData(int $blockId, array $params = []): PublicActionResult
|
||||
{
|
||||
$result = new PublicActionResult();
|
||||
$result->setResult(false);
|
||||
$error = new Landing\Error;
|
||||
|
||||
$block = new Landing\Block($blockId);
|
||||
if (!$block->getId())
|
||||
{
|
||||
$error->addError(
|
||||
'BLOCK_NOT_FOUND',
|
||||
Loc::getMessage('LANDING_WIDGET_BLOCK_NOT_FOUND')
|
||||
);
|
||||
$result->setError($error);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
if (!Loader::includeModule('rest'))
|
||||
{
|
||||
$error->addError(
|
||||
'REST_NOT_FOUND',
|
||||
Loc::getMessage('LANDING_WIDGET_REST_NOT_FOUND')
|
||||
);
|
||||
$result->setError($error);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
// check app
|
||||
$repoId = $block->getRepoId();
|
||||
$app = Landing\Repo::getAppInfo($repoId);
|
||||
if (
|
||||
!$repoId
|
||||
|| empty($app)
|
||||
|| !isset($app['CLIENT_ID'])
|
||||
)
|
||||
{
|
||||
$error->addError(
|
||||
'APP_NOT_FOUND',
|
||||
Loc::getMessage('LANDING_WIDGET_APP_NOT_FOUND')
|
||||
);
|
||||
$result->setError($error);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
// check subtype
|
||||
$manifest = $block->getManifest();
|
||||
if (
|
||||
!in_array(self::SUBTYPE_WIDGET, (array)$manifest['block']['subtype'], true)
|
||||
|| !is_array($manifest['block']['subtype_params'])
|
||||
|| !isset($manifest['block']['subtype_params']['handler'])
|
||||
)
|
||||
{
|
||||
$error->addError(
|
||||
'HANDLER_NOT_FOUND',
|
||||
Loc::getMessage('LANDING_WIDGET_HANDLER_NOT_FOUND_2')
|
||||
);
|
||||
$result->setError($error);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
// get auth
|
||||
$auth = Rest\Application::getAuthProvider()->get(
|
||||
$app['CLIENT_ID'],
|
||||
'landing',
|
||||
[],
|
||||
Manager::getUserId()
|
||||
);
|
||||
if (isset($auth['error']))
|
||||
{
|
||||
$error->addError(
|
||||
'APP_AUTH_ERROR__' . $auth['error'],
|
||||
$auth['error_description'] ?? ''
|
||||
);
|
||||
$result->setError($error);
|
||||
|
||||
return $result;
|
||||
}
|
||||
$params['auth'] = $auth;
|
||||
|
||||
// request
|
||||
$url = (string)$manifest['block']['subtype_params']['handler'];
|
||||
$http = new HttpClient();
|
||||
$data = $http->post(
|
||||
$url,
|
||||
$params
|
||||
);
|
||||
|
||||
if ($http->getStatus() !== 200)
|
||||
{
|
||||
$error->addError(
|
||||
'HANDLER_NOT_ALLOW',
|
||||
Loc::getMessage('LANDING_WIDGET_HANDLER_NOT_ALLOW')
|
||||
);
|
||||
$result->setError($error);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$type = empty($params) ? 'fetch' : 'fetch_params';
|
||||
UsageStatTable::logLandingWidget($app['CLIENT_ID'], $type);
|
||||
UsageStatTable::finalize();
|
||||
|
||||
if (isset($data['error']))
|
||||
{
|
||||
$error->addError(
|
||||
$data['error'],
|
||||
$data['error_description'] ?? ''
|
||||
);
|
||||
$result->setError($error);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result->setResult($data);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable widgets debug logging
|
||||
* @param string $appCode
|
||||
|
||||
@@ -54,4 +54,14 @@ abstract class Scope
|
||||
* @return array
|
||||
*/
|
||||
abstract public static function getExcludedHooks(): array;
|
||||
|
||||
/**
|
||||
* Check is scope can use extension
|
||||
* @param string $code - name of extension
|
||||
* @return bool
|
||||
*/
|
||||
public static function isExtensionAllow(string $code): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Bitrix\Landing\Site\Scope;
|
||||
|
||||
use Bitrix\Landing\Block\BlockRepo;
|
||||
use Bitrix\Landing\Landing;
|
||||
use Bitrix\Landing\Role;
|
||||
use Bitrix\Landing\Manager;
|
||||
use Bitrix\Landing\Domain;
|
||||
@@ -15,6 +17,14 @@ use Bitrix\Main\EventManager;
|
||||
*/
|
||||
class Mainpage extends Scope
|
||||
{
|
||||
private const ALLOWED_EXTENSIONS = [
|
||||
'landing.widgetvue',
|
||||
'landing_inline_video',
|
||||
'landing_carousel',
|
||||
'landing_jquery',
|
||||
'landing_icon_fonts',
|
||||
];
|
||||
|
||||
/**
|
||||
* Method for first time initialization scope.
|
||||
* @param array $params Additional params.
|
||||
@@ -29,7 +39,7 @@ class Mainpage extends Scope
|
||||
$eventManager->addEventHandler(
|
||||
'landing',
|
||||
'onBlockRepoSetFilters',
|
||||
function(Event $event)
|
||||
function (Event $event)
|
||||
{
|
||||
$result = new Entity\EventResult();
|
||||
$result->modifyFields([
|
||||
@@ -131,7 +141,8 @@ class Mainpage extends Scope
|
||||
];
|
||||
$manifest = array_filter(
|
||||
$manifest,
|
||||
function ($key) use ($allowedManifestKeys) {
|
||||
function ($key) use ($allowedManifestKeys)
|
||||
{
|
||||
return in_array(mb_strtolower($key), $allowedManifestKeys);
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
@@ -142,17 +153,12 @@ class Mainpage extends Scope
|
||||
// not all assets allowed
|
||||
if (isset($manifest['assets']))
|
||||
{
|
||||
$allowedExt = [
|
||||
'landing.widgetvue',
|
||||
'landing_inline_video',
|
||||
'landing_carousel',
|
||||
];
|
||||
$manifest['assets'] = [
|
||||
'ext' => array_filter(
|
||||
(array)$manifest['assets']['ext'],
|
||||
function ($item) use ($allowedExt)
|
||||
static function ($item)
|
||||
{
|
||||
return in_array(mb_strtolower($item), $allowedExt);
|
||||
return in_array(mb_strtolower($item), self::ALLOWED_EXTENSIONS, true);
|
||||
}
|
||||
),
|
||||
];
|
||||
@@ -171,7 +177,8 @@ class Mainpage extends Scope
|
||||
];
|
||||
$manifest['block']['subtype'] = array_filter(
|
||||
(array)$manifest['block']['subtype'],
|
||||
function ($item) use ($allowedSubtypes) {
|
||||
function ($item) use ($allowedSubtypes)
|
||||
{
|
||||
return in_array(mb_strtolower($item), $allowedSubtypes);
|
||||
}
|
||||
);
|
||||
@@ -190,7 +197,8 @@ class Mainpage extends Scope
|
||||
];
|
||||
$manifest['callbacks'] = array_filter(
|
||||
(array)$manifest['callbacks'],
|
||||
function ($item) use ($allowedCallbacks) {
|
||||
function ($item) use ($allowedCallbacks)
|
||||
{
|
||||
return in_array(mb_strtolower($item), $allowedCallbacks);
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
@@ -201,7 +209,7 @@ class Mainpage extends Scope
|
||||
unset($manifest['callbacks']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//unset not allowed style
|
||||
$allowedStyles = [
|
||||
//for landing block
|
||||
@@ -217,7 +225,12 @@ class Mainpage extends Scope
|
||||
'margin-left',
|
||||
'margin-right',
|
||||
'text-align',
|
||||
'font-size',
|
||||
'font-family',
|
||||
'font-weight',
|
||||
'button',
|
||||
'text-transform',
|
||||
'container-max-width',
|
||||
//for widget
|
||||
'widget',
|
||||
'widget-type',
|
||||
@@ -266,4 +279,14 @@ class Mainpage extends Scope
|
||||
|
||||
return $manifest;
|
||||
}
|
||||
|
||||
public static function isExtensionAllow(string $code): bool
|
||||
{
|
||||
if (Landing::getEditMode())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return in_array($code, self::ALLOWED_EXTENSIONS, true);
|
||||
}
|
||||
}
|
||||
@@ -281,4 +281,19 @@ class Type
|
||||
|
||||
return $manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check is current scope can use extension
|
||||
* @param string $code - name of extension
|
||||
* @return bool
|
||||
*/
|
||||
public static function isExtensionAllow(string $code): bool
|
||||
{
|
||||
if (self::$currentScopeClass !== null)
|
||||
{
|
||||
return self::$currentScopeClass::isExtensionAllow($code);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ class Form
|
||||
{
|
||||
$content = self::replaceFormMarkers($content);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
@@ -152,9 +153,10 @@ class Form
|
||||
|
||||
//add class g-cursor-pointer
|
||||
preg_match_all('/(class="[^"]*)/i', $matches['pre'], $matchesPre);
|
||||
$matches['pre'] = str_replace($matchesPre[1][0], $matchesPre[1][0]. ' g-cursor-pointer', $matches['pre']);
|
||||
$matches['pre'] =
|
||||
str_replace($matchesPre[1][0], $matchesPre[1][0] . ' g-cursor-pointer', $matches['pre']);
|
||||
|
||||
return $script . $matches['pre'] . ' '. $matches['pre2'];
|
||||
return $script . $matches['pre'] . ' ' . $matches['pre2'];
|
||||
}
|
||||
|
||||
return $matches[0];
|
||||
@@ -200,7 +202,7 @@ class Form
|
||||
foreach ($sites as $site)
|
||||
{
|
||||
Site::update($site, [
|
||||
'DATE_MODIFY' => false
|
||||
'DATE_MODIFY' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -279,7 +281,7 @@ class Form
|
||||
$forms[$form['ID']] = $form;
|
||||
}
|
||||
}
|
||||
else if (isset($res['error']))
|
||||
elseif (isset($res['error']))
|
||||
{
|
||||
self::$errors[] = [
|
||||
'code' => $res['error'],
|
||||
@@ -330,11 +332,11 @@ class Form
|
||||
return self::getFormsByFilter(['=IS_CALLBACK_FORM' => 'Y', '=ACTIVE' => 'Y']);
|
||||
}
|
||||
|
||||
protected static function getFormsByFilter(array $filter): array
|
||||
protected static function getFormsByFilter(array $filter, bool $force = false): array
|
||||
{
|
||||
static $cache = [];
|
||||
$cacheKey = serialize($filter);
|
||||
if (array_key_exists($cacheKey, $cache))
|
||||
if (array_key_exists($cacheKey, $cache) && !$force)
|
||||
{
|
||||
return $cache[$cacheKey];
|
||||
}
|
||||
@@ -343,12 +345,9 @@ class Form
|
||||
$filter,
|
||||
static function ($key)
|
||||
{
|
||||
$key = trim($key);
|
||||
$equalKey = ltrim($key, '=');
|
||||
return
|
||||
in_array($key, self::AVAILABLE_FORM_FIELDS, true)
|
||||
|| in_array($equalKey, self::AVAILABLE_FORM_FIELDS, true)
|
||||
;
|
||||
$clearKey = preg_replace('/^[^A-Z]*/', '', $key);
|
||||
|
||||
return in_array($clearKey, self::AVAILABLE_FORM_FIELDS, true);
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
@@ -365,7 +364,8 @@ class Form
|
||||
$filtred = true;
|
||||
foreach ($filter as $key => $value)
|
||||
{
|
||||
if (!$form[$key] || $form[$key] !== $value)
|
||||
$clearKey = preg_replace('/[^a-zA-Z0-9]/', '', $key);
|
||||
if (!$form[$clearKey] || $form[$clearKey] !== $value)
|
||||
{
|
||||
$filtred = false;
|
||||
break;
|
||||
@@ -431,7 +431,7 @@ class Form
|
||||
{
|
||||
$link = '/crm/webform/';
|
||||
}
|
||||
else if (Manager::isB24Connector())
|
||||
elseif (Manager::isB24Connector())
|
||||
{
|
||||
$link = '/bitrix/admin/b24connector_crm_forms.php?lang=' . LANGUAGE_ID;
|
||||
}
|
||||
@@ -475,8 +475,8 @@ class Form
|
||||
{
|
||||
// try to get 1) default callback form 2) last added form 3) create new form
|
||||
$forms = self::getFormsByFilter([
|
||||
'=XML_ID' => 'crm_preset_fb'
|
||||
]);
|
||||
'=XML_ID' => 'crm_preset_fb',
|
||||
], true);
|
||||
$forms = self::prepareFormsToAttrs($forms);
|
||||
if (empty($forms))
|
||||
{
|
||||
@@ -604,11 +604,13 @@ class Form
|
||||
'attribute' => self::ATTR_FORM_PARAMS,
|
||||
'type' => 'list',
|
||||
'items' => !empty(self::$errors)
|
||||
? array_map(fn ($item) => ['name' => $item['message'], 'value' => false], self::$errors)
|
||||
: [[
|
||||
'name' => Loc::getMessage('LANDING_BLOCK_WEBFORM_NO_FORM'),
|
||||
'value' => false,
|
||||
]],
|
||||
? array_map(fn($item) => ['name' => $item['message'], 'value' => false], self::$errors)
|
||||
: [
|
||||
[
|
||||
'name' => Loc::getMessage('LANDING_BLOCK_WEBFORM_NO_FORM'),
|
||||
'value' => false,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -622,7 +624,8 @@ class Form
|
||||
*/
|
||||
protected static function prepareFormsToAttrs(array $forms): array
|
||||
{
|
||||
$sorted = [];
|
||||
$callback = [];
|
||||
$other = [];
|
||||
foreach ($forms as $form)
|
||||
{
|
||||
if (array_key_exists('ACTIVE', $form) && $form['ACTIVE'] !== 'Y')
|
||||
@@ -637,15 +640,15 @@ class Form
|
||||
|
||||
if ($form['IS_CALLBACK_FORM'] === 'Y')
|
||||
{
|
||||
$sorted[] = $item;
|
||||
$callback[] = $item;
|
||||
}
|
||||
else
|
||||
{
|
||||
array_unshift($sorted, $item);
|
||||
$other[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
return $sorted;
|
||||
return $callback + $other;
|
||||
}
|
||||
// endregion
|
||||
|
||||
@@ -678,8 +681,7 @@ class Form
|
||||
'CONTENT' => '%data-b24form=%',
|
||||
],
|
||||
]
|
||||
)->fetchAll()
|
||||
;
|
||||
)->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -694,6 +696,7 @@ class Form
|
||||
{
|
||||
return (int)$matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -736,7 +739,7 @@ class Form
|
||||
*/
|
||||
protected static function createDefaultForm(): array
|
||||
{
|
||||
if ($formId = self::createForm([]))
|
||||
if ($formId = self::createForm(['XML_ID' => 'crm_preset_fb']))
|
||||
{
|
||||
return self::getFormsByFilter(['=ID' => $formId]);
|
||||
}
|
||||
@@ -754,9 +757,11 @@ class Form
|
||||
{
|
||||
$form = new WebForm\Form;
|
||||
|
||||
$defaultData = WebForm\Preset::getById('crm_preset_cd');
|
||||
$xmlId = $formData['XML_ID'] ?? 'crm_preset_cd';
|
||||
|
||||
$defaultData['XML_ID'] = '';
|
||||
$defaultData = WebForm\Preset::getById($xmlId) ?? [];
|
||||
|
||||
$defaultData['XML_ID'] = $xmlId;
|
||||
$defaultData['ACTIVE'] = 'Y';
|
||||
$defaultData['IS_SYSTEM'] = 'N';
|
||||
$defaultData['IS_CALLBACK_FORM'] = 'N';
|
||||
@@ -770,7 +775,7 @@ class Form
|
||||
$defaultData['AGREEMENT_ID'] = $agreementId;
|
||||
}
|
||||
|
||||
$isLeadEnabled = LeadSettings::getCurrent()->isEnabled();
|
||||
$isLeadEnabled = LeadSettings::getCurrent()?->isEnabled();
|
||||
$defaultData['ENTITY_SCHEME'] = (string)(
|
||||
$isLeadEnabled
|
||||
? WebForm\Entity::ENUM_ENTITY_SCHEME_LEAD
|
||||
|
||||
@@ -205,16 +205,10 @@ class WidgetVue
|
||||
}
|
||||
|
||||
$vueParams = Json::encode($vueParams);
|
||||
$type = Landing\Site\Scope::getCurrentScopeId();
|
||||
|
||||
return "
|
||||
<script>
|
||||
(() => {
|
||||
if (BX.Landing.Env)
|
||||
{
|
||||
BX.Landing.Env.getInstance().setType('{$type}');
|
||||
}
|
||||
|
||||
const init = () => {
|
||||
(new BX.Landing.WidgetVue(
|
||||
{$vueParams}
|
||||
|
||||
@@ -926,9 +926,9 @@ class Landing
|
||||
protected static function prepareAdditionalFields(array $data, array $additional, array $ratio = null): array
|
||||
{
|
||||
$data['ADDITIONAL_FIELDS']['THEME_USE'] = 'N';
|
||||
if (isset($additional['theme']) || isset($additional['theme_use_site']))
|
||||
if (isset($additional['theme']))
|
||||
{
|
||||
$color = $additional['theme_use_site'] ?? $additional['theme'];
|
||||
$color = $additional['theme'];
|
||||
if ($color[0] !== '#')
|
||||
{
|
||||
$color = '#'.$color;
|
||||
@@ -938,7 +938,7 @@ class Landing
|
||||
|
||||
// for variant if import only page in existing site
|
||||
$isSinglePage = !is_array($ratio) || empty($ratio);
|
||||
if ($isSinglePage && !$additional['theme_use_site'])
|
||||
if ($isSinglePage)
|
||||
{
|
||||
$data['ADDITIONAL_FIELDS']['THEME_USE'] = 'Y';
|
||||
}
|
||||
@@ -1197,7 +1197,9 @@ class Landing
|
||||
{
|
||||
if (array_key_exists('data-end-date', $attrItem))
|
||||
{
|
||||
$neededAttr = $attrItem['data-end-date'] / 1000;
|
||||
$neededAttr = is_numeric($attrItem['data-end-date'])
|
||||
? (int)$attrItem['data-end-date'] / 1000
|
||||
: 0;
|
||||
$currenDate = time();
|
||||
if ($neededAttr < $currenDate)
|
||||
{
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
<?php
|
||||
namespace Bitrix\Landing\Zip\Nginx;
|
||||
|
||||
use \Bitrix\Main\Context;
|
||||
use \Bitrix\Main\Loader;
|
||||
|
||||
class Archive
|
||||
{
|
||||
/**
|
||||
* Archive name.
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* Archive Entries.
|
||||
* @var ArchiveEntry[]
|
||||
*/
|
||||
protected $entries = [];
|
||||
|
||||
/**
|
||||
* Archive constructor.
|
||||
* @param string $name Archive name.
|
||||
*/
|
||||
public function __construct($name)
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add one entry. in current archive.
|
||||
* @param ArchiveEntry $archiveEntry Entry for archive.
|
||||
*/
|
||||
public function addEntry($archiveEntry)
|
||||
{
|
||||
if ($archiveEntry instanceof ArchiveEntry)
|
||||
{
|
||||
$this->entries[] = $archiveEntry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the archive does not have entries.
|
||||
* @return bool
|
||||
*/
|
||||
public function isEmpty()
|
||||
{
|
||||
return empty($this->entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return entries as string.
|
||||
* @return string
|
||||
*/
|
||||
protected function getFileList()
|
||||
{
|
||||
$list = [];
|
||||
foreach ($this->entries as $entry)
|
||||
{
|
||||
$list[] = (string)$entry;
|
||||
}
|
||||
unset($entry);
|
||||
|
||||
return implode("\n", $list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add necessary headers.
|
||||
* @return void
|
||||
* @throws \Bitrix\Main\ArgumentNullException
|
||||
*/
|
||||
protected function addHeaders()
|
||||
{
|
||||
$httpResponse = Context::getCurrent()->getResponse();
|
||||
$httpResponse->addHeader('X-Archive-Files', 'zip');
|
||||
|
||||
$utfName = \CHTTP::urnEncode($this->name, 'UTF-8');
|
||||
$translitName = \CUtil::translit($this->name, LANGUAGE_ID, [
|
||||
'max_len' => 1024,
|
||||
'safe_chars' => '.',
|
||||
'replace_space' => '-',
|
||||
]);
|
||||
$httpResponse->addHeader(
|
||||
'Content-Disposition',
|
||||
"attachment; filename=\"" . $translitName . "\"; filename*=utf-8''" . $utfName
|
||||
);
|
||||
|
||||
unset($utfName, $translitName, $httpResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends content to output stream and sets necessary headers.
|
||||
* @return void
|
||||
*/
|
||||
public function send()
|
||||
{
|
||||
if (!$this->isEmpty())
|
||||
{
|
||||
$this->disableCompression();
|
||||
$this->addHeaders();
|
||||
Context::getCurrent()->getResponse()->flush(
|
||||
$this->getFileList()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable compression of module compression.
|
||||
*/
|
||||
protected function disableCompression()
|
||||
{
|
||||
if (Loader::includeModule('compression'))
|
||||
{
|
||||
\CCompress::disableCompression();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
<?php
|
||||
namespace Bitrix\Landing\Zip\Nginx;
|
||||
|
||||
use \Bitrix\Main\Text\Encoding;
|
||||
use \Bitrix\Main\Config\Option;
|
||||
use \Bitrix\Landing\File;
|
||||
use \Bitrix\Landing\Manager;
|
||||
|
||||
class ArchiveEntry
|
||||
{
|
||||
/**
|
||||
* File name in entry.
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* File path in entry.
|
||||
* @var string
|
||||
*/
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* File id.
|
||||
* @var string
|
||||
*/
|
||||
protected $fileId;
|
||||
|
||||
/**
|
||||
* File size in entry.
|
||||
* @var string
|
||||
*/
|
||||
protected $size;
|
||||
|
||||
/**
|
||||
* Entry constructor.
|
||||
*/
|
||||
protected function __construct()
|
||||
{
|
||||
$this->fileId = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Entry from file path.
|
||||
* @param string $filePath File id from b_file.
|
||||
* @return static
|
||||
*/
|
||||
public static function createFromFilePath($filePath)
|
||||
{
|
||||
$fileArray = \CFile::MakeFileArray($filePath);
|
||||
|
||||
if ($fileArray)
|
||||
{
|
||||
return self::createFromFile([
|
||||
'ID' => 0,
|
||||
'ORIGINAL_NAME' => $fileArray['name'],
|
||||
'FILE_SIZE' => $fileArray['size'],
|
||||
'SRC' => substr(
|
||||
$fileArray['tmp_name'],
|
||||
strlen(Manager::getDocRoot())
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Entry from file id (from b_file).
|
||||
* @param int $fileId File id from b_file.
|
||||
* @return static
|
||||
*/
|
||||
public static function createFromFileId($fileId)
|
||||
{
|
||||
$fileArray = File::getFileArray($fileId);
|
||||
|
||||
if (
|
||||
!$fileArray ||
|
||||
empty($fileArray['SRC'])
|
||||
)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return self::createFromFile($fileArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Entry from file array.
|
||||
* @param array $fileArray File id from b_file.
|
||||
* @return static
|
||||
*/
|
||||
protected static function createFromFile(array $fileArray)
|
||||
{
|
||||
$zipEntry = new static;
|
||||
$zipEntry->name = $fileArray['ORIGINAL_NAME'];
|
||||
$zipEntry->fileId = $fileArray['ID'];
|
||||
$zipEntry->size = $fileArray['FILE_SIZE'];
|
||||
|
||||
$fromClouds = false;
|
||||
$filename = $fileArray['SRC'];
|
||||
|
||||
if (isset($fileArray['HANDLER_ID']) && !empty($fileArray['HANDLER_ID']))
|
||||
{
|
||||
$fromClouds = true;
|
||||
}
|
||||
|
||||
unset($fileArray);
|
||||
|
||||
if ($fromClouds)
|
||||
{
|
||||
$filename = preg_replace('~^(http[s]?)(\://)~i', '\\1.' , $filename);
|
||||
$cloudUploadPath = Option::get(
|
||||
'main',
|
||||
'bx_cloud_upload',
|
||||
'/upload/bx_cloud_upload/'
|
||||
);
|
||||
$zipEntry->path = $cloudUploadPath . $filename;
|
||||
unset($cloudUploadPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
$zipEntry->path = self::encodeUrn(
|
||||
Encoding::convertEncoding($filename, LANG_CHARSET, 'UTF-8')
|
||||
);
|
||||
}
|
||||
unset($filename);
|
||||
|
||||
return $zipEntry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes uri: explodes uri by / and encodes in UTF-8 and rawurlencodes.
|
||||
* @param string $uri Uri.
|
||||
* @return string
|
||||
*/
|
||||
protected function encodeUrn($uri)
|
||||
{
|
||||
$result = '';
|
||||
$parts = preg_split(
|
||||
"#(://|:\\d+/|/|\\?|=|&)#", $uri, -1, PREG_SPLIT_DELIM_CAPTURE
|
||||
);
|
||||
|
||||
foreach ($parts as $i => $part)
|
||||
{
|
||||
$part = Manager::getApplication()->convertCharset(
|
||||
$part,
|
||||
LANG_CHARSET,
|
||||
'UTF-8'
|
||||
);
|
||||
$result .= ($i % 2)
|
||||
? $part
|
||||
: rawurlencode($part);
|
||||
}
|
||||
unset($parts, $i, $part);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns representation zip entry as string.
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
$name = Encoding::convertEncoding(
|
||||
$this->name,
|
||||
LANG_CHARSET,
|
||||
'UTF-8'
|
||||
);
|
||||
return "- {$this->size} {$this->path} /upload/{$this->fileId}/{$name}";
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
namespace Bitrix\Landing\Zip\Nginx;
|
||||
|
||||
use \Bitrix\Landing\Manager;
|
||||
use \Bitrix\Main\ModuleManager;
|
||||
|
||||
class Config
|
||||
{
|
||||
/**
|
||||
* Enable or not main option.
|
||||
* @return bool
|
||||
*/
|
||||
public static function serviceEnabled()
|
||||
{
|
||||
if (ModuleManager::isModuleInstalled('bitrix24'))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Manager::getOption('enable_mod_zip', 'N') == 'Y';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user