آموزش سئو

اتصال وردپرس به مدل زبانی بدون افزونه: سه روش با REST API

وحید بهنام32 دقیقه مطالعه7 بازدید0 دیدگاه
اتصال وردپرس به مدل زبانی بدون افزونه

اگر تا حالا خواسته‌اید سایت وردپرسی‌تان خلاصه بسازد، عنوان پیشنهاد بدهد یا به سؤال خواننده جواب بدهد، به این جمله رسیده‌اید: «باید یک افزونه نصب کنم». این جمله همیشه درست نیست. وردپرس سال‌هاست ابزارهای لازم برای حرف‌زدن با یک مدل زبانی را در هسته دارد و هر افزونهٔ اتصال، در واقع همان ابزارها را با یک پنل تنظیمات بسته‌بندی می‌کند.

این مقاله سه روش بدون افزونه را با کد کامل 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! */ تعریف کنید:

PHP
// در wp-config.php، بالاتر از خط «That's all, stop editing!» بگذارید.
define( 'SPLLLM_API_KEY', 'کلید-واقعی-خود-را-اینجا-بگذارید' );

فایل wp-config.php بیرون از دیتابیس است، در بکاپ دیتابیس نمی‌آید و از پیشخوان وردپرس خوانده نمی‌شود. سطح دسترسی فایل را هم روی ۶۰۰ بگذارید تا فقط کاربر هاست بتواند بخواندش. کلید را در تنظیمات وردپرس، در فایل قالب، یا در جاوااسکریپت نگذارید؛ هر سه راه، کلید را جایی می‌برد که نباید.

ساختار درخواست و پاسخ: چه چیزی واقعاً رد و بدل می‌شود

پیش از کد، بدنهٔ خام را ببینید. این همان چیزی است که توابع PHP برای شما می‌سازند:

مسیر درخواست از وردپرس تا پاسخ مدل
از مرورگر تا پاسخ JSON، چهار گام
JSON
{
  "model": "your-model-name",
  "messages": [
    { "role": "system", "content": "پاسخ را کوتاه، دقیق و فارسی بنویس." },
    { "role": "user", "content": "سه عنوان برای یک مقالهٔ وردپرس پیشنهاد بده." }
  ],
  "max_tokens": 600,
  "temperature": 0.3
}

و پاسخ موفق، این شکل را دارد:

JSON
{
  "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
<?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
<?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
<?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:

BASH
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 در همان درخواستی اجرا می‌شود که دکمهٔ «به‌روزرسانی» را زده‌اید؛ اگر آنجا ۲۵ ثانیه منتظر مدل بمانید، ویرایشگر همان‌قدر معطل می‌شود و کاربر دوباره کلیک می‌کند. راه درست، زمان‌بندی یک رویداد و انجام کار در اجرای بعدی است.

سه روش اتصال وردپرس به مدل زبانی
سه روش، سه سطح کنترل: از یک درخواست ساده تا endpoint داخلی
PHP
<?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_failedDNS، گواهی یا اتصال شبکهsslverify را خاموش نکنید؛ علت را پیدا کنید
کد ۴۰۱ یا ۴۰۳کلید یا دسترسیتکرار نکنید؛ کلید را بررسی و در صورت لزوم بچرخانید
کد ۴۲۹محدودیت نرخ سرویسهدر Retry-After را بخوانید و همان‌قدر صبر کنید
کد ۵xxخرابی سمت سرویسیک تلاش دوباره با فاصله، بعد پیام عمومی
بدنهٔ خالی با کد ۲۰۰پاسخ ناقص یا فیلترشدهWP_Error برگردانید؛ خروجی خالی را ذخیره نکنید

کد زیر سه چیز اضافه می‌کند: یک تلاش دوبارهٔ کنترل‌شده، خواندن هدر Retry-After، و لاگ بدون داده. این فایل را کنار فایل روش اول بگذارید.

PHP
<?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 یک رمز است. هر جا مرورگر بتواند بخواندش، کاربر هم می‌تواند — و هر کسی که آن صفحه را باز کند، با کلید شما خرج می‌کند. سه لایهٔ دفاع لازم است:

مرز امن نگهداری کلید 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
<?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 عوض می‌شود، کسی باید وصله بدهد. اگر آن کسی شما نیستید، افزونه بخرید.

در مقابل، برای یک قابلیت مشخص — «خلاصه بساز»، «دسته پیشنهاد بده»، «به این فرم جواب بده» — کد خودتان کوتاه‌تر، سبک‌تر و قابل حسابرسی‌تر است. ترکیب هم ممکن است: افزونه برای قابلیت‌های جانبی، کد خودتان برای هستهٔ کاری.

اگر تازه شروع کرده‌اید و می‌خواهید جای این کار در نقشهٔ کلی را ببینید، دو صفحهٔ آموزش‌های هوش مصنوعی سئوپلاس و از کجا شروع کنم: نقشهٔ راه هوش مصنوعی مسیر را از مفاهیم تا پیاده‌سازی نشان می‌دهند.

چک‌لیست امنیتی قبل از انتشار

پیش از اینکه این کد روی سایت زنده برود، این فهرست را خط به خط بگذرانید:

  1. HTTPS برقرار است. بدون آن، کلید در مسیر و در هدرها قابل شنود است و Application Passwords هم کار نمی‌کند.
  2. کلید در جاوااسکریپت نیست. صفحه را باز کنید، «نمایش منبع» را بزنید و عبارت کلید را جست‌وجو کنید. باید صفر نتیجه بدهد.
  3. کلید در دیتابیس نیست. در wp_options و در متای نوشته‌ها جست‌وجو کنید.
  4. permission_callback دارد و خالی نیست. هر مسیر REST باید تصمیم دسترسی بگیرد.
  5. nonce در همهٔ مسیرهای نوشتاری هست. هم در REST، هم در admin-post.php.
  6. ورودی محدود است. حداقل و حداکثر طول متن، و سقف max_tokens برای پاسخ.
  7. محدودیت نرخ کار می‌کند. با یک حساب آزمایشی، بیش از سهمیه درخواست بفرستید و ۴۲۹ بگیرید.
  8. هر فیلد با تابع خودش پاک‌سازی می‌شود. متن، ایمیل، و متن چندخطی سه تابع متفاوت دارند.
  9. خروجی مدل escape می‌شود. با esc_html یا wp_kses_post، نه خام.
  10. خروجی مدل هیچ کاری را اجرا نمی‌کند. نه کوئری، نه دستور سیستم، نه تغییر تنظیمات، نه ارسال ایمیل انبوه.
  11. sslverify خاموش نشده است. اگر خطای گواهی دارید، گواهی سرور را درست کنید.
  12. لاگ دادهٔ کاربر ندارد. فقط کد خطا، بدون متن پرامپت و بدون هدر Authorization.
  13. بکاپ دارید و می‌دانید چطور برگردید. فایل را جدا و قابل حذف نگه دارید؛ mu-plugins بدون فعال‌سازی اجرا می‌شود، پس حذف فایل تنها راه خاموش کردنش است.
  14. مسیرها آزموده شده‌اند. یک بار با کاربر واردشده، یک بار با مهمان، یک بار با کلید غلط، و یک بار در حالی که سرویس در دسترس نیست.

بند چهاردهم را جدی بگیرید: بیشتر خرابی‌ها در همان حالتی رخ می‌دهد که سرویس جواب نمی‌دهد و کد شما فرض کرده همیشه جواب می‌دهد. اگر پاسخ درست را در آن حالت ندیدید، کد آمادهٔ انتشار نیست.

بیشتر بخوانید

پرسش‌های متداول

آیا برای اتصال وردپرس به مدل زبانی باید افزونه نصب کنم؟

نه. وردپرس از نسخهٔ ۲.۷ تابع wp_remote_post و از نسخهٔ ۴.۷ REST API را در هسته دارد. برای یک قابلیت مشخص، همان دو ابزار کافی است و افزونه فقط پنل و پشتیبانی اضافه می‌کند.

کلید API را کجا بگذارم که در مرورگر دیده نشود؟

در wp-config.php به‌شکل یک ثابت، یا در متغیر محیطی. مرورگر هرگز نباید کلید را ببیند؛ مرورگر با endpoint خود سایت حرف می‌زند و سایت با کلید به سرویس مدل می‌زند.

چرا درخواستم با خطای http_request_timeout برمی‌گردد؟

چون پیش‌فرض تایم‌اوت در وردپرس ۵ ثانیه است و مدل زبانی معمولاً بیشتر طول می‌کشد. مقدار timeout را صریح بالا ببرید و سقف زمان اجرای PHP هاست را هم بررسی کنید.

آیا می‌توانم مدل زبانی را روی هاست اشتراکی اجرا کنم؟

در بیشتر موارد نه. proc_open و shell_exec بسته‌اند، حافظه به چند صد مگابایت محدود است و PHP فرایند پایدار ندارد. مدل را روی دستگاه خودتان یا روی یک سرویس بیرونی اجرا کنید.

فرق روش endpoint داخلی با فراخوانی مستقیم چیست؟

فراخوانی مستقیم از سمت سرور است و مرورگر در آن نقشی ندارد؛ برای زمان‌بند و ذخیرهٔ نوشته مناسب است. endpoint داخلی یک مسیر REST در سایت شما می‌سازد تا مرورگر هم بتواند بپرسد، بدون اینکه کلید را ببیند.

فهرست بقیهٔ مقاله‌های آموزشی این سایت در فهرست مقالات و آموزش‌های سئوپلاس آمده است. اگر فقط یک قدم بردارید، همان روش اول باشد: یک فایل، یک ثابت، یک تابع؛ و پیش از هر چیز چک‌لیست بخش آخر را بگذرانید.

نوشته‌شده و بازبینی‌شده توسط

متخصص سئو، وردپرس و تولید محتوا

اولین نفری باش که نظر می‌ده!

فقط نام لازم است، ایمیل اختیاریه و منتشر نمی‌شه.