'; echo 'AIO Agent Article Generator: PHP 7.4以上が必要です。'; echo '現在のバージョン: ' . esc_html(PHP_VERSION); echo '

'; }); return; // プラグイン読み込み中止 } // 必須PHP拡張チェック(load_dependencies 前に検出) $aagl_missing_ext = array(); if (!extension_loaded('mbstring')) { $aagl_missing_ext[] = 'mbstring'; } if (!extension_loaded('dom')) { $aagl_missing_ext[] = 'dom'; } if (!extension_loaded('openssl')) { $aagl_missing_ext[] = 'openssl'; } if (!empty($aagl_missing_ext)) { add_action('admin_notices', function () use ($aagl_missing_ext) { echo '

'; echo 'AIO Agent Article Generator: 必須PHP拡張が不足しています: '; echo esc_html(implode(', ', $aagl_missing_ext)); echo '

'; }); return; // プラグイン読み込み中止 } // WordPress 5.3 未満で欠けている関数を補う(4.9系のサイトが実在するため) // クラス定義や他の読み込みより前に置くこと require_once __DIR__ . '/includes/compat-wp53.php'; // プラグイン定数の定義 define('AAGL_VERSION', '1.0.1'); define('AAGL_PLUGIN_FILE', __FILE__); define('AAGL_PLUGIN_BASENAME', plugin_basename(__FILE__)); define('AAGL_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('AAGL_PLUGIN_URL', plugin_dir_url(__FILE__)); define('AAGL_INCLUDES_DIR', AAGL_PLUGIN_DIR . 'includes/'); define('AAGL_TEMPLATES_DIR', AAGL_PLUGIN_DIR . 'templates/'); define('AAGL_ASSETS_URL', AAGL_PLUGIN_URL . 'assets/'); define('AAGL_DEFAULT_CLAUDE_MODEL', 'claude-sonnet-4-6'); define('AAGL_DEFAULT_GEMINI_MODEL', 'gemini-2.5-flash'); // 配布エディション判定。Lite版(WordPress.org公開版)は build-lite.sh がビルド時に true へ置換する。 // API タイムアウト設定(秒) define('AAGL_TIMEOUT_AI_API', 120); // AI API呼び出し(Claude/Gemini) define('AAGL_TIMEOUT_SEARCH_API', 30); // 外部APIの検索系呼び出し define('AAGL_TIMEOUT_CONTENT_FETCH', 20); // URL コンテンツ取得 define('AAGL_TIMEOUT_IMAGE_FETCH', 60); // 画像ダウンロード // リトライ設定 define('AAGL_MAX_RETRIES', 2); // AI記事生成の最大リトライ回数 // レート制限(マイクロ秒) define('AAGL_RATE_LIMIT_DELAY', 500000); // API間の待機時間(0.5秒) // 記事デフォルト設定 define('AAGL_DEFAULT_MIN_LENGTH', 800); // 記事最小文字数 define('AAGL_DEFAULT_MAX_LENGTH', 2000); // 記事最大文字数 define('AAGL_DEFAULT_CATEGORY_ID', 1); // デフォルトカテゴリID // OPcache自動無効化: FTPアップロード後にバイトコードキャッシュを確実に更新 // file_get_contents()はOPcacheを経由せずファイルシステムから直接読み取るため、 // キャッシュされた古いAAGL_VERSIONではなく、実ファイルのバージョンで比較できる if ( function_exists( 'opcache_invalidate' ) && is_admin() ) { $aagl_file_header = @file_get_contents( __FILE__, false, null, 0, 500 ); if ( $aagl_file_header && preg_match( '/Version:\s*([\d.]+)/', $aagl_file_header, $aagl_ver_match ) ) { $aagl_file_version = $aagl_ver_match[1]; $aagl_cached_version = get_option( 'aagl_opcache_version', '' ); if ( $aagl_file_version !== $aagl_cached_version ) { $aagl_dirs = array( __DIR__, __DIR__ . '/includes', __DIR__ . '/templates' ); foreach ( $aagl_dirs as $aagl_dir ) { $aagl_files = glob( $aagl_dir . '/*.php' ); if ( $aagl_files ) { foreach ( $aagl_files as $aagl_f ) { @opcache_invalidate( $aagl_f, true ); } } } update_option( 'aagl_opcache_version', $aagl_file_version, true ); } } } // プラグイン多重読み込み防止(ランタイムフラグ方式) // 注: class_exists() による防止は OPcache early binding と干渉するため不使用 if ( ! empty( $GLOBALS['aagl_plugin_loaded'] ) ) { return; } $GLOBALS['aagl_plugin_loaded'] = true; /** * メインプラグインクラス */ class AIO_Article_Generator_Lite { /** * プラグインインスタンス */ private static $instance = null; /** * シングルトンインスタンス取得 */ public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } /** * コンストラクタ */ private function __construct() { $this->init(); } /** * プラグイン初期化 */ private function init() { // アクティベーション・ディアクティベーションフックを登録 register_activation_hook(AAGL_PLUGIN_FILE, array($this, 'activate')); register_deactivation_hook(AAGL_PLUGIN_FILE, array($this, 'deactivate')); // プラグイン削除フック register_uninstall_hook(AAGL_PLUGIN_FILE, array('AIO_Article_Generator_Lite', 'uninstall')); // WordPress初期化後にプラグインを開始 add_action('init', array($this, 'plugin_init')); add_action('admin_init', array($this, 'admin_init')); add_action('admin_menu', array($this, 'setup_admin_menu')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); // フロントエンド: 生成記事にCSSを適用 add_action('wp_enqueue_scripts', array($this, 'enqueue_front_styles')); add_filter('the_content', array($this, 'wrap_aagl_content'), 99); // モデル廃止等によるAPI 404を管理画面に表面化 add_action('admin_notices', array($this, 'render_model_error_notice')); // モデル/プロバイダの自動切替が起きたことを管理画面に表面化 add_action('admin_notices', array($this, 'render_auto_switch_notice')); // 必要なファイルを読み込み $this->load_dependencies(); // モデルレジストリのフック登録(CRON実処理とAJAX) // CRONの「登録」はactivate()とバージョン移行時のみ行う(毎ページロードで叩かない) if (class_exists('AAGL_Model_Registry')) { AAGL_Model_Registry::init(); } } /** * 依存関係ファイルの読み込み * * 各ファイルの存在を事前チェックし、不完全なアップロードによる * fatal error を防止する。 */ private function load_dependencies() { $required_files = array( 'database.php', 'admin-pages.php', 'utilities.php', 'model-registry.php', 'keyword-functions.php', 'ai-integration.php', 'wordpress-native-functions.php', ); foreach ($required_files as $file) { $filepath = AAGL_INCLUDES_DIR . $file; if (!file_exists($filepath)) { add_action('admin_notices', function () use ($file) { echo '

