اگر تا حالا خواستهاید سایت وردپرسیتان خلاصه بسازد، عنوان پیشنهاد بدهد یا به سؤال خواننده جواب بدهد، به این جمله رسیدهاید: «باید یک افزونه نصب کنم». این جمله همیشه درست نیست. وردپرس سالهاست ابزارهای لازم برای حرفزدن با یک مدل زبانی را در هسته دارد و هر افزونهٔ اتصال، در واقع همان ابزارها را با یک پنل تنظیمات بستهبندی میکند.
این مقاله سه روش بدون افزونه را با کد کامل PHP نشان میدهد: فراخوانی مستقیم، ساخت endpoint داخلی برای مرورگر، و اتصال از راه هوک در ویرایشگر یا فرم تماس. در هر سه روش کلید API سمت سرور میماند و مرورگر آن را نمیبیند. کدها با PHP ۸.۱ سازگارند و فقط از تابعهای هستهٔ وردپرس استفاده میکنند؛ هیچ افزونهٔ فرم، اتصال یا مدیریت کلید لازم ندارند. در سراسر متن هیچ عددی دربارهٔ سرعت یا کیفیت پاسخ پیدا نمیکنید، چون این دو به مدل و سرویس انتخابی شما بستگی دارند، نه به کد. اگر فرم را خودت ساختهای، ساخت فرم تماس بدون افزونه نقطهٔ شروع همین اتصال است. اگر فرم را خودت ساختهای، ساخت فرم تماس بدون افزونه نقطهٔ شروع همین اتصال است.
چرا اتصال وردپرس به مدل زبانی به افزونه نیاز ندارد
سه ابزار در هستهٔ وردپرس وجود دارد و هر سه سالها پیش از این اضافه شدهاند:
- WP_Http با تابع
wp_remote_post— از نسخهٔ ۲.۷. یک درخواست HTTP از سمت سرور میفرستد و پاسخ را برمیگرداند. همان کاری که افزونههای اتصال در پشت صحنه انجام میدهند. - REST API — از نسخهٔ ۴.۷. با
register_rest_routeمیتوانید مسیر دلخواه خودتان را ثبت کنید و از مرورگر به آن حرف بزنید. - Application Passwords — از نسخهٔ ۵.۶. اگر لازم شد یک اسکریپت بیرونی به سایت شما وصل شود، بهجای رمز اصلی، یک رمز جداگانه و قابل ابطال میسازید.
پس «اتصال به مدل زبانی» در سادهترین شکلش سه خط است: یک آدرس، یک کلید، یک بدنهٔ JSON. چیزی که افزونه اضافه میکند پنل تنظیمات، پشتیبانی، و آماده بودن برای کسی است که نمیخواهد کد ببیند — نه قابلیتی که در هسته نباشد.
این را هم بگوییم که افزونه رایگان نیست، حتی وقتی پول نمیدهید. هزینهاش اینهاست:
- سطح بهروزرسانی بزرگتر. هر افزونه یک قطعهٔ کد اضافه است که باید با نسخهٔ وردپرس و PHP همگام بماند.
- وزن صفحه. بیشتر افزونههای اتصال، در همهٔ صفحات پیشخوان یا حتی سایت، CSS و جاوااسکریپت خودشان را بار میکنند.
- جای کلید. بخشی از آنها کلید را در جدول تنظیمات وردپرس (
wp_options) نگه میدارند؛ جایی که در هر بکاپ دیتابیس کپی میشود. - وابستگی. تصمیمهای محصول با تصمیمهای شما یکی نیست؛ مدلی که پشتیبانی میکند با مدلی که میخواهید فرق دارد.
در عوض، وقتی کد را خودتان مینویسید، مالک عواقبش هم هستید: خطا را خودتان میخوانید، تایماوت را خودتان تنظیم میکنید و کلید را خودتان جای درست میگذارید. برای یک قابلیت مشخص، این معاوضه معمولاً به سود شماست. در بخش «چه زمانی افزونه بهتر است» مرز دقیقش را میگوییم.
پیشنیازها: کلید API و یک endpoint سازگار با OpenAI
«سازگار با OpenAI» یک قرارداد است، نه نام یک شرکت. یعنی سرویس شما این سه چیز را میپذیرد:
- یک درخواست
POSTبه مسیری مثلhttps://api.example.com/v1/chat/completions. - هدر
Authorization: Bearer YOUR_KEYو بدنهٔ JSON با فیلدهایmodelوmessages. - پاسخ JSON که متن تولیدشده در
choices[0].message.contentمینشیند.
هر سرویسی که این سه شرط را رعایت کند، با کد این مقاله کار میکند. نام مدل را از مستندات همان سرویس بردارید و در کد جای your-model-name بگذارید؛ در این مقاله عمداً هیچ مدل و هیچ سرویس مشخصی تبلیغ نمیشود.
سه چیز دیگر لازم دارید که معمولاً از قلم میافتد:
- آدرس پایه و مسیر کامل. بعضی سرویسها
/v1را در آدرس پایه میگیرند و بعضی در مسیر. آدرس نهایی باید همان چیزی باشد که در مستندات «chat completions» نام دارد. - نام مدل دقیق. یک حرف اضافه یا کم، پاسخ ۴۰۴ یا ۴۰۰ میدهد.
- سقف مصرف. اگر سرویس امکان تعیین سقف هزینهٔ ماهانه دارد، همان روز اول روشنش کنید. کد شما میتواند محدودیت نرخ بگذارد، ولی نمیتواند جلوی یک حلقهٔ منطقی در خودش را بگیرد.
کلید را در فایل wp-config.php و بالاتر از خط /* That's all, stop editing! */ تعریف کنید:
// در wp-config.php، بالاتر از خط «That's all, stop editing!» بگذارید.
define( 'SPLLLM_API_KEY', 'کلید-واقعی-خود-را-اینجا-بگذارید' );فایل wp-config.php بیرون از دیتابیس است، در بکاپ دیتابیس نمیآید و از پیشخوان وردپرس خوانده نمیشود. سطح دسترسی فایل را هم روی ۶۰۰ بگذارید تا فقط کاربر هاست بتواند بخواندش. کلید را در تنظیمات وردپرس، در فایل قالب، یا در جاوااسکریپت نگذارید؛ هر سه راه، کلید را جایی میبرد که نباید.
ساختار درخواست و پاسخ: چه چیزی واقعاً رد و بدل میشود
پیش از کد، بدنهٔ خام را ببینید. این همان چیزی است که توابع PHP برای شما میسازند:

