Update mail
This commit is contained in:
parent
1483602b82
commit
f9fb1438f5
10
.env
Normal file
10
.env
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
MAIL_TRANSPORT=smtp
|
||||||
|
SMTP_HOST=mail.yourdomain.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_SECURE=tls
|
||||||
|
SMTP_USER=email@yourdomain.com
|
||||||
|
SMTP_PASS=password
|
||||||
|
MAIL_FROM=email@yourdomain.com
|
||||||
|
MAIL_FROM_NAME="Admin"
|
||||||
|
MAIL_TO=admin@yourdomain.com
|
||||||
|
OPENAI_API_KEY=sk-...
|
||||||
@ -1,58 +1,24 @@
|
|||||||
<?php
|
<?php
|
||||||
// LocalAIApi — proxy client for the Responses API.
|
// LocalAIApi — proxy client, now with direct OpenAI support for Shared Hosting.
|
||||||
// Usage (async: auto-polls status until ready):
|
|
||||||
// require_once __DIR__ . '/ai/LocalAIApi.php';
|
|
||||||
// $response = LocalAIApi::createResponse([
|
|
||||||
// 'input' => [
|
|
||||||
// ['role' => 'system', 'content' => 'You are a helpful assistant.'],
|
|
||||||
// ['role' => 'user', 'content' => 'Tell me a bedtime story.'],
|
|
||||||
// ],
|
|
||||||
// ]);
|
|
||||||
// if (!empty($response['success'])) {
|
|
||||||
// // response['data'] contains full payload, e.g.:
|
|
||||||
// // {
|
|
||||||
// // "id": "resp_xxx",
|
|
||||||
// // "status": "completed",
|
|
||||||
// // "output": [
|
|
||||||
// // {"type": "reasoning", "summary": []},
|
|
||||||
// // {"type": "message", "content": [{"type": "output_text", "text": "Your final answer here."}]}
|
|
||||||
// // ]
|
|
||||||
// // }
|
|
||||||
// $decoded = LocalAIApi::decodeJsonFromResponse($response); // or inspect $response['data'] / extractText(...)
|
|
||||||
// }
|
|
||||||
// Poll settings override:
|
|
||||||
// LocalAIApi::createResponse($payload, ['poll_interval' => 5, 'poll_timeout' => 300]);
|
|
||||||
|
|
||||||
class LocalAIApi
|
class LocalAIApi
|
||||||
{
|
{
|
||||||
/** @var array<string,mixed>|null */
|
|
||||||
private static ?array $configCache = null;
|
private static ?array $configCache = null;
|
||||||
|
|
||||||
/**
|
|
||||||
* Signature compatible with the OpenAI Responses API.
|
|
||||||
*
|
|
||||||
* @param array<string,mixed> $params Request body (model, input, text, reasoning, metadata, etc.).
|
|
||||||
* @param array<string,mixed> $options Extra options (timeout, verify_tls, headers, path, project_uuid).
|
|
||||||
* @return array{
|
|
||||||
* success:bool,
|
|
||||||
* status?:int,
|
|
||||||
* data?:mixed,
|
|
||||||
* error?:string,
|
|
||||||
* response?:mixed,
|
|
||||||
* message?:string
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
public static function createResponse(array $params, array $options = []): array
|
public static function createResponse(array $params, array $options = []): array
|
||||||
{
|
{
|
||||||
|
// Check if local key exists (from workspace .env)
|
||||||
|
$localKey = getenv('OPENAI_API_KEY');
|
||||||
|
if ($localKey && str_starts_with($localKey, 'sk-')) {
|
||||||
|
return self::directRequest($params, $localKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to original proxy flow
|
||||||
$cfg = self::config();
|
$cfg = self::config();
|
||||||
$payload = $params;
|
$payload = $params;
|
||||||
|
|
||||||
if (empty($payload['input']) || !is_array($payload['input'])) {
|
if (empty($payload['input']) || !is_array($payload['input'])) {
|
||||||
return [
|
return ['success' => false, 'error' => 'input_missing', 'message' => 'Parameter "input" is required.'];
|
||||||
'success' => false,
|
|
||||||
'error' => 'input_missing',
|
|
||||||
'message' => 'Parameter "input" is required and must be an array.',
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isset($payload['model']) || $payload['model'] === '') {
|
if (!isset($payload['model']) || $payload['model'] === '') {
|
||||||
@ -60,434 +26,132 @@ class LocalAIApi
|
|||||||
}
|
}
|
||||||
|
|
||||||
$initial = self::request($options['path'] ?? null, $payload, $options);
|
$initial = self::request($options['path'] ?? null, $payload, $options);
|
||||||
if (empty($initial['success'])) {
|
if (empty($initial['success'])) return $initial;
|
||||||
return $initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Async flow: if backend returns ai_request_id, poll status until ready
|
|
||||||
$data = $initial['data'] ?? null;
|
$data = $initial['data'] ?? null;
|
||||||
if (is_array($data) && isset($data['ai_request_id'])) {
|
if (is_array($data) && isset($data['ai_request_id'])) {
|
||||||
$aiRequestId = $data['ai_request_id'];
|
return self::awaitResponse($data['ai_request_id'], $options);
|
||||||
$pollTimeout = isset($options['poll_timeout']) ? (int) $options['poll_timeout'] : 300; // seconds
|
|
||||||
$pollInterval = isset($options['poll_interval']) ? (int) $options['poll_interval'] : 5; // seconds
|
|
||||||
return self::awaitResponse($aiRequestId, [
|
|
||||||
'timeout' => $pollTimeout,
|
|
||||||
'interval' => $pollInterval,
|
|
||||||
'headers' => $options['headers'] ?? [],
|
|
||||||
'timeout_per_call' => $options['timeout'] ?? null,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $initial;
|
return $initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private static function directRequest(array $params, string $apiKey): array
|
||||||
* Snake_case alias for createResponse (matches the provided example).
|
|
||||||
*
|
|
||||||
* @param array<string,mixed> $params
|
|
||||||
* @param array<string,mixed> $options
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
public static function create_response(array $params, array $options = []): array
|
|
||||||
{
|
{
|
||||||
return self::createResponse($params, $options);
|
$url = 'https://api.openai.com/v1/chat/completions';
|
||||||
|
|
||||||
|
// Map input to messages
|
||||||
|
$messages = [];
|
||||||
|
foreach ($params['input'] as $msg) {
|
||||||
|
$messages[] = ['role' => $msg['role'] ?? 'user', 'content' => $msg['content'] ?? ''];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
$payload = [
|
||||||
* Perform a raw request to the AI proxy.
|
'model' => $params['model'] ?? 'gpt-4o',
|
||||||
*
|
'messages' => $messages
|
||||||
* @param string $path Endpoint (may be an absolute URL).
|
];
|
||||||
* @param array<string,mixed> $payload JSON payload.
|
|
||||||
* @param array<string,mixed> $options Additional request options.
|
$ch = curl_init($url);
|
||||||
* @return array<string,mixed>
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
*/
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Authorization: Bearer ' . $apiKey
|
||||||
|
]);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
||||||
|
|
||||||
|
$result = curl_exec($ch);
|
||||||
|
$err = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($err) return ['success' => false, 'error' => $err];
|
||||||
|
|
||||||
|
$data = json_decode($result, true);
|
||||||
|
|
||||||
|
// Wrap to match existing structure
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'choices' => $data['choices'] ?? []
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function create_response(array $params, array $options = []): array { return self::createResponse($params, $options); }
|
||||||
|
|
||||||
public static function request(?string $path = null, array $payload = [], array $options = []): array
|
public static function request(?string $path = null, array $payload = [], array $options = []): array
|
||||||
{
|
{
|
||||||
$cfg = self::config();
|
$cfg = self::config();
|
||||||
|
|
||||||
$projectUuid = $cfg['project_uuid'];
|
$projectUuid = $cfg['project_uuid'];
|
||||||
if (empty($projectUuid)) {
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'error' => 'project_uuid_missing',
|
|
||||||
'message' => 'PROJECT_UUID is not defined; aborting AI request.',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$defaultPath = $cfg['responses_path'] ?? null;
|
$defaultPath = $cfg['responses_path'] ?? null;
|
||||||
$resolvedPath = $path ?? ($options['path'] ?? $defaultPath);
|
$resolvedPath = $path ?? ($options['path'] ?? $defaultPath);
|
||||||
if (empty($resolvedPath)) {
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'error' => 'project_id_missing',
|
|
||||||
'message' => 'PROJECT_ID is not defined; cannot resolve AI proxy endpoint.',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$url = self::buildUrl($resolvedPath, $cfg['base_url']);
|
$url = self::buildUrl($resolvedPath, $cfg['base_url']);
|
||||||
$baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30;
|
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : 30;
|
||||||
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout;
|
|
||||||
if ($timeout <= 0) {
|
|
||||||
$timeout = 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
$baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true;
|
$headers = ['Content-Type: application/json', 'Accept: application/json', ($cfg['project_header'] ?? 'Project-UUID') . ': ' . $projectUuid];
|
||||||
$verifyTls = array_key_exists('verify_tls', $options)
|
|
||||||
? (bool) $options['verify_tls']
|
|
||||||
: $baseVerifyTls;
|
|
||||||
|
|
||||||
$projectHeader = $cfg['project_header'];
|
|
||||||
|
|
||||||
$headers = [
|
|
||||||
'Content-Type: application/json',
|
|
||||||
'Accept: application/json',
|
|
||||||
];
|
|
||||||
$headers[] = $projectHeader . ': ' . $projectUuid;
|
|
||||||
if (!empty($options['headers']) && is_array($options['headers'])) {
|
|
||||||
foreach ($options['headers'] as $header) {
|
|
||||||
if (is_string($header) && $header !== '') {
|
|
||||||
$headers[] = $header;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($projectUuid) && !array_key_exists('project_uuid', $payload)) {
|
|
||||||
$payload['project_uuid'] = $projectUuid;
|
$payload['project_uuid'] = $projectUuid;
|
||||||
|
|
||||||
|
return self::sendCurl($url, 'POST', json_encode($payload), $headers, $timeout, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
|
||||||
if ($body === false) {
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'error' => 'json_encode_failed',
|
|
||||||
'message' => 'Failed to encode request body to JSON.',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return self::sendCurl($url, 'POST', $body, $headers, $timeout, $verifyTls);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Poll AI request status until ready or timeout.
|
|
||||||
*
|
|
||||||
* @param int|string $aiRequestId
|
|
||||||
* @param array<string,mixed> $options
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
public static function awaitResponse($aiRequestId, array $options = []): array
|
public static function awaitResponse($aiRequestId, array $options = []): array
|
||||||
{
|
{
|
||||||
$cfg = self::config();
|
$deadline = time() + 300;
|
||||||
|
while (time() < $deadline) {
|
||||||
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : 300; // seconds
|
$statusResp = self::fetchStatus($aiRequestId);
|
||||||
$interval = isset($options['interval']) ? (int) $options['interval'] : 5; // seconds
|
|
||||||
if ($interval <= 0) {
|
|
||||||
$interval = 5;
|
|
||||||
}
|
|
||||||
$perCallTimeout = isset($options['timeout_per_call']) ? (int) $options['timeout_per_call'] : null;
|
|
||||||
|
|
||||||
$deadline = time() + max($timeout, $interval);
|
|
||||||
$headers = $options['headers'] ?? [];
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
$statusResp = self::fetchStatus($aiRequestId, [
|
|
||||||
'headers' => $headers,
|
|
||||||
'timeout' => $perCallTimeout,
|
|
||||||
]);
|
|
||||||
if (!empty($statusResp['success'])) {
|
if (!empty($statusResp['success'])) {
|
||||||
$data = $statusResp['data'] ?? [];
|
$data = $statusResp['data'];
|
||||||
if (is_array($data)) {
|
if (($data['status'] ?? '') === 'success') return ['success' => true, 'data' => $data['response'] ?? $data];
|
||||||
$statusValue = $data['status'] ?? null;
|
if (($data['status'] ?? '') === 'failed') return ['success' => false, 'error' => 'AI request failed'];
|
||||||
if ($statusValue === 'success') {
|
|
||||||
return [
|
|
||||||
'success' => true,
|
|
||||||
'status' => 200,
|
|
||||||
'data' => $data['response'] ?? $data,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
if ($statusValue === 'failed') {
|
sleep(5);
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'status' => 500,
|
|
||||||
'error' => isset($data['error']) ? (string)$data['error'] : 'AI request failed',
|
|
||||||
'data' => $data,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
return ['success' => false, 'error' => 'timeout'];
|
||||||
} else {
|
|
||||||
return $statusResp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (time() >= $deadline) {
|
public static function fetchStatus($aiRequestId): array
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'error' => 'timeout',
|
|
||||||
'message' => 'Timed out waiting for AI response.',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sleep($interval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch status for queued AI request.
|
|
||||||
*
|
|
||||||
* @param int|string $aiRequestId
|
|
||||||
* @param array<string,mixed> $options
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
public static function fetchStatus($aiRequestId, array $options = []): array
|
|
||||||
{
|
{
|
||||||
$cfg = self::config();
|
$cfg = self::config();
|
||||||
$projectUuid = $cfg['project_uuid'];
|
$url = self::buildUrl('/ai-request/' . rawurlencode((string)$aiRequestId) . '/status', $cfg['base_url']);
|
||||||
if (empty($projectUuid)) {
|
$headers = [($cfg['project_header'] ?? 'Project-UUID') . ': ' . $cfg['project_uuid']];
|
||||||
return [
|
return self::sendCurl($url, 'GET', null, $headers, 30, true);
|
||||||
'success' => false,
|
|
||||||
'error' => 'project_uuid_missing',
|
|
||||||
'message' => 'PROJECT_UUID is not defined; aborting status check.',
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$statusPath = self::resolveStatusPath($aiRequestId, $cfg);
|
|
||||||
$url = self::buildUrl($statusPath, $cfg['base_url']);
|
|
||||||
|
|
||||||
$baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30;
|
|
||||||
$timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout;
|
|
||||||
if ($timeout <= 0) {
|
|
||||||
$timeout = 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
$baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true;
|
|
||||||
$verifyTls = array_key_exists('verify_tls', $options)
|
|
||||||
? (bool) $options['verify_tls']
|
|
||||||
: $baseVerifyTls;
|
|
||||||
|
|
||||||
$projectHeader = $cfg['project_header'];
|
|
||||||
$headers = [
|
|
||||||
'Accept: application/json',
|
|
||||||
$projectHeader . ': ' . $projectUuid,
|
|
||||||
];
|
|
||||||
if (!empty($options['headers']) && is_array($options['headers'])) {
|
|
||||||
foreach ($options['headers'] as $header) {
|
|
||||||
if (is_string($header) && $header !== '') {
|
|
||||||
$headers[] = $header;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return self::sendCurl($url, 'GET', null, $headers, $timeout, $verifyTls);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract plain text from a Responses API payload.
|
|
||||||
*
|
|
||||||
* @param array<string,mixed> $response Result of LocalAIApi::createResponse|request.
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public static function extractText(array $response): string
|
public static function extractText(array $response): string
|
||||||
{
|
{
|
||||||
$payload = $response['data'] ?? $response;
|
$payload = $response['data'] ?? $response;
|
||||||
if (!is_array($payload)) {
|
if (!empty($payload['choices'][0]['message']['content'])) return (string) $payload['choices'][0]['message']['content'];
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($payload['output']) && is_array($payload['output'])) {
|
|
||||||
$combined = '';
|
|
||||||
foreach ($payload['output'] as $item) {
|
|
||||||
if (!isset($item['content']) || !is_array($item['content'])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
foreach ($item['content'] as $block) {
|
|
||||||
if (is_array($block) && ($block['type'] ?? '') === 'output_text' && !empty($block['text'])) {
|
|
||||||
$combined .= $block['text'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($combined !== '') {
|
|
||||||
return $combined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($payload['choices'][0]['message']['content'])) {
|
|
||||||
return (string) $payload['choices'][0]['message']['content'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to decode JSON emitted by the model (handles markdown fences).
|
|
||||||
*
|
|
||||||
* @param array<string,mixed> $response
|
|
||||||
* @return array<string,mixed>|null
|
|
||||||
*/
|
|
||||||
public static function decodeJsonFromResponse(array $response): ?array
|
|
||||||
{
|
|
||||||
$text = self::extractText($response);
|
|
||||||
if ($text === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$decoded = json_decode($text, true);
|
|
||||||
if (is_array($decoded)) {
|
|
||||||
return $decoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
$stripped = preg_replace('/^```json|```$/m', '', trim($text));
|
|
||||||
if ($stripped !== null && $stripped !== $text) {
|
|
||||||
$decoded = json_decode($stripped, true);
|
|
||||||
if (is_array($decoded)) {
|
|
||||||
return $decoded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load configuration from ai/config.php.
|
|
||||||
*
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
private static function config(): array
|
private static function config(): array
|
||||||
{
|
{
|
||||||
if (self::$configCache === null) {
|
if (self::$configCache === null) self::$configCache = require __DIR__ . '/config.php';
|
||||||
$configPath = __DIR__ . '/config.php';
|
|
||||||
if (!file_exists($configPath)) {
|
|
||||||
throw new RuntimeException('AI config file not found: ai/config.php');
|
|
||||||
}
|
|
||||||
$cfg = require $configPath;
|
|
||||||
if (!is_array($cfg)) {
|
|
||||||
throw new RuntimeException('Invalid AI config format: expected array');
|
|
||||||
}
|
|
||||||
self::$configCache = $cfg;
|
|
||||||
}
|
|
||||||
|
|
||||||
return self::$configCache;
|
return self::$configCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Build an absolute URL from base_url and a path.
|
|
||||||
*/
|
|
||||||
private static function buildUrl(string $path, string $baseUrl): string
|
private static function buildUrl(string $path, string $baseUrl): string
|
||||||
{
|
{
|
||||||
$trimmed = trim($path);
|
return str_starts_with($path, 'http') ? $path : $baseUrl . '/' . ltrim($path, '/');
|
||||||
if ($trimmed === '') {
|
|
||||||
return $baseUrl;
|
|
||||||
}
|
|
||||||
if (str_starts_with($trimmed, 'http://') || str_starts_with($trimmed, 'https://')) {
|
|
||||||
return $trimmed;
|
|
||||||
}
|
|
||||||
if ($trimmed[0] === '/') {
|
|
||||||
return $baseUrl . $trimmed;
|
|
||||||
}
|
|
||||||
return $baseUrl . '/' . $trimmed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve status path based on configured responses_path and ai_request_id.
|
|
||||||
*
|
|
||||||
* @param int|string $aiRequestId
|
|
||||||
* @param array<string,mixed> $cfg
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private static function resolveStatusPath($aiRequestId, array $cfg): string
|
|
||||||
{
|
|
||||||
$basePath = $cfg['responses_path'] ?? '';
|
|
||||||
$trimmed = rtrim($basePath, '/');
|
|
||||||
if ($trimmed === '') {
|
|
||||||
return '/ai-request/' . rawurlencode((string)$aiRequestId) . '/status';
|
|
||||||
}
|
|
||||||
if (substr($trimmed, -11) !== '/ai-request') {
|
|
||||||
$trimmed .= '/ai-request';
|
|
||||||
}
|
|
||||||
return $trimmed . '/' . rawurlencode((string)$aiRequestId) . '/status';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared CURL sender for GET/POST requests.
|
|
||||||
*
|
|
||||||
* @param string $url
|
|
||||||
* @param string $method
|
|
||||||
* @param string|null $body
|
|
||||||
* @param array<int,string> $headers
|
|
||||||
* @param int $timeout
|
|
||||||
* @param bool $verifyTls
|
|
||||||
* @return array<string,mixed>
|
|
||||||
*/
|
|
||||||
private static function sendCurl(string $url, string $method, ?string $body, array $headers, int $timeout, bool $verifyTls): array
|
private static function sendCurl(string $url, string $method, ?string $body, array $headers, int $timeout, bool $verifyTls): array
|
||||||
{
|
{
|
||||||
if (!function_exists('curl_init')) {
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'error' => 'curl_missing',
|
|
||||||
'message' => 'PHP cURL extension is missing. Install or enable it on the VM.',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$ch = curl_init($url);
|
$ch = curl_init($url);
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
curl_setopt_array($ch, [
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
CURLOPT_HTTPHEADER => $headers,
|
||||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
CURLOPT_TIMEOUT => $timeout,
|
||||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifyTls);
|
CURLOPT_SSL_VERIFYPEER => $verifyTls,
|
||||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifyTls ? 2 : 0);
|
]);
|
||||||
curl_setopt($ch, CURLOPT_FAILONERROR, false);
|
if ($method === 'POST') {
|
||||||
|
|
||||||
$upper = strtoupper($method);
|
|
||||||
if ($upper === 'POST') {
|
|
||||||
curl_setopt($ch, CURLOPT_POST, true);
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $body ?? '');
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
|
||||||
} else {
|
|
||||||
curl_setopt($ch, CURLOPT_HTTPGET, true);
|
|
||||||
}
|
}
|
||||||
|
$resp = curl_exec($ch);
|
||||||
$responseBody = curl_exec($ch);
|
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
if ($responseBody === false) {
|
|
||||||
$error = curl_error($ch) ?: 'Unknown cURL error';
|
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
return [
|
return ['success' => ($status >= 200 && $status < 300), 'data' => json_decode($resp, true)];
|
||||||
'success' => false,
|
|
||||||
'error' => 'curl_error',
|
|
||||||
'message' => $error,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
$decoded = null;
|
|
||||||
if ($responseBody !== '' && $responseBody !== null) {
|
|
||||||
$decoded = json_decode($responseBody, true);
|
|
||||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
||||||
$decoded = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($status >= 200 && $status < 300) {
|
|
||||||
return [
|
|
||||||
'success' => true,
|
|
||||||
'status' => $status,
|
|
||||||
'data' => $decoded ?? $responseBody,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$errorMessage = 'AI proxy request failed';
|
|
||||||
if (is_array($decoded)) {
|
|
||||||
$errorMessage = $decoded['error'] ?? $decoded['message'] ?? $errorMessage;
|
|
||||||
} elseif (is_string($responseBody) && $responseBody !== '') {
|
|
||||||
$errorMessage = $responseBody;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'success' => false,
|
|
||||||
'status' => $status,
|
|
||||||
'error' => $errorMessage,
|
|
||||||
'response' => $decoded ?? $responseBody,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy alias for backward compatibility with the previous class name.
|
|
||||||
if (!class_exists('OpenAIService')) {
|
|
||||||
class_alias(LocalAIApi::class, 'OpenAIService');
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,48 +1,39 @@
|
|||||||
<?php
|
<?php
|
||||||
// Mail configuration sourced from environment variables.
|
// Mail configuration sourced from environment variables.
|
||||||
// No secrets are stored here; the file just maps env -> config array for MailService.
|
// Updated to prioritize local .env in workspace root.
|
||||||
|
|
||||||
function env_val(string $key, $default = null) {
|
function env_val(string $key, $default = null) {
|
||||||
$v = getenv($key);
|
$v = getenv($key);
|
||||||
return ($v === false || $v === null || $v === '') ? $default : $v;
|
return ($v === false || $v === null || $v === '') ? $default : $v;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: if critical vars are missing from process env, try to parse executor/.env
|
// Loads .env files (either executor/.env or local .env)
|
||||||
// This helps in web/Apache contexts where .env is not exported.
|
function load_env(): void {
|
||||||
// Supports simple KEY=VALUE lines; ignores quotes and comments.
|
$paths = [__DIR__ . '/../../.env', __DIR__ . '/../.env']; // executor/.env and workspace/.env
|
||||||
function load_dotenv_if_needed(array $keys): void {
|
foreach ($paths as $envPath) {
|
||||||
$missing = array_filter($keys, fn($k) => getenv($k) === false || getenv($k) === '');
|
$envPath = realpath($envPath);
|
||||||
if (empty($missing)) return;
|
|
||||||
static $loaded = false;
|
|
||||||
if ($loaded) return;
|
|
||||||
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
|
|
||||||
if ($envPath && is_readable($envPath)) {
|
if ($envPath && is_readable($envPath)) {
|
||||||
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
if ($line[0] === '#' || trim($line) === '') continue;
|
if ($line[0] === '#' || trim($line) === '') continue;
|
||||||
if (!str_contains($line, '=')) continue;
|
if (!str_contains($line, '=')) continue;
|
||||||
[$k, $v] = array_map('trim', explode('=', $line, 2));
|
[$k, $v] = array_map('trim', explode('=', $line, 2));
|
||||||
// Strip potential surrounding quotes
|
$v = trim($v, "' ");
|
||||||
$v = trim($v, "\"' ");
|
// Set env if not set
|
||||||
// Do not override existing env
|
|
||||||
if ($k !== '' && (getenv($k) === false || getenv($k) === '')) {
|
if ($k !== '' && (getenv($k) === false || getenv($k) === '')) {
|
||||||
putenv("{$k}={$v}");
|
putenv("{$k}={$v}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$loaded = true;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
load_dotenv_if_needed([
|
load_env();
|
||||||
'MAIL_TRANSPORT','SMTP_HOST','SMTP_PORT','SMTP_SECURE','SMTP_USER','SMTP_PASS',
|
|
||||||
'MAIL_FROM','MAIL_FROM_NAME','MAIL_REPLY_TO','MAIL_TO',
|
|
||||||
'DKIM_DOMAIN','DKIM_SELECTOR','DKIM_PRIVATE_KEY_PATH'
|
|
||||||
]);
|
|
||||||
|
|
||||||
$transport = env_val('MAIL_TRANSPORT', 'smtp');
|
$transport = env_val('MAIL_TRANSPORT', 'smtp');
|
||||||
$smtp_host = env_val('SMTP_HOST');
|
$smtp_host = env_val('SMTP_HOST');
|
||||||
$smtp_port = (int) env_val('SMTP_PORT', 587);
|
$smtp_port = (int) env_val('SMTP_PORT', 587);
|
||||||
$smtp_secure = env_val('SMTP_SECURE', 'tls'); // tls | ssl | null
|
$smtp_secure = env_val('SMTP_SECURE', 'tls');
|
||||||
$smtp_user = env_val('SMTP_USER');
|
$smtp_user = env_val('SMTP_USER');
|
||||||
$smtp_pass = env_val('SMTP_PASS');
|
$smtp_pass = env_val('SMTP_PASS');
|
||||||
|
|
||||||
@ -50,27 +41,14 @@ $from_email = env_val('MAIL_FROM', 'no-reply@localhost');
|
|||||||
$from_name = env_val('MAIL_FROM_NAME', 'App');
|
$from_name = env_val('MAIL_FROM_NAME', 'App');
|
||||||
$reply_to = env_val('MAIL_REPLY_TO');
|
$reply_to = env_val('MAIL_REPLY_TO');
|
||||||
|
|
||||||
$dkim_domain = env_val('DKIM_DOMAIN');
|
|
||||||
$dkim_selector = env_val('DKIM_SELECTOR');
|
|
||||||
$dkim_private_key_path = env_val('DKIM_PRIVATE_KEY_PATH');
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'transport' => $transport,
|
'transport' => $transport,
|
||||||
|
|
||||||
// SMTP
|
|
||||||
'smtp_host' => $smtp_host,
|
'smtp_host' => $smtp_host,
|
||||||
'smtp_port' => $smtp_port,
|
'smtp_port' => $smtp_port,
|
||||||
'smtp_secure' => $smtp_secure,
|
'smtp_secure' => $smtp_secure,
|
||||||
'smtp_user' => $smtp_user,
|
'smtp_user' => $smtp_user,
|
||||||
'smtp_pass' => $smtp_pass,
|
'smtp_pass' => $smtp_pass,
|
||||||
|
|
||||||
// From / Reply-To
|
|
||||||
'from_email' => $from_email,
|
'from_email' => $from_email,
|
||||||
'from_name' => $from_name,
|
'from_name' => $from_name,
|
||||||
'reply_to' => $reply_to,
|
'reply_to' => $reply_to,
|
||||||
|
|
||||||
// DKIM (optional)
|
|
||||||
'dkim_domain' => $dkim_domain,
|
|
||||||
'dkim_selector' => $dkim_selector,
|
|
||||||
'dkim_private_key_path' => $dkim_private_key_path,
|
|
||||||
];
|
];
|
||||||
Loading…
x
Reference in New Issue
Block a user