'; echo 'AIO Agent Article Generator: 必須ファイルが見つかりません: includes/' . esc_html($file); echo ' — プラグインを再インストールしてください。

'; }); return; } require_once $filepath; } } /** * プラグインアクティベーション処理 */ public function activate() { // マルチサイトのネットワーク有効化は非対応 // phpcs:ignore WordPress.Security.NonceVerification.Recommended if (is_multisite() && isset($_GET['networkwide']) && $_GET['networkwide'] === '1') { deactivate_plugins(plugin_basename(AAGL_PLUGIN_FILE)); wp_die( 'AIO Agent Article Generator はマルチサイトのネットワーク有効化に対応していません。各サイト個別に有効化してください。

' . 'プラグイン一覧に戻る', 'AIO Agent Article Generator - ネットワーク有効化非対応', array('back_link' => true) ); } // 必須PHP拡張のチェック $missing_extensions = array(); if (!extension_loaded('mbstring')) { $missing_extensions[] = 'mbstring(日本語テキスト処理に必要)'; } if (!extension_loaded('dom')) { $missing_extensions[] = 'dom(HTML解析・コンテンツ抽出に必要)'; } if (!extension_loaded('openssl')) { $missing_extensions[] = 'openssl(AI API通信に必要)'; } if (!empty($missing_extensions)) { deactivate_plugins(plugin_basename(AAGL_PLUGIN_FILE)); wp_die( sprintf( 'AIO Agent Article Generator を有効化するには以下のPHP拡張が必要です:

%s

サーバー管理者に連絡して拡張をインストールしてください。