{
"model": "your-model-name",
"messages": [
{ "role": "system", "content": "پاسخ را کوتاه، دقیق و فارسی بنویس." },
{ "role": "user", "content": "سه عنوان برای یک مقالهٔ وردپرس پیشنهاد بده." }
],
"max_tokens": 600,
"temperature": 0.3
}و پاسخ موفق، این شکل را دارد:
{
"id": "chatcmpl-example",
"object": "chat.completion",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "۱) ... ۲) ... ۳) ..." },
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 48,
"completion_tokens": 120,
"total_tokens": 168
}
}پنج فیلد بدنه را بشناسید:
| فیلد | کارش چیست | اشتباه رایج |
|---|---|---|
model | کدام مدل جواب بدهد | نام کوتاه یا مستعار نوشتن بهجای شناسهٔ کامل |
messages | تاریخچهٔ گفتگو؛ هر عضو یک role و یک content | فرستادن رشتهٔ خالی یا جابهجا کردن نقشها |
max_tokens | سقف طول پاسخ؛ همان چیزی که هزینه را کنترل میکند | نداشتنش، یا گذاشتن عدد بزرگ برای یک خلاصهٔ کوتاه |
temperature | میزان تنوع خروجی؛ نزدیک صفر یعنی پاسخ تکرارپذیرتر | گذاشتن یک برای کاری که خروجی ثابت میخواهد |
stream | پاسخ تکهتکه؛ برای چت زنده | روشن کردنش در مسیری که منتظر JSON کامل است |
در پاسخ، فقط سه چیز اهمیت دارد: choices[0].message.content که متن است، finish_reason که میگوید پاسخ کامل شد یا به سقف توکن خورد، و usage که مصرف را برای حسابرسی میدهد. اگر خطایی رخ داده باشد، معمولاً فیلد error.message پر است و کد وضعیت HTTP هم ۴xx یا ۵xx است. پس همیشه اول کد وضعیت را بخوانید، بعد بدنه را.
روش اول: فراخوانی مستقیم با wp_remote_post
این روش برای جایی است که خودِ وردپرس تصمیم میگیرد مدل را صدا بزند: ذخیرهٔ نوشته، اجرای یک زمانبند، یا ساخت یک گزارش. مرورگر در این مسیر هیچ نقشی ندارد.
چرا wp_remote_post و نه curl خام؟ چون وردپرس درخواست را از لایهٔ خودش میفرستد: پروکسی سرور را رعایت میکند، فیلترهای http_request_args را اجرا میکند، و گواهیهای CA خودش را میشناسد. curl خام روی خیلی از هاستهای اشتراکی بسته است یا تنظیمات پروکسی را نمیبیند.
کد زیر کامل است. آن را در فایل wp-content/mu-plugins/splllm-client.php بگذارید؛ پوشهٔ mu-plugins نیازی به فعالسازی ندارد و با آپدیت قالب یا افزونهها پاک نمیشود.
<?php
/**
* Plugin Name: اتصال به مدل زبانی — روش اول
* Description: فراخوانی مستقیم یک endpoint سازگار با OpenAI از داخل وردپرس، بدون افزونه.
*/
defined( 'ABSPATH' ) || exit;
/**
* یک درخواست به مدل میفرستد و متن پاسخ را برمیگرداند.
*
* @param string $prompt پیام کاربر؛ باید پیش از رسیدن به این تابع پاکسازی شده باشد.
* @return string|WP_Error متن پاسخ، یا WP_Error با کدی که میتوان تشخیص داد.
*/
function splllm_complete( $prompt ) {
$api_key = defined( 'SPLLLM_API_KEY' ) ? SPLLLM_API_KEY : '';
if ( '' === $api_key ) {
return new WP_Error( 'splllm_no_key', 'کلید API تعریف نشده است.' );
}
$endpoint = 'https://api.example.com/v1/chat/completions';
$payload = array(
'model' => 'your-model-name',
'messages' => array(
array(
'role' => 'system',
'content' => 'پاسخ را کوتاه، دقیق و فارسی بنویس.',
),
array(
'role' => 'user',
'content' => $prompt,
),
),
'max_tokens' => 600,
'temperature' => 0.3,
);
$response = wp_remote_post(
$endpoint,
array(
'timeout' => 25,
'redirection' => 0,
'httpversion' => '1.1',
'headers' => array(
'Authorization' => 'Bearer ' . $api_key,
'Content-Type' => 'application/json',
'Accept' => 'application/json',
),
'body' => wp_json_encode( $payload ),
'data_format' => 'body',
)
);
if ( is_wp_error( $response ) ) {
return $response;
}
$code = (int) wp_remote_retrieve_response_code( $response );
$body = (string) wp_remote_retrieve_body( $response );
if ( $code < 200 || $code >= 300 ) {
return new WP_Error(
'splllm_http_' . $code,
'سرویس با کد ' . $code . ' پاسخ داد.',
array( 'status' => $code )
);
}
if ( '' === trim( $body ) ) {
return new WP_Error( 'splllm_empty_body', 'پاسخ خالی برگشت.' );
}
$data = json_decode( $body, true );
if ( JSON_ERROR_NONE !== json_last_error() || ! is_array( $data ) ) {
return new WP_Error( 'splllm_bad_json', 'پاسخ JSON معتبر نبود.' );
}
if ( empty( $data['choices'][0]['message']['content'] ) ) {
return new WP_Error( 'splllm_shape', 'ساختار پاسخ مطابق قالب مورد انتظار نبود.' );
}
return trim( (string) $data['choices'][0]['message']['content'] );
}بخشهای مهم این کد:
- کلید از ثابت خوانده میشود، نه از دیتابیس. اگر ثابت تعریف نشده باشد، تابع پیش از هر تماس شبکهای با
WP_Errorبرمیگردد؛ بهتر از فرستادن درخواست بدون کلید و خواندن یک ۴۰۱ گنگ است. timeoutروی ۲۵ ثانیه است. پیشفرض وردپرس ۵ ثانیه است و برای مدل زبانی کافی نیست؛ همان پنج ثانیه، شایعترین دلیل خطایhttp_request_timeoutدر این نوع اتصال است. عدد ۲۵ انتخاب ماست، نه استاندارد؛ اگر سرویس شما کندتر جواب میدهد بالاترش ببرید، ولی سقفmax_execution_timeهاست را هم در نظر بگیرید.httpversionروی1.1است. پیشفرض وردپرس1.0است و بعضی سرویسها با نسخهٔ ۱.۰ بدرفتاری میکنند.redirectionصفر است. در فراخوانی API، دنبال کردن ریدایرکت یعنی فرستادن هدر Authorization به آدرسی که انتظارش را ندارید.data_formatرویbodyاست. یعنی بدنه از قبل JSON شده و وردپرس نباید آن را دوباره به شکل فرم کدگذاری کند. بدون این مقدار، ممکن است بدنهٔ شما بهشکل آرایه ارسال شود و سرویس ۴۰۰ بدهد.- سه سنجش پشت سر هم:
is_wp_errorبرای خطای انتقال، کد وضعیت برای خطای سرویس، و بدنهٔ خالی برای وقتی که سرویس ۲۰۰ میدهد ولی چیزی نمیفرستد — همان حالتی که اگر چک نکنید، یک صفحهٔ سفید یا یک خطای «آرایهٔ ناموجود» در لاگ میسازد. - بررسی شکل پاسخ. حتی JSON سالم میتواند ساختار دیگری داشته باشد. اگر
choices[0].message.contentنبود، بهتر استWP_Errorبرگردانید تا اینکه متن خالی را بهعنوان جواب ذخیره کنید.
نمونهٔ استفاده، با خروجی امن:
<?php
// نمونهٔ استفاده در قالب یا شورتکد:
$answer = splllm_complete( 'سه عنوان پیشنهادی برای این مقاله بده.' );
if ( is_wp_error( $answer ) ) {
// پیام عمومی برای کاربر؛ جزئیات فقط در لاگ میماند.
echo '<p>فعلاً پاسخگویی ممکن نیست.</p>';
} else {
echo '<div class="splllm-answer">' . nl2br( esc_html( $answer ) ) . '</div>';
}تابع esc_html اینجا تزئینی نیست. خروجی مدل متن تولیدشده است، نه HTML معتبر؛ اگر آن را خام چاپ کنید، یک پاسخ حاوی تگ <script> روی سایت شما اجرا میشود. اگر به شکستن خط نیاز دارید، nl2br( esc_html( $answer ) ) بدهید، نه nl2br( $answer ).
یک نکتهٔ عملی: این تابع را در مسیر ذخیرهٔ نوشته صدا نزنید. هر بار ذخیره، ۲۵ ثانیه انتظار یعنی ویرایشگر قفل. روش سوم نشان میدهد چطور همین کار را به اجرای بعدی موکول کنید.
روش دوم: ساخت endpoint داخلی با register_rest_route
وقتی مرورگر باید سؤال بپرسد، دو انتخاب دارید: کلید را به جاوااسکریپت بدهید، یا یک endpoint در سایت خودتان بسازید که مرورگر با آن حرف بزند و کلید سمت سرور بماند. انتخاب درست همیشه دومی است.
کد زیر یک مسیر POST /wp-json/splllm/v1/ask میسازد، با اعتبارسنجی طول، محدودیت نرخ ساعتی، و یک شورتکد که فرم و جاوااسکریپتش را چاپ میکند. تابع splllm_complete از روش اول باید در همان نصب موجود باشد؛ لاگ خطا اختیاری است و اگر فایل مدیریت خطا نصب نشده باشد، بیصدا رد میشود.
<?php
/**
* Plugin Name: اتصال به مدل زبانی — روش دوم
* Description: endpoint داخلی وردپرس که مرورگر به آن حرف میزند و کلید API پشت سرور میماند.
*/
defined( 'ABSPATH' ) || exit;
add_action( 'rest_api_init', 'splllm_register_routes' );
/**
* مسیر POST /wp-json/splllm/v1/ask را ثبت میکند.
*/
function splllm_register_routes() {
register_rest_route(
'splllm/v1',
'/ask',
array(
'methods' => WP_REST_Server::CREATABLE, // فقط POST.
'callback' => 'splllm_handle_ask',
'permission_callback' => 'splllm_can_ask',
'args' => array(
'prompt' => array(
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'splllm_validate_prompt',
),
),
)
);
}
/**
* طول متن را میسنجد؛ بدون این سنجش، هر کاربر میتواند هزینهٔ شما را باز کند.
*
* @param mixed $value مقدار خام پارامتر.
* @return true|WP_Error
*/
function splllm_validate_prompt( $value ) {
$value = (string) $value;
$length = function_exists( 'mb_strlen' ) ? mb_strlen( $value ) : strlen( $value );
if ( $length < 3 ) {
return new WP_Error( 'splllm_too_short', 'متن کوتاهتر از حد مجاز است.', array( 'status' => 400 ) );
}
if ( $length > 2000 ) {
return new WP_Error( 'splllm_too_long', 'متن بلندتر از حد مجاز است.', array( 'status' => 400 ) );
}
return true;
}
/**
* فقط کاربر واردشده اجازه دارد. کوکی وردپرس بهتنهایی کافی نیست؛
* هسته هدر X-WP-Nonce را هم بررسی میکند و بدون آن پاسخ ۴۰۳ میدهد.
*
* @return true|WP_Error
*/
function splllm_can_ask() {
if ( ! is_user_logged_in() ) {
return new WP_Error( 'splllm_login_required', 'برای استفاده وارد شوید.', array( 'status' => 401 ) );
}
return true;
}
/**
* سهمیهٔ ساعتی هر کاربر. transient برای سایتهای بدون کش شیء کافی است،
* ولی در بار همزمان بالا احتمال یک درخواست اضافه وجود دارد.
*
* @param int $limit تعداد مجاز در هر پنجره.
* @param int $window طول پنجره به ثانیه.
* @return bool
*/
function splllm_rate_limit_ok( $limit = 10, $window = HOUR_IN_SECONDS ) {
$user_id = get_current_user_id();
if ( 0 === $user_id ) {
return false;
}
$key = 'splllm_rl_' . $user_id;
$used = (int) get_transient( $key );
if ( $used >= $limit ) {
return false;
}
set_transient( $key, $used + 1, $window );
return true;
}
/**
* بدنهٔ endpoint: اعتبارسنجی، سهمیه، فراخوانی مدل، پاسخ تمیز.
*
* @param WP_REST_Request $request درخواست ورودی.
* @return WP_REST_Response|WP_Error
*/
function splllm_handle_ask( WP_REST_Request $request ) {
if ( ! splllm_rate_limit_ok() ) {
return new WP_Error( 'splllm_rate_limited', 'سهمیهٔ ساعتی تمام شد.', array( 'status' => 429 ) );
}
$prompt = (string) $request->get_param( 'prompt' );
$answer = splllm_complete( $prompt ); // تابع روش اول.
if ( is_wp_error( $answer ) ) {
// تابع فایل «مدیریت خطا»؛ اگر آن فایل نصب نشده باشد، بیصدا رد میشود.
if ( function_exists( 'splllm_log_error' ) ) {
splllm_log_error( $answer );
}
return new WP_Error(
'splllm_upstream',
'سرویس پاسخ نداد. کمی بعد دوباره تلاش کنید.',
array( 'status' => 502 )
);
}
return rest_ensure_response( array( 'answer' => $answer ) );
}
add_shortcode( 'splllm_ask', 'splllm_render_form' );
/**
* فرم + جاوااسکریپت. کلید API اینجا نیست؛ فقط nonce هست.
*
* @return string
*/
function splllm_render_form() {
if ( ! is_user_logged_in() ) {
return '<p>برای پرسیدن سؤال وارد حساب خود شوید.</p>';
}
$endpoint = rest_url( 'splllm/v1/ask' );
$nonce = wp_create_nonce( 'wp_rest' );
ob_start();
?>
<form id="splllm-form" class="splllm-form">
<label for="splllm-prompt">سؤال شما</label>
<textarea id="splllm-prompt" name="prompt" rows="4" maxlength="2000" required></textarea>
<button type="submit">بپرس</button>
<div id="splllm-out" role="status" aria-live="polite"></div>
</form>
<script>
( function () {
var form = document.getElementById( 'splllm-form' );
var out = document.getElementById( 'splllm-out' );
if ( ! form || ! out ) { return; }
form.addEventListener( 'submit', function ( event ) {
event.preventDefault();
out.textContent = 'در حال ارسال…';
fetch( <?php echo wp_json_encode( $endpoint ); ?>, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': <?php echo wp_json_encode( $nonce ); ?>
},
body: JSON.stringify( { prompt: document.getElementById( 'splllm-prompt' ).value } )
} ).then( function ( response ) {
return response.json().then( function ( data ) {
out.textContent = response.ok ? data.answer : ( data.message || 'خطا در پردازش درخواست.' );
} );
} ).catch( function () {
out.textContent = 'ارتباط با سایت برقرار نشد.';
} );
} );
}() );
</script>
<?php
return ob_get_clean();
}چند تصمیم در این کد عمدی است: لایهٔ دوم امنیت را با هدرهای امنیتی وردپرس کامل کن. لایهٔ دوم امنیت را با هدرهای امنیتی وردپرس کامل کن.
permission_callbackخالی نیست. از وردپرس ۵.۵ اگر این کلید نباشد، هسته هشدار_doing_it_wrongمیدهد و مسیر بدون بررسی دسترسی ثبت میشود. یعنی هر کسی میتواند از اعتبار شما خرج کند.- کوکی وردپرس بهتنهایی کافی نیست. احراز هویت با کوکی در REST API فقط وقتی پذیرفته میشود که هدر
X-WP-Nonceمعتبر همراه درخواست باشد؛ همین غیرفعال کردن حملهٔ CSRF است. شورتکد این nonce را باwp_create_nonce( 'wp_rest' )میسازد و جاوااسکریپت آن را در هدر میگذارد. - محدودیت نرخ با transient. سهمیهٔ هر کاربر در یک ساعت شمرده میشود. این روش ساده و بدون افزونه است، ولی در ترافیک همزمان بالا دقیق نیست: دو درخواست همزمان میتوانند سهمیه را یک واحد رد کنند. برای محدودیت دقیق باید شمارندهٔ اتمیک (کش شیء یا جدول اختصاصی) داشت.
- خطای داخلی به مرورگر نمیرود. کد خطای واقعی لاگ میشود و کاربر یک پیام عمومی با وضعیت ۵۰۲ میگیرد. اگر متن خطای سرویس را به مرورگر برگردانید، ممکن است نام مدل، آدرس داخلی یا بخشی از پیکربندی شما را لو بدهد.
- اعتبارسنجی و پاکسازی، دو کار جدا هستند.
validate_callbackدربارهٔ پذیرفتن یا رد کردن تصمیم میگیرد،sanitize_callbackمقدار را تمیز میکند. هر دو لازماند.
برای آزمودن مسیر از بیرون، با یک Application Password و curl:
curl -X POST "https://example.com/wp-json/splllm/v1/ask"
-u "USERNAME:APPLICATION_PASSWORD"
-H "Content-Type: application/json"
-d '{"prompt":"یک عنوان برای مقاله پیشنهاد بده"}'این مسیر برای کاربر واردشده ساخته شده است. اگر میخواهید مهمان هم بتواند بپرسد، permission_callback را به یک بررسی مبتنی بر nonce و هانیپات تغییر بدهید و سهمیه را بر پایهٔ نشانی شبکه محدود کنید؛ همان کاری که در روش سوم برای فرم تماس انجام شده است. بدون این دو، یک endpoint عمومی بهسرعت به یک قبض تبدیل میشود.
روش سوم: استفاده از هوک در فرم تماس یا ویرایشگر
دو جای طبیعی برای صدا زدن مدل وجود دارد که هیچ افزونهای لازم ندارند: چرخهٔ ذخیرهٔ نوشته در ویرایشگر، و ارسال فرم تماس.
در ویرایشگر، قاعدهٔ طلایی این است: درخواست شبکهای را داخل save_post اجرا نکنید. save_post در همان درخواستی اجرا میشود که دکمهٔ «بهروزرسانی» را زدهاید؛ اگر آنجا ۲۵ ثانیه منتظر مدل بمانید، ویرایشگر همانقدر معطل میشود و کاربر دوباره کلیک میکند. راه درست، زمانبندی یک رویداد و انجام کار در اجرای بعدی است.

<?php
/**
* Plugin Name: اتصال به مدل زبانی — روش سوم
* Description: هوک در چرخهٔ ذخیرهٔ نوشته و در فرم تماس، بدون هیچ افزونهٔ فرم.
*/
defined( 'ABSPATH' ) || exit;
/* ---------- الف) ویرایشگر: خلاصهٔ پیشنهادی بعد از ذخیره ---------- */
add_action( 'save_post', 'splllm_queue_summary', 20, 3 );
/**
* فقط زمانبندی میکند؛ هیچ درخواست شبکهای در مسیر ذخیرهٔ نوشته اجرا نمیشود.
*
* @param int $post_id شناسهٔ نوشته.
* @param WP_Post $post شیء نوشته.
* @param bool $update بهروزرسانی یا ساخت تازه.
*/
function splllm_queue_summary( $post_id, $post, $update ) {
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
return;
}
if ( 'post' !== $post->post_type ) {
return;
}
if ( ! in_array( $post->post_status, array( 'publish', 'draft' ), true ) ) {
return;
}
if ( '' !== (string) get_post_meta( $post_id, '_splllm_summary', true ) ) {
return;
}
if ( wp_next_scheduled( 'splllm_make_summary', array( $post_id ) ) ) {
return;
}
wp_schedule_single_event( time() + 60, 'splllm_make_summary', array( $post_id ) );
}
add_action( 'splllm_make_summary', 'splllm_make_summary' );
/**
* کار سنگین در اجرای بعدی WP-Cron انجام میشود.
*
* @param int $post_id شناسهٔ نوشته.
*/
function splllm_make_summary( $post_id ) {
$post = get_post( $post_id );
if ( ! $post ) {
return;
}
$text = splllm_cut( wp_strip_all_tags( $post->post_content ), 3000 );
if ( '' === trim( $text ) ) {
return;
}
$answer = splllm_complete( 'این متن را در حداکثر ۱۵۰ نویسه خلاصه کن و فقط خلاصه را بنویس: ' . $text );
if ( is_wp_error( $answer ) ) {
splllm_log_error( $answer );
return;
}
update_post_meta( $post_id, '_splllm_summary', sanitize_text_field( $answer ) );
}
/**
* برش امن متن؛ اگر mbstring نبود، بر پایهٔ بایت بریده میشود.
*
* @param string $text متن.
* @param int $limit طول مجاز.
* @return string
*/
function splllm_cut( $text, $limit ) {
if ( function_exists( 'mb_substr' ) ) {
return mb_substr( (string) $text, 0, $limit );
}
return substr( (string) $text, 0, $limit );
}
/* ---------- ب) فرم تماس بدون افزونه ---------- */
add_action( 'admin_post_nopriv_splllm_contact', 'splllm_contact_relay' );
add_action( 'admin_post_splllm_contact', 'splllm_contact_relay' );
/**
* پیام فرم را میگیرد، دستهبندی پیشنهادی مدل را به آن میچسباند و ایمیل میکند.
*/
function splllm_contact_relay() {
$back = wp_get_referer();
$back = $back ? $back : home_url( '/' );
if ( ! isset( $_POST['splllm_nonce'] ) ) {
wp_safe_redirect( add_query_arg( 'splllm', 'nonce', $back ) );
exit;
}
$nonce = sanitize_text_field( wp_unslash( $_POST['splllm_nonce'] ) );
if ( ! wp_verify_nonce( $nonce, 'splllm_contact' ) ) {
wp_safe_redirect( add_query_arg( 'splllm', 'nonce', $back ) );
exit;
}
if ( ! empty( $_POST['splllm_hp'] ) ) { // هانیپات: ربات آن را پر میکند.
wp_safe_redirect( add_query_arg( 'splllm', 'ok', $back ) );
exit;
}
$name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
$message = isset( $_POST['message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['message'] ) ) : '';
if ( '' === trim( $message ) || ! is_email( $email ) ) {
wp_safe_redirect( add_query_arg( 'splllm', 'invalid', $back ) );
exit;
}
$bucket = splllm_complete(
'این پیام مشتری را فقط با یکی از این سه واژه دستهبندی کن: پشتیبانی، فروش، همکاری. پیام: '
. splllm_cut( $message, 1200 )
);
$label = is_wp_error( $bucket ) ? 'نامشخص' : sanitize_text_field( splllm_cut( $bucket, 40 ) );
$body = "نام: {$name}nایمیل: {$email}nدسته: {$label}nn{$message}";
wp_mail(
get_option( 'admin_email' ),
'پیام تازهٔ فرم تماس — ' . $label,
$body,
array( 'Reply-To: ' . $email )
);
wp_safe_redirect( add_query_arg( 'splllm', 'ok', $back ) );
exit;
}
add_shortcode( 'splllm_contact_form', 'splllm_render_contact_form' );
/**
* فرم خام؛ بدون افزونهٔ فرم، بدون جاوااسکریپت، با nonce و هانیپات.
*
* @return string
*/
function splllm_render_contact_form() {
$nonce = wp_create_nonce( 'splllm_contact' );
ob_start();
?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="splllm-contact">
<input type="hidden" name="action" value="splllm_contact">
<input type="hidden" name="splllm_nonce" value="<?php echo esc_attr( $nonce ); ?>">
<p><label>نام<br><input type="text" name="name" required></label></p>
<p><label>ایمیل<br><input type="email" name="email" required></label></p>
<p><label>پیام<br><textarea name="message" rows="5" required></textarea></label></p>
<p style="position:absolute;left:-9999px" aria-hidden="true">
<label>این فیلد را خالی بگذارید <input type="text" name="splllm_hp" value=""></label>
</p>
<p><button type="submit">ارسال</button></p>
</form>
<?php
return ob_get_clean();
}چند نکته که بدون آنها این کد در عمل میشکند:
wp_schedule_single_eventیک زمانبند واقعی نیست. WP-Cron در جریان بازدید بعدی سایت اجرا میشود؛ یعنی اگر سایت بازدیدکننده نداشته باشد، کار هم اجرا نمیشود. برای سایت پربازدید کافی است، برای سایت کمبازدید باید زمانبند واقعی هاست را رویwp-cron.phpتنظیم کنید.- شرط «قبلاً ساخته شده» جلوی حلقه را میگیرد. هم بررسی متای موجود و هم
wp_next_scheduledلازم است؛ بدون آنها هر ذخیره یک رویداد تازه میسازد. - در فرم تماس، nonce و هانیپات با هم. nonce جلوی ارسال از سایت دیگر را میگیرد و هانیپات جلوی رباتهای ساده را. هیچکدام بهتنهایی کافی نیست.
- هر فیلد جداگانه پاکسازی میشود. نام و ایمیل و متن، سه تابع متفاوت میخواهند؛
sanitize_text_fieldبرای متن چندخطی مناسب نیست. - خروجی مدل هرگز اجرا نمیشود. در این کد، پاسخ مدل فقط یک برچسب در موضوع ایمیل است و با
sanitize_text_fieldبریده میشود. اگر جایی خروجی را بهعنوان دستور، کوئری یا تصمیم اجرایی بهکار ببرید، یک پرامپت تزریقی میتواند آن را هدایت کند. admin_post_noprivفقط وقتی لازم است که مهمان هم بتواند فرم بفرستد. اگر فرم مخصوص کاربران است، همان نسخهٔ بدونnoprivکافی است و سطح حمله کوچکتر میشود.
مدیریت خطا: تایماوت، محدودیت نرخ و پاسخ خالی
در اتصال به مدل، خطا قاعده است نه استثنا. چهار دستهٔ خطا را جدا کنید، چون رفتار درست برای هرکدام فرق دارد:

| نشانه | معنی | کار درست |
|---|---|---|
http_request_timeout | پاسخ در مهلت نرسید | تایماوت را بالا ببرید؛ یک تلاش دوباره، نه بیشتر |
http_request_failed | DNS، گواهی یا اتصال شبکه | sslverify را خاموش نکنید؛ علت را پیدا کنید |
| کد ۴۰۱ یا ۴۰۳ | کلید یا دسترسی | تکرار نکنید؛ کلید را بررسی و در صورت لزوم بچرخانید |
| کد ۴۲۹ | محدودیت نرخ سرویس | هدر Retry-After را بخوانید و همانقدر صبر کنید |
| کد ۵xx | خرابی سمت سرویس | یک تلاش دوباره با فاصله، بعد پیام عمومی |
| بدنهٔ خالی با کد ۲۰۰ | پاسخ ناقص یا فیلترشده | WP_Error برگردانید؛ خروجی خالی را ذخیره نکنید |
کد زیر سه چیز اضافه میکند: یک تلاش دوبارهٔ کنترلشده، خواندن هدر Retry-After، و لاگ بدون داده. این فایل را کنار فایل روش اول بگذارید.
<?php
/**
* Plugin Name: اتصال به مدل زبانی — مدیریت خطا
* Description: تلاش دوبارهٔ کنترلشده، خواندن Retry-After و لاگ بدون دادهٔ کاربر.
*/
defined( 'ABSPATH' ) || exit;
/**
* فقط کد خطا را مینویسد؛ نه متن پرامپت، نه پاسخ سرویس، نه کلید.
*
* @param WP_Error $error خطای بازگشتی.
*/
function splllm_log_error( $error ) {
if ( ! is_wp_error( $error ) ) {
return;
}
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( '[splllm] ' . $error->get_error_code() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
}
}
/**
* یک تلاش دوباره، فقط برای خطاهای گذرا (شبکه و ۵xx).
*
* @param string $prompt پیام.
* @return string|WP_Error
*/
function splllm_complete_with_retry( $prompt ) {
$attempts = 0;
$wait = 0;
$last = null;
while ( $attempts < 2 ) {
if ( $wait > 0 ) {
sleep( $wait );
}
$result = splllm_complete( $prompt );
if ( ! is_wp_error( $result ) ) {
return $result;
}
$last = $result;
$code = (string) $result->get_error_code();
$attempts++;
if ( 0 === strpos( $code, 'splllm_http_4' ) && 'splllm_http_429' !== $code ) {
break; // خطای سمت درخواست؛ تکرار همان جواب را میدهد.
}
$wait = 2;
if ( 'splllm_http_429' === $code && ! empty( $GLOBALS['splllm_last_retry_after'] ) ) {
$wait = min( 30, (int) $GLOBALS['splllm_last_retry_after'] ); // سقف ۳۰ ثانیه.
}
}
return $last;
}
/**
* مقدار انتظار پیشنهادی سرویس را از هدر Retry-After میخواند.
*
* @param array $response پاسخ wp_remote_post.
* @return int ثانیه؛ صفر یعنی هدر معتبری نبود.
*/
function splllm_retry_after( $response ) {
$raw = (string) wp_remote_retrieve_header( $response, 'retry-after' );
if ( '' === $raw ) {
return 0;
}
if ( ctype_digit( $raw ) ) {
return (int) $raw; // ثانیه.
}
$timestamp = strtotime( $raw ); // قالب تاریخ HTTP.
return $timestamp ? max( 0, $timestamp - time() ) : 0;
}
/**
* هدر Retry-After را از پاسخ خام برمیدارد و برای تلاش دوباره نگه میدارد.
* فیلتر http_response روی همهٔ درخواستهای همین نصب اجرا میشود، پس آدرس را میسنجیم.
*
* @param array $response پاسخ خام.
* @param array $args آرگومانهای درخواست.
* @param string $url آدرس مقصد.
* @return array
*/
function splllm_capture_retry_after( $response, $args, $url ) {
if ( 0 !== strpos( (string) $url, 'https://api.example.com/' ) ) {
return $response;
}
$GLOBALS['splllm_last_retry_after'] = splllm_retry_after( $response );
return $response;
}
add_filter( 'http_response', 'splllm_capture_retry_after', 10, 3 );سه قاعده که در این کد رعایت شده و در کدهای دستساز معمولاً نقض میشود:
- تلاش دوباره فقط برای خطاهای گذرا. تکرار یک ۴۰۱ یا ۴۰۰ فقط همان خطا را دوباره میگیرد و ممکن است سرویس شما را موقتاً ببندد. کد ۴۲۹ استثناست: تکرار با فاصلهٔ درست، درست است.
- هدر
Retry-Afterاز پاسخ خام خوانده میشود. تابعsplllm_completeفقط متن را برمیگرداند، پس هدر باید جای دیگری برداشته شود: فیلترhttp_responseپاسخ خام را میگیرد،splllm_retry_afterمقدار را به ثانیه تبدیل میکند و برای تلاش دوباره کنار میگذارد. فیلتر آدرس مقصد را هم میسنجد تا پاسخ سرویسهای دیگر را به اشتباه نخواند. انتظار با سقف ۳۰ ثانیه بریده میشود؛ انتظار طولانیتر را به زمانبند بسپارید، نه به درخواست کاربر. sleepفقط در مسیر غیرهمگام. در مسیر ذخیرهٔ نوشته یا در پاسخ به کاربر، خوابیدن یعنی کند کردن سایت. اگر مجبورید، آن را به همان رویداد زمانبندیشده منتقل کنید.- لاگ بدون دادهٔ کاربر. فقط کد خطا نوشته میشود، نه متن پرامپت و نه پاسخ سرویس. لاگ هاست اشتراکی گاهی برای پشتیبانی قابل خواندن است و متن کاربر میتواند اطلاعات شخصی داشته باشد.
اگر خطا از نوع HTTP بود و ریشهاش در تنظیمات سایت نبود، بخش خطاهای وردپرس همین سایت را ببینید: مرکز خطاهای وردپرس سئوپلاس خطاهای رایج را با علت و راهحل فهرست کرده است.
پنهان کردن کلید API از مرورگر
کلید API یک رمز است. هر جا مرورگر بتواند بخواندش، کاربر هم میتواند — و هر کسی که آن صفحه را باز کند، با کلید شما خرج میکند. سه لایهٔ دفاع لازم است:

- لایهٔ اول: کلید در جاوااسکریپت نیست. نه در فایل، نه در تگ
script، نه درwp_localize_script. هر چیزی که به مرورگر میرود، عمومی است. - لایهٔ دوم: مرورگر با endpoint خودتان حرف میزند. همان روش دوم: مرورگر سؤال را به سایت شما میفرستد، سایت شما با کلید به سرویس مدل میزند و فقط متن جواب را برمیگرداند.
- لایهٔ سوم: کلید در دیتابیس نیست.
wp-config.phpیا متغیر محیطی. جدول تنظیمات در بکاپها، در ابزارهای مهاجرت و در بعضی افزونههای پشتیبانگیری کپی میشود.
چند جزئیات که در عمل تفاوت میسازند:
- هدر Authorization را لاگ نکنید. بعضی ابزارهای دیباگ، کل درخواست را با هدرها ذخیره میکنند. همان فایل، کلید شما را برای هر کسی که به آن دسترسی دارد میگذارد.
- کلید را بچرخانید. یک تاریخ در تقویم بگذارید و کلید را عوض کنید. کلید ثابت دو ساله، ریسک بیدلیل است.
- سطح دسترسی کلید را کم کنید. اگر سرویس چند نوع کلید دارد، کلیدی بگیرید که فقط همان مدل را صدا بزند، نه کلید مدیریت حساب.
- پاسخ خطا را خام چاپ نکنید. متن خطای سرویس گاهی نام مدل، شناسهٔ پروژه یا بخشی از کلید را در خود دارد.
اگر چند سایت یا چند ابزار دارید و نمیخواهید کلید در هر کدام تکرار شود، یک واسط محلی بسازید که همه از یک آدرس حرف بزنند و کلیدها یکجا بمانند. راهنمای نصب 9Router در ویندوز همین لایه را قدمبهقدم توضیح میدهد؛ بعد از آن، آدرس endpoint در کد بالا فقط به همان واسط محلی اشاره میکند و کلید سرویس اصلی جای دیگری میماند.
یک نکتهٔ معکوس هم هست: اگر میخواهید یک اسکریپت بیرونی به وردپرس شما وصل شود، از Application Passwords استفاده کنید، نه رمز اصلی. آن رمز جداگانه است، در دیتابیس هش میشود، فقط یک بار نمایش داده میشود و بینیاز از عوض کردن رمز اصلی، قابل ابطال است. این قابلیت هم فقط روی HTTPS کار میکند.
محدودیت هاست اشتراکی در اجرای مدل محلی
اجرای مدل زبانی روی هاست اشتراکی، در بیشتر موارد ممکن نیست، و دلیلش سختافزار نیست؛ معماری است. PHP در هاست اشتراکی یک فرایند کوتاهعمر است: درخواست میآید، اجرا میشود، تمام. هیچ فرایندی باقی نمیماند که مدل را در حافظه نگه دارد و هیچ پورتی هم برای گوش دادن در اختیار شما نیست.

چهار سد عملی:
- توابع اجرای فرایند بستهاند.
proc_openوshell_execدر بیشتر هاستهای اشتراکی درdisable_functionsهستند. بدون آنها نمیتوانید یک برنامهٔ مدل را از PHP بالا بیاورید. - حافظه کافی نیست. سقف
memory_limitمعمولاً بین ۱۲۸ تا ۵۱۲ مگابایت است، در حالی که حتی یک مدل کوچک چند گیگابایت میخواهد. بهعنوان قاعدهٔ سرانگشتی، مدل چهاربیتی برای هر میلیارد پارامتر حدود ۰.۶ تا ۰.۷ گیگابایت حافظه میخواهد، بهعلاوهٔ سرریز پنجرهٔ متن؛ یعنی یک مدل ۷ میلیارد پارامتری چیزی در حدود ۴ تا ۵ گیگابایت. این عدد تقریبی است و به پیادهسازی بستگی دارد. - زمان اجرا کوتاه است. پیشفرض
max_execution_timeدر PHP برابر ۳۰ ثانیه است و هاستها آن را کم یا زیاد میکنند. تولید چند صد توکن روی پردازنده، از این پنجره بیرون میزند. - پردازنده مشترک است. حتی اگر مدل بالا بیاید، مصرف مداوم پردازنده در هاست اشتراکی به محدودسازی حساب میرسد. سرعت هم روی پردازنده معمولاً چند توکن در ثانیه است؛ عدد دقیقش را باید روی همان سرویس اندازه بگیرید، نه از روی حدس.
پیش از هر تصمیمی، محدودیتهای واقعی همین نصب را ببینید. این کد یک اعلان مدیریتی میسازد که همان اعداد را نشان میدهد:
<?php
/**
* Plugin Name: اتصال به مدل زبانی — سنجش محدودیت هاست
* Description: یک اعلان مدیریتی که میگوید هاست شما اجازهٔ اجرای مدل محلی را میدهد یا نه.
*/
defined( 'ABSPATH' ) || exit;
add_action( 'admin_notices', 'splllm_show_limits' );
/**
* مقادیر واقعی همین نصب را نشان میدهد؛ پیش از تصمیمگیری دربارهٔ مدل محلی اجرا کنید.
*/
function splllm_show_limits() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$rows = array(
'memory_limit' => ini_get( 'memory_limit' ),
'max_execution_time' => ini_get( 'max_execution_time' ),
'post_max_size' => ini_get( 'post_max_size' ),
'upload_max_filesize' => ini_get( 'upload_max_filesize' ),
'proc_open' => function_exists( 'proc_open' ) ? 'فعال' : 'بسته',
'shell_exec' => function_exists( 'shell_exec' ) ? 'فعال' : 'بسته',
'mbstring' => function_exists( 'mb_substr' ) ? 'فعال' : 'بسته',
'curl' => function_exists( 'curl_init' ) ? 'فعال' : 'بسته',
);
echo '<div class="notice notice-info"><p><strong>محدودیتهای اجرای مدل روی این هاست:</strong></p><ul>';
foreach ( $rows as $label => $value ) {
echo '<li>' . esc_html( $label ) . ': ' . esc_html( (string) $value ) . '</li>';
}
echo '</ul></div>';
}اگر proc_open و shell_exec بسته باشند و حافظه زیر یک گیگابایت باشد، پروندهٔ مدل محلی روی هاست اشتراکی بسته است. دو راه واقعبینانه میمانند: مدل را روی دستگاه خودتان اجرا کنید و از راه یک تونل امن در دسترس سایت بگذارید، یا از یک endpoint بیرونی استفاده کنید. در حالت اول، همان قاعدهٔ کلید برقرار است: تونل باید احراز هویت داشته باشد و در معرض عمومی نباشد. اگر میخواهی مدل روی خودت اجرا شود، اجرای مدل محلی با Ollama مسیر جداگانهای دارد. اگر میخواهی مدل روی خودت اجرا شود، اجرای مدل محلی با Ollama مسیر جداگانهای دارد.
یک هشدار: مدل را داخل مسیر درخواست کاربر اجرا نکنید. حتی روی سرور اختصاصی، پاسخ ۳۰ ثانیهای به یک بازدیدکننده یعنی تجربهٔ خراب و احتمال تایماوت سرور. کارهای سنگین را به صف و زمانبند ببرید.
چه زمانی افزونه بهتر است
این مقاله مخالف افزونه نیست؛ مخالف افزونه برای کاری است که با سی خط کد درست انجام میشود. در این موقعیتها افزونه انتخاب بهتر یا تنها انتخاب معقول است:
- مالک سایت کد نمینویسد. اگر بعد از تحویل، هیچکس نمیتواند یک تایماوت را عوض کند، پنل تنظیمات افزونه ارزش پولش را دارد.
- چند ارائهدهنده با چرخش خودکار. مدیریت چند کلید، سقف مصرف و جانشین خودکار، کاری است که از یک قطعه کد کوچک بیرون میزند.
- جستوجوی معنایی و پاسخ از روی محتوای سایت. تقسیم متن به قطعه، ساخت بردار، ذخیره در پایگاه دادهٔ برداری و بازیابی — اینها یک پروژهٔ جدا هستند، نه یک تابع.
- تولید تصویر و متن در پیشخوان، برای تیم محتوا. رابط کاربری، تاریخچه و تنظیمات سطح نوشته را کسی باید بسازد.
- نیاز به پشتیبانی و بهروزرسانی. وقتی API عوض میشود، کسی باید وصله بدهد. اگر آن کسی شما نیستید، افزونه بخرید.
در مقابل، برای یک قابلیت مشخص — «خلاصه بساز»، «دسته پیشنهاد بده»، «به این فرم جواب بده» — کد خودتان کوتاهتر، سبکتر و قابل حسابرسیتر است. ترکیب هم ممکن است: افزونه برای قابلیتهای جانبی، کد خودتان برای هستهٔ کاری.
اگر تازه شروع کردهاید و میخواهید جای این کار در نقشهٔ کلی را ببینید، دو صفحهٔ آموزشهای هوش مصنوعی سئوپلاس و از کجا شروع کنم: نقشهٔ راه هوش مصنوعی مسیر را از مفاهیم تا پیادهسازی نشان میدهند.
چکلیست امنیتی قبل از انتشار
پیش از اینکه این کد روی سایت زنده برود، این فهرست را خط به خط بگذرانید:
- HTTPS برقرار است. بدون آن، کلید در مسیر و در هدرها قابل شنود است و Application Passwords هم کار نمیکند.
- کلید در جاوااسکریپت نیست. صفحه را باز کنید، «نمایش منبع» را بزنید و عبارت کلید را جستوجو کنید. باید صفر نتیجه بدهد.
- کلید در دیتابیس نیست. در
wp_optionsو در متای نوشتهها جستوجو کنید. permission_callbackدارد و خالی نیست. هر مسیر REST باید تصمیم دسترسی بگیرد.- nonce در همهٔ مسیرهای نوشتاری هست. هم در REST، هم در
admin-post.php. - ورودی محدود است. حداقل و حداکثر طول متن، و سقف
max_tokensبرای پاسخ. - محدودیت نرخ کار میکند. با یک حساب آزمایشی، بیش از سهمیه درخواست بفرستید و ۴۲۹ بگیرید.
- هر فیلد با تابع خودش پاکسازی میشود. متن، ایمیل، و متن چندخطی سه تابع متفاوت دارند.
- خروجی مدل escape میشود. با
esc_htmlیاwp_kses_post، نه خام. - خروجی مدل هیچ کاری را اجرا نمیکند. نه کوئری، نه دستور سیستم، نه تغییر تنظیمات، نه ارسال ایمیل انبوه.
sslverifyخاموش نشده است. اگر خطای گواهی دارید، گواهی سرور را درست کنید.- لاگ دادهٔ کاربر ندارد. فقط کد خطا، بدون متن پرامپت و بدون هدر Authorization.
- بکاپ دارید و میدانید چطور برگردید. فایل را جدا و قابل حذف نگه دارید؛
mu-pluginsبدون فعالسازی اجرا میشود، پس حذف فایل تنها راه خاموش کردنش است. - مسیرها آزموده شدهاند. یک بار با کاربر واردشده، یک بار با مهمان، یک بار با کلید غلط، و یک بار در حالی که سرویس در دسترس نیست.
بند چهاردهم را جدی بگیرید: بیشتر خرابیها در همان حالتی رخ میدهد که سرویس جواب نمیدهد و کد شما فرض کرده همیشه جواب میدهد. اگر پاسخ درست را در آن حالت ندیدید، کد آمادهٔ انتشار نیست.
بیشتر بخوانید
پرسشهای متداول
آیا برای اتصال وردپرس به مدل زبانی باید افزونه نصب کنم؟
نه. وردپرس از نسخهٔ ۲.۷ تابع wp_remote_post و از نسخهٔ ۴.۷ REST API را در هسته دارد. برای یک قابلیت مشخص، همان دو ابزار کافی است و افزونه فقط پنل و پشتیبانی اضافه میکند.
کلید API را کجا بگذارم که در مرورگر دیده نشود؟
در wp-config.php بهشکل یک ثابت، یا در متغیر محیطی. مرورگر هرگز نباید کلید را ببیند؛ مرورگر با endpoint خود سایت حرف میزند و سایت با کلید به سرویس مدل میزند.
چرا درخواستم با خطای http_request_timeout برمیگردد؟
چون پیشفرض تایماوت در وردپرس ۵ ثانیه است و مدل زبانی معمولاً بیشتر طول میکشد. مقدار timeout را صریح بالا ببرید و سقف زمان اجرای PHP هاست را هم بررسی کنید.
آیا میتوانم مدل زبانی را روی هاست اشتراکی اجرا کنم؟
در بیشتر موارد نه. proc_open و shell_exec بستهاند، حافظه به چند صد مگابایت محدود است و PHP فرایند پایدار ندارد. مدل را روی دستگاه خودتان یا روی یک سرویس بیرونی اجرا کنید.
فرق روش endpoint داخلی با فراخوانی مستقیم چیست؟
فراخوانی مستقیم از سمت سرور است و مرورگر در آن نقشی ندارد؛ برای زمانبند و ذخیرهٔ نوشته مناسب است. endpoint داخلی یک مسیر REST در سایت شما میسازد تا مرورگر هم بتواند بپرسد، بدون اینکه کلید را ببیند.
فهرست بقیهٔ مقالههای آموزشی این سایت در فهرست مقالات و آموزشهای سئوپلاس آمده است. اگر فقط یک قدم بردارید، همان روش اول باشد: یک فایل، یک ثابت، یک تابع؛ و پیش از هر چیز چکلیست بخش آخر را بگذرانید.