プラグイン一覧に戻る', wp_kses(implode('
', $missing_extensions), array( 'br' => array() )), esc_url(admin_url('plugins.php')) ), 'AIO Agent Article Generator - 必須PHP拡張が不足しています', array('back_link' => true) ); } // データベーステーブル作成 AAGL_Database::create_tables(); // デフォルト設定を作成 $this->setup_default_options(); // ハードコードテンプレートをデータベースに移行 $this->migrate_hardcoded_templates(); // バージョン情報を保存 update_option('aagl_version', AAGL_VERSION); update_option('aagl_activation_time', current_time('mysql')); // 権限確認 if (!current_user_can('activate_plugins')) { return; } // グローバルCRON登録(毎ページロードではなく有効化時のみ) // 既存スケジュールをクリアしてから再登録(冪等な再登録パターン) // // 間隔はフィルターで変えられる。個別スケジュールは自前の recurring フックを // 持つため、このグローバル巡回は取りこぼしの拾い直しにすぎない。 // 顧客サーバーへの負荷を抑えたい案件では 'daily' に落とせる。 // (過去にCRONの集中発火が顧客サイトのWAF閾値を超え、 // お客様の管理画面が403でブロックされる障害を起こした経緯がある) // モデル一覧の週次更新CRONを登録(新モデル追随・廃止の事前検知) if (class_exists('AAGL_Model_Registry')) { AAGL_Model_Registry::schedule_refresh(); } // フラッシュリライトルール flush_rewrite_rules(); } /** * プラグインディアクティベーション処理 */ public function deactivate() { // 権限確認 if (!current_user_can('activate_plugins')) { return; } // モデル一覧更新CRONをクリア if (class_exists('AAGL_Model_Registry')) { AAGL_Model_Registry::unschedule_refresh(); } // フラッシュリライトルール flush_rewrite_rules(); } /** * プラグイン削除処理(静的メソッド) */ /** * グローバル巡回CRONの間隔を返す。 * * 既定は 'hourly'。負荷を抑えたい案件では wp-config.php に * define('AAGL_GLOBAL_CRON_RECURRENCE', 'daily'); * を書くか、フィルター aagl_global_cron_recurrence で変更する。 * * @return string wp_get_schedules() のキー */ public static function get_global_cron_recurrence() { $recurrence = defined('AAGL_GLOBAL_CRON_RECURRENCE') ? AAGL_GLOBAL_CRON_RECURRENCE : 'hourly'; $recurrence = apply_filters('aagl_global_cron_recurrence', $recurrence); // 未登録の間隔を渡すとCRONが二度と動かなくなるため、実在するものだけ通す $schedules = wp_get_schedules(); return isset($schedules[$recurrence]) ? $recurrence : 'hourly'; } public static function uninstall() { // 権限確認 if (!current_user_can('activate_plugins')) { return; } // 案件によっては「削除」で設定が消えると復旧が重い。 // wp-config.php に define('AAGL_KEEP_DATA_ON_UNINSTALL', true); を書けば残す。 if (defined('AAGL_KEEP_DATA_ON_UNINSTALL') && AAGL_KEEP_DATA_ON_UNINSTALL) { return; } // プラグイン設定削除 delete_option('aagl_settings'); delete_option('aagl_version'); delete_option('aagl_activation_time'); // ユーザーメタ削除 delete_metadata('user', 0, 'aagl_user_preferences', '', true); // Lite版(WordPress.org配布): 削除時はプラグインのデータを残さない // (キーワードテーブル・テンプレート・モデルカタログ・transient)。生成済み投稿は残す。 global $wpdb; $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}aagl_main_keywords"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}aagl_sub_keywords"); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s", $wpdb->esc_like('aagl_') . '%', $wpdb->esc_like('_aagl_') . '%', $wpdb->esc_like('_transient_aagl_') . '%', $wpdb->esc_like('_transient_timeout_aagl_') . '%' ) ); } /** * デフォルト設定のセットアップ */ private function setup_default_options() { $default_settings = array( 'api_settings' => array( 'claude_api_key' => '', 'gemini_api_key' => '', ), 'system_settings' => array( // 日次上限は暴走時の安全弁。月次は0=無制限(daily運用は月100本超になり得るため既定では制限しない) 'max_articles_per_day' => 10, 'max_articles_per_month' => 0 ), 'ai_settings' => array( 'preferred_ai' => 'claude', 'claude_model' => AAGL_DEFAULT_CLAUDE_MODEL, 'gemini_model' => AAGL_DEFAULT_GEMINI_MODEL, // 手入力モデルID(新モデル発表当日に、プラグイン更新なしで移行するための欄) 'claude_model_custom' => '', 'gemini_model_custom' => '', // 片方のAIがモデル廃止等で使えなくなったとき、もう片方で生成を続ける 'provider_fallback' => 1, 'temperature' => 0.7 ) ); // 設定が存在しない場合のみ追加 if (!get_option('aagl_settings')) { update_option('aagl_settings', $default_settings, false); } } /** * ハードコードテンプレートをデータベースに移行(移行済みの場合はスキップ) */ private function migrate_hardcoded_templates() { if (get_option('aagl_templates_migrated', false)) { return; } update_option('aagl_templates_migrated', true); } /** * WordPress初期化 */ public function plugin_init() { // 国際化対応(WP.org以外での配布やWP 4.6未満環境に対応) load_plugin_textdomain('aio-agent-article-generator', false, dirname(AAGL_PLUGIN_BASENAME) . '/languages/'); // phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound // プラグインバージョンチェック・アップグレード処理 $this->check_version_upgrade(); // Stage 2: キーワード管理クラスの初期化 if (class_exists('AAGL_Keyword_Functions')) { AAGL_Keyword_Functions::get_instance(); } if (class_exists('AAGL_AI_Integration')) { AAGL_AI_Integration::get_instance(); } // ショートコード登録 add_shortcode('aagl_articles', array($this, 'shortcode_aagl_articles')); } /** * [aagl_articles] ショートコード — 生成記事の一覧を表示(ページネーション対応) * * @param array $atts ショートコード属性 * - per_page: 1ページあたりの表示件数(デフォルト: 20) * - orderby: ソート順(date / title / modified、デフォルト: date) * - order: 昇順/降順(ASC / DESC、デフォルト: DESC) * @return string HTML出力 */ public function shortcode_aagl_articles($atts) { $atts = shortcode_atts(array( 'per_page' => 20, 'orderby' => 'date', 'order' => 'DESC', ), $atts, 'aagl_articles'); $per_page = max(1, min(100, intval($atts['per_page']))); $paged = max(1, intval(get_query_var('paged', 1))); $aagl_post_type = AAGL_Utilities::get_post_type_setting(); $args = array( 'post_type' => $aagl_post_type, 'post_status' => 'publish', 'posts_per_page' => $per_page, 'paged' => $paged, 'orderby' => in_array($atts['orderby'], array('date', 'title', 'modified', 'ID'), true) ? $atts['orderby'] : 'date', 'order' => in_array(strtoupper($atts['order']), array('ASC', 'DESC'), true) ? strtoupper($atts['order']) : 'DESC', 'meta_query' => array( array( 'key' => '_aagl_generated', 'value' => 'yes', ), ), ); // 固定ページモード+親ページ設定時は子ページのみ表示 if ($aagl_post_type === 'page') { $parent_page_id = AAGL_Utilities::get_parent_page_setting(); if ($parent_page_id > 0) { $args['post_parent'] = $parent_page_id; } } $query = new WP_Query($args); if (!$query->have_posts()) { return '

' . esc_html__('記事がまだありません。', 'aio-agent-article-generator') . '

'; } wp_enqueue_style('aagl-article-list'); $output = ''; $output .= ''; // ページネーション $total_pages = $query->max_num_pages; if ($total_pages > 1) { $output .= $this->render_pagination($paged, $total_pages); } wp_reset_postdata(); return $output; } /** * ページネーションHTMLを生成 * * @param int $current 現在のページ番号 * @param int $total 総ページ数 * @return string ページネーションHTML */ private function render_pagination($current, $total) { $output = ''; return $output; } /** * 管理画面初期化 */ public function admin_init() { // 管理画面での権限チェック if (!current_user_can('manage_options')) { return; } // 設定登録 $this->register_settings(); } /** * 設定の登録 */ private function register_settings() { register_setting('aagl_settings_group', 'aagl_settings', array($this, 'validate_settings')); } /** * APIキーのサニタイズ+暗号化 * * @param string $api_key 入力されたAPIキー * @param string $existing_encrypted 既存の暗号化済みキー(マスク入力時に保持用) * @return string 暗号化済みAPIキー */ private function sanitize_api_key($api_key, $existing_encrypted = '') { $clean = sanitize_text_field($api_key); if (empty($clean)) { return ''; } // マスク文字列(••••)が送信された場合は既存キーをそのまま保持 if (preg_match('/^[•]+$/', $clean)) { return $existing_encrypted; } return AAGL_Utilities::encrypt_api_key($clean); } /** * 設定バリデーション(エラーハンドリング強化) */ public function validate_settings($input) { try { $sanitized = array(); // 既存の設定を取得 $current_settings = get_option('aagl_settings', array()); // API設定のサニタイズ if (isset($input['api_settings'])) { $current_api = $current_settings['api_settings'] ?? array(); $sanitized['api_settings'] = array( 'claude_api_key' => $this->sanitize_api_key($input['api_settings']['claude_api_key'] ?? '', $current_api['claude_api_key'] ?? ''), 'gemini_api_key' => $this->sanitize_api_key($input['api_settings']['gemini_api_key'] ?? '', $current_api['gemini_api_key'] ?? ''), 'ai_provider' => sanitize_key($input['api_settings']['ai_provider'] ?? 'claude') ); } else { // API設定が送信されていない場合は既存の設定を保持 $sanitized['api_settings'] = $current_settings['api_settings'] ?? array(); } // システム設定のサニタイズ if (isset($input['system_settings'])) { $post_type_val = sanitize_key($input['system_settings']['post_type'] ?? 'post'); $sanitized['system_settings'] = array( 'default_post_status' => sanitize_key($input['system_settings']['default_post_status'] ?? 'draft'), 'auto_assign_category' => (bool) ($input['system_settings']['auto_assign_category'] ?? true), 'default_category_id' => absint($input['system_settings']['default_category_id'] ?? 1), 'enable_featured_image' => (bool) ($input['system_settings']['enable_featured_image'] ?? true), 'max_articles_per_day' => absint($input['system_settings']['max_articles_per_day'] ?? 10), 'max_articles_per_month' => absint($input['system_settings']['max_articles_per_month'] ?? 0), 'article_min_length' => absint($input['system_settings']['article_min_length'] ?? 800), 'article_max_length' => absint($input['system_settings']['article_max_length'] ?? 2000), 'post_type' => post_type_exists($post_type_val) ? $post_type_val : 'post', 'parent_page_id' => absint($input['system_settings']['parent_page_id'] ?? 0), ); } else { $sanitized['system_settings'] = $current_settings['system_settings'] ?? array(); } // AI設定のサニタイズ if (isset($input['ai_settings'])) { // 注意: このコールバックと admin-pages.php の保存ハンドラの2経路が // 同じ aagl_settings を書く。新しいキーは必ず両方に定義すること // (過去に post_type と max_articles_per_month が片方漏れで消えている) $sanitized['ai_settings'] = array( 'preferred_ai' => sanitize_key($input['ai_settings']['preferred_ai'] ?? 'claude'), 'claude_model' => sanitize_text_field($input['ai_settings']['claude_model'] ?? AAGL_DEFAULT_CLAUDE_MODEL), 'gemini_model' => sanitize_text_field($input['ai_settings']['gemini_model'] ?? AAGL_DEFAULT_GEMINI_MODEL), 'claude_model_custom' => sanitize_text_field($input['ai_settings']['claude_model_custom'] ?? ''), 'gemini_model_custom' => sanitize_text_field($input['ai_settings']['gemini_model_custom'] ?? ''), 'provider_fallback' => empty($input['ai_settings']['provider_fallback']) ? 0 : 1, 'temperature' => floatval($input['ai_settings']['temperature'] ?? 0.7), 'max_tokens' => absint($input['ai_settings']['max_tokens'] ?? 2000) ); // 設定を見直した以上、自動切替の通知は役目を終える if (class_exists('AAGL_Model_Registry')) { AAGL_Model_Registry::clear_auto_switches(); } } else { $sanitized['ai_settings'] = $current_settings['ai_settings'] ?? array(); } // 成功メッセージを追加 add_settings_error( 'aagl_settings', 'aagl_settings_saved', '設定を保存しました。', 'updated' ); return $sanitized; } catch (Exception $e) { AAGL_Utilities::log('Settings: バリデーションエラー - ' . $e->getMessage(), 'error'); // エラーメッセージを追加 add_settings_error( 'aagl_settings', 'aagl_settings_error', '設定の保存に失敗しました。詳細はエラーログを確認してください。', 'error' ); // 既存の設定を返す return get_option('aagl_settings', array()); } } /** * バージョンアップグレード処理 */ private function check_version_upgrade() { $current_version = get_option('aagl_version'); if ($current_version !== AAGL_VERSION) { // アップグレード処理を実行 $this->upgrade_plugin($current_version, AAGL_VERSION); update_option('aagl_version', AAGL_VERSION); } } /** * プラグインアップグレード処理 */ private function upgrade_plugin($old_version, $new_version) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed // バージョン別アップグレード処理 // 2.5.0: テンプレート変数を {{DOUBLE}} ブラケットに統一 if (version_compare($old_version, '2.5.0', '<')) { $this->migrate_template_variables(); } // 2.11.0: APIキーを平文から暗号化に移行 if (version_compare($old_version, '2.11.0', '<')) { $this->migrate_api_keys_to_encrypted(); } // 2.17.0: 生成上限カウント修復に伴い既存の上限値を0(無制限)にリセット if (version_compare($old_version, '2.17.0', '<')) { $this->reset_article_limits_for_2_17(); } // 2.18.0: モデル一覧の週次更新CRONを登録し、初回の取得を即実行 // // プラグインの更新はファイル差し替えで行われることが多く、その場合 // activate() は走らない。既存の導入先にCRONを行き渡らせるため、 // バージョン移行のタイミングで登録する。 if (version_compare($old_version, '2.18.0', '<')) { $this->setup_model_registry_for_2_18(); } // データベーステーブル更新が必要な場合 AAGL_Database::create_tables(); } /** * v2.18.0移行: モデル一覧の週次更新CRONを登録し、既定値を補完する * * 既存の導入先は ai_settings に手入力欄・フォールバック設定のキーを持たない。 * 未定義のままだと保存時に消える(過去にpost_typeとmax_articles_per_monthで * 同じ事故を起こしている)ため、移行時に明示的に初期値を入れておく。 */ private function setup_model_registry_for_2_18() { if (!class_exists('AAGL_Model_Registry')) { return; } // 週次CRONを登録。初回だけ5分後に前倒しして選択肢を早く埋める // // ここで refresh() を直接呼ぶと、更新後の最初のページ表示で // 外部API通信(最大60秒)を待たせることになるため、必ずCRON経由にする。 AAGL_Model_Registry::schedule_refresh(300); // ai_settings に新キーを補完(既存値は壊さない) $settings = get_option('aagl_settings', array()); $ai_settings = isset($settings['ai_settings']) && is_array($settings['ai_settings']) ? $settings['ai_settings'] : array(); $defaults = array( 'claude_model_custom' => '', 'gemini_model_custom' => '', 'provider_fallback' => 1, ); foreach ($defaults as $key => $value) { if (!array_key_exists($key, $ai_settings)) { $ai_settings[$key] = $value; } } $settings['ai_settings'] = $ai_settings; update_option('aagl_settings', $settings, false); AAGL_Utilities::clear_settings_cache(); AAGL_Utilities::log('v2.18.0 migration: model registry initialized'); } /** * v2.17.0移行: 日次/月次の生成上限を0(無制限)にリセット * * 旧バージョンでは上限カウントが常に0で上限機能が無効だったため(履歴テーブル * INSERT欠落バグ)、保存済みの上限値は実際の生成量と一度も照合されていない。 * カウント修復と同時に旧上限値を有効化すると、稼働中のスケジュール生成が * 月の途中で停止する恐れがあるため、一旦無制限に戻し、意識的に再設定された * 時点から上限を効かせる。 */ private function reset_article_limits_for_2_17() { $settings = get_option('aagl_settings', array()); $system_settings = $settings['system_settings'] ?? array(); $old_daily = intval($system_settings['max_articles_per_day'] ?? 0); $old_monthly = intval($system_settings['max_articles_per_month'] ?? 0); $system_settings['max_articles_per_day'] = 0; $system_settings['max_articles_per_month'] = 0; $settings['system_settings'] = $system_settings; update_option('aagl_settings', $settings, false); AAGL_Utilities::clear_settings_cache(); AAGL_Utilities::log(sprintf('v2.17.0移行: 生成上限をリセットしました(日次 %d→0, 月次 %d→0)。上限機能が修復されたため、必要に応じてAPI設定画面で再設定してください。', $old_daily, $old_monthly), 'info'); AAGL_Utilities::add_admin_notice('AI記事生成: 生成上限カウントの不具合を修正しました。これに伴い日次/月次上限を一旦「無制限」にリセットしています。上限を使う場合はAPI設定画面で再設定してください。', 'warning'); } /** * APIキーを平文から暗号化に移行 */ private function migrate_api_keys_to_encrypted() { $settings = get_option('aagl_settings', array()); $api_settings = $settings['api_settings'] ?? array(); $updated = false; $key_names = array('claude_api_key', 'gemini_api_key'); foreach ($key_names as $name) { $val = $api_settings[$name] ?? ''; if (!empty($val) && 0 !== strpos($val, 'enc:')) { $api_settings[$name] = AAGL_Utilities::encrypt_api_key($val); $updated = true; } } if ($updated) { $settings['api_settings'] = $api_settings; update_option('aagl_settings', $settings, false); AAGL_Utilities::clear_settings_cache(); AAGL_Utilities::log('APIキーを暗号化に移行しました', 'info'); } } /** * テンプレート変数を {single} から {{DOUBLE}} ブラケットに移行 */ private function migrate_template_variables() { $templates = get_option('aagl_article_templates', array()); if (empty($templates)) { return; } $variable_map = array( '{title}' => '{{TITLE}}', '{main_keyword}' => '{{MAIN_KEYWORD}}', '{sub_keywords}' => '{{SUB_KEYWORDS}}', '{reference_section}' => '{{REFERENCE_INSTRUCTION}}', '{keyword}' => '{{MAIN_KEYWORD}}', ); $updated = false; foreach ($templates as &$template) { if (!empty($template['prompt_template'])) { $original = $template['prompt_template']; $template['prompt_template'] = str_replace( array_keys($variable_map), array_values($variable_map), $template['prompt_template'] ); if ($original !== $template['prompt_template']) { $updated = true; } } } unset($template); if ($updated) { update_option('aagl_article_templates', $templates, false); AAGL_Utilities::log('Template variables migrated to {{DOUBLE}} bracket format'); } } /** * 管理メニューセットアップ */ public function setup_admin_menu() { // メインメニューページ add_menu_page( __('AI記事生成', 'aio-agent-article-generator'), __('AI記事生成', 'aio-agent-article-generator'), 'manage_options', 'aagl-dashboard', array('AAGL_Admin_Pages', 'dashboard_page'), 'dashicons-edit-large', 30 ); // サブメニューページ add_submenu_page( 'aagl-dashboard', __('ダッシュボード', 'aio-agent-article-generator'), __('ダッシュボード', 'aio-agent-article-generator'), 'manage_options', 'aagl-dashboard', array('AAGL_Admin_Pages', 'dashboard_page') ); add_submenu_page( 'aagl-dashboard', __('キーワード管理', 'aio-agent-article-generator'), __('キーワード管理', 'aio-agent-article-generator'), 'manage_options', 'aagl-keywords', array('AAGL_Admin_Pages', 'keywords_page') ); // Lite版: スケジュールの代わりに、その場で1本生成するページ add_submenu_page( 'aagl-dashboard', __('記事生成', 'aio-agent-article-generator'), __('記事生成', 'aio-agent-article-generator'), 'manage_options', 'aagl-generate', array('AAGL_Admin_Pages', 'generate_page') ); add_submenu_page( 'aagl-dashboard', __('生成履歴', 'aio-agent-article-generator'), __('生成履歴', 'aio-agent-article-generator'), 'manage_options', 'aagl-history', array('AAGL_Admin_Pages', 'history_page') ); add_submenu_page( 'aagl-dashboard', __('テンプレート管理', 'aio-agent-article-generator'), __('テンプレート管理', 'aio-agent-article-generator'), 'manage_options', 'aagl-templates', array('AAGL_Admin_Pages', 'templates_page') ); add_submenu_page( 'aagl-dashboard', __('API設定', 'aio-agent-article-generator'), __('API設定', 'aio-agent-article-generator'), 'manage_options', 'aagl-settings', array('AAGL_Admin_Pages', 'settings_page') ); add_submenu_page( 'aagl-dashboard', __('マニュアル', 'aio-agent-article-generator'), __('マニュアル', 'aio-agent-article-generator'), 'manage_options', 'aagl-manual', array('AAGL_Admin_Pages', 'manual_page') ); // v2.16.4: 編集者向けにテンプレート管理のみ単独メニューで開放 // (管理者は上の通常メニューを使用。設定・キーワード・スケジュールは引き続き管理者のみ) if ( ! current_user_can( 'manage_options' ) && current_user_can( 'edit_others_posts' ) ) { add_menu_page( __('AI記事テンプレート', 'aio-agent-article-generator'), __('AI記事テンプレート', 'aio-agent-article-generator'), 'edit_others_posts', 'aagl-templates', array('AAGL_Admin_Pages', 'templates_page'), 'dashicons-welcome-write-blog', 31 ); } } /** * フロントエンドCSS読み込み(生成記事のみ) */ public function enqueue_front_styles() { // ショートコードは生成記事以外のページにも置かれるため、登録だけは常に行う。 wp_register_style( 'aagl-article-list', AAGL_ASSETS_URL . 'css/article-list.css', array(), AAGL_VERSION ); if (!is_singular()) { return; } $post_id = get_queried_object_id(); if ($post_id && get_post_meta($post_id, '_aagl_generated', true) === 'yes') { wp_enqueue_style( 'aagl-front-style', AAGL_ASSETS_URL . 'css/front.css', array(), AAGL_VERSION ); } } /** * 生成記事のコンテンツを .aagl-content でラップ */ public function wrap_aagl_content($content) { if (!is_singular() || !in_the_loop() || !is_main_query()) { return $content; } $post_id = get_queried_object_id(); if ($post_id && $content && get_post_meta($post_id, '_aagl_generated', true) === 'yes') { $content = '
' . $content . '
'; } return $content; } /** * モデルエラー通知の表示(モデル廃止等によるAPI 404検出時) */ public function render_model_error_notice() { if (!current_user_can('manage_options')) { return; } $errors = get_option('aagl_model_error', array()); if (empty($errors) || !is_array($errors)) { return; } foreach ($errors as $provider => $info) { echo '

AIO Agent Article Generator: '; echo esc_html(sprintf( '%s API がモデル「%s」を受け付けませんでした(%s)。モデルが廃止された可能性があります。API設定画面でモデルを変更してください。', ucfirst($provider), $info['model'] ?? '', $info['time'] ?? '' )); echo '

'; } } /** * 自動切替の通知表示 * * モデル廃止やプロバイダ障害で自動的に切り替えて生成した場合、 * 黙って動き続けると設定の誤りが放置されるため管理画面で知らせる。 */ public function render_auto_switch_notice() { if (!current_user_can('manage_options') || !class_exists('AAGL_Model_Registry')) { return; } $switches = AAGL_Model_Registry::get_auto_switches(); if (empty($switches)) { return; } foreach ($switches as $type => $info) { $label = ('provider' === $type) ? 'AIサービス' : 'AIモデル'; echo '

AIO Agent Article Generator: '; echo esc_html(sprintf( '%sを「%s」から「%s」へ自動的に切り替えて記事を生成しました(%s)。API設定画面で設定を見直してください。', $label, $info['from'] ?? '', $info['to'] ?? '', $info['time'] ?? '' )); echo '

'; } } /** * 管理画面スクリプト・スタイルの読み込み */ public function enqueue_admin_scripts($hook) { // AI記事生成プラグインページでのみ読み込み if (strpos($hook, 'aagl-') === false) { return; } // 記事生成ページでは追加で確認 // テンプレートページではメディアアップローダーを読み込み if (strpos($hook, 'aagl-templates') !== false) { wp_enqueue_media(); } // CSS読み込み wp_enqueue_style( 'aagl-admin-style', AAGL_ASSETS_URL . 'css/admin.css', array(), AAGL_VERSION ); // JavaScript読み込み wp_enqueue_script( 'aagl-admin-script', AAGL_ASSETS_URL . 'js/admin.js', array('jquery'), AAGL_VERSION, true ); // JavaScript用データをローカライズ wp_localize_script('aagl-admin-script', 'aagl_ajax_object', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('aagl_ajax_nonce'), 'strings' => array( 'confirm_delete' => __('本当に削除しますか?', 'aio-agent-article-generator'), 'generating' => __('生成中...', 'aio-agent-article-generator'), 'error' => __('エラーが発生しました', 'aio-agent-article-generator') ) )); } } /** * プラグインインスタンス取得関数 */ if (!function_exists('AAGL')) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid, Universal.Files.SeparateFunctionsFromOO.Mixed function AAGL() { return AIO_Article_Generator_Lite::get_instance(); } } // プラグイン開始 AAGL